Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polio-1556 sub activities front end #1340

66 changes: 66 additions & 0 deletions docs/pages/dev/reference/campaigns/subactivities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SubActivity and SubActivityScope Models and APIs

## Models

### SubActivity

The `SubActivity` model represents a sub-activity within a round of a campaign. It has the following fields:

- `round`: A foreign key to the `Round` model, representing the round to which the sub-activity belongs.
- `name`: A string field representing the name of the sub-activity.
- `age_unit`: A choice field representing the unit of age targeted by the sub-activity. The choices are "Months" and "Years".
- `age_min`: An integer field representing the minimum age targeted by the sub-activity.
- `age_max`: An integer field representing the maximum age targeted by the sub-activity.
- `start_date`: A date field representing the start date of the sub-activity.
- `end_date`: A date field representing the end date of the sub-activity.

### SubActivityScope

The `SubActivityScope` model represents the scope of a sub-activity, including the selection of an organizational unit and the vaccines used. It has the following fields:

- `group`: A one-to-one field to the `Group` model, representing the group of organizational units for the sub-activity.
- `subactivity`: A foreign key to the `SubActivity` model, representing the sub-activity to which the scope belongs.
- `vaccine`: A choice field representing the vaccine used in the sub-activity. The choices are "mOPV2", "nOPV2", and "bOPV".

## APIs

The `SubActivity` API allows for the creation, retrieval, update, and deletion of sub-activities. The API endpoint is `/api/polio/campaigns_subactivities/`.

### Create

To create a new sub-activity, send a POST request to the endpoint with the following data:

```json
{
"round_number": <round_number>,
"campaign": <campaign_obr_name>,
"name": <subactivity_name>,
"start_date": <start_date>,
"end_date": <end_date>,
"scopes": [
{
"group": {
"name": <group_name>,
"org_units": [<org_unit_id>]
},
"vaccine": <vaccine_choice>
}
]
}
```

### Retrieve

To retrieve all sub-activities, send a GET request to the endpoint. To retrieve a specific sub-activity, send a GET request to `/api/polio/campaigns_subactivities/<subactivity_id>/`.

### Update

To update a sub-activity, send a PUT request to `/api/polio/campaigns_subactivities/<subactivity_id>/` with the new data.

### Delete

To delete a sub-activity, send a DELETE request to `/api/polio/campaigns_subactivities/<subactivity_id>/`.

## Permissions

Only authenticated users can interact with the `SubActivity` API. The user must belong to the same account as the campaign associated with the round of the sub-activity.
10 changes: 10 additions & 0 deletions hat/assets/js/apps/Iaso/domains/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ export type Plugin = {
export type Plugins = {
plugins: Plugin[];
};

export type PaginatedResponse<T> = {
hasPrevious?: boolean;
hasNext?: boolean;
count?: number;
page?: number;
pages?: number;
limit?: number;
results?: T[];
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { FunctionComponent } from 'react';
import { userHasOneOfPermissions } from '../../users/utils';
import { baseUrls } from '../../../constants/urls';
import * as Permission from '../../../utils/permissions';
import { useCurrentUser } from '../../../utils/usersUtils';
import MESSAGES from '../../assignments/messages';
import { SUBMISSIONS, SUBMISSIONS_UPDATE } from '../../../utils/permissions';
Expand Down Expand Up @@ -42,5 +41,4 @@ export const LinkToInstance: FunctionComponent<Props> = ({
iconSize={iconSize}
/>
);
color={color}
};
16 changes: 9 additions & 7 deletions hat/assets/js/apps/Iaso/hooks/useTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { useRedirectToReplace } from 'bluesquare-components';
import { Optional } from '../types/utils';

type UseTabsParams<T> = {
params: Record<string, Optional<string>>;
params?: Record<string, Optional<string>>;
defaultTab: T;
baseUrl: string;
baseUrl?: string;
};

// T should be a union type of the possible string values for the Tabs
Expand All @@ -32,11 +32,13 @@ export const useTabs = <T,>({

const handleChangeTab = useCallback(
(_event, newTab) => {
const newParams = {
...params,
tab: newTab,
};
redirectToReplace(baseUrl, newParams);
if (baseUrl && params) {
const newParams = {
...params,
tab: newTab,
};
redirectToReplace(baseUrl, newParams);
}
setTab(newTab);
},
[params, redirectToReplace, baseUrl],
Expand Down
28 changes: 28 additions & 0 deletions hat/assets/js/apps/Iaso/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
setTableSelection,
} from 'bluesquare-components';
import { Selection } from '../domains/orgUnits/types/selection';
import { useObjectState } from '../hooks/useObjectState';
import { PaginationParams } from '../types/general';

type UseTableSelection<T> = {
selection: Selection<T>;
Expand Down Expand Up @@ -74,3 +76,29 @@ export const useTableSelection = <T>(count?: number): UseTableSelection<T> => {
};
}, [handleSelectAll, handleTableSelection, handleUnselectAll, selection]);
};

const defaultInitialState: PaginationParams = {
order: '-updated_at',
page: '1',
pageSize: '10',
};

type TableState = {
params: PaginationParams;
// eslint-disable-next-line no-unused-vars
onTableParamsChange: (newParams: PaginationParams) => void;
};
export const useTableState = (initialState?: PaginationParams): TableState => {
const [tableState, setTableState] = useObjectState(
initialState ?? defaultInitialState,
);

const onTableParamsChange = useCallback(
(newParams: PaginationParams) => setTableState(newParams),
[setTableState],
);

return useMemo(() => {
return { params: tableState, onTableParamsChange };
}, [onTableParamsChange, tableState]);
};
35 changes: 35 additions & 0 deletions plugins/polio/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
Round,
RoundDateHistoryEntry,
SpreadSheetImport,
SubActivity,
SubActivityScope,
URLCache,
VaccineArrivalReport,
VaccineAuthorization,
Expand Down Expand Up @@ -289,6 +291,39 @@ class BudgetProcessAdmin(admin.ModelAdmin):
inlines = [RoundAdminInline, BudgetStepAdminInline]


class SubActivityScopeInline(admin.StackedInline):
model = SubActivityScope
extra = 0
show_change_link = True
fields = ("id", "group", "vaccine")
raw_id_fields = ("group",)


@admin.register(SubActivity)
class SubActivityAdmin(admin.ModelAdmin):
list_display = (
"name",
"start_date",
"end_date",
)
fields = ("name", "start_date", "end_date", "round")
raw_id_fields = ("round",)
inlines = [SubActivityScopeInline]

def save_related(self, request, form, formsets, change):
for formset in formsets:
if not formset.is_valid():
print(f"Formset errors: {formset.errors}")
else:
if formset.extra_forms:
for fo in formset.extra_forms:
if fo.is_valid():
fo.save()
else:
print(f"Form errors: {fo.errors}")
formset.save()


admin.site.register(RoundDateHistoryEntry)
admin.site.register(CountryUsersGroup)
admin.site.register(URLCache)
Expand Down
121 changes: 121 additions & 0 deletions plugins/polio/api/campaigns/subactivities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from django.shortcuts import get_object_or_404
from django_filters.rest_framework import DjangoFilterBackend # type: ignore
from rest_framework import permissions, serializers

from iaso.api.common import ModelViewSet
from iaso.models import Group
from plugins.polio.api.shared_serializers import GroupSerializer
from plugins.polio.models import Round, SubActivity, SubActivityScope


class SubActivityScopeSerializer(serializers.ModelSerializer):
class Meta:
model = SubActivityScope
fields = ["group", "vaccine"]

group = GroupSerializer(required=False)


class SubActivityCreateUpdateSerializer(serializers.ModelSerializer):
round_number = serializers.IntegerField(write_only=True, required=False)
campaign = serializers.CharField(write_only=True, required=False)
scopes = SubActivityScopeSerializer(many=True, required=False)

class Meta:
model = SubActivity
fields = [
"id",
"round_number",
"campaign",
"name",
"start_date",
"end_date",
"scopes",
"age_unit",
"age_min",
"age_max",
]

def create(self, validated_data):
round_number = validated_data.pop("round_number", None)
campaign = validated_data.pop("campaign", None)
scopes_data = validated_data.pop("scopes", [])

if round_number is not None and campaign is not None:
the_round = get_object_or_404(Round, campaign__obr_name=campaign, number=round_number)
validated_data["round"] = the_round

if self.context["request"].user.iaso_profile.account != the_round.campaign.account:
raise serializers.ValidationError(
"You do not have permission to create a SubActivity for this Campaign."
)

else:
raise serializers.ValidationError("Both round_number and campaign must be provided.")

sub_activity = super().create(validated_data)

for scope_data in scopes_data:
group_data = scope_data.pop("group")
group_data["source_version"] = self.context["request"].user.iaso_profile.account.default_version
group_org_units = group_data.pop("org_units", [])
group = Group.objects.create(**group_data)
group.org_units.set(group_org_units)
SubActivityScope.objects.create(subactivity=sub_activity, group=group, **scope_data)

return sub_activity

def update(self, instance, validated_data):
scopes_data = validated_data.pop("scopes", None)

if scopes_data is not None:
# Get the groups associated with the current scopes
groups_to_check = [scope.group for scope in instance.scopes.all()]

# Delete the scopes
instance.scopes.all().delete()

# Check if the groups are used by any other SubActivityScope
for group in groups_to_check:
if not SubActivityScope.objects.filter(group=group).exists():
group.delete()

for scope_data in scopes_data:
group_data = scope_data.pop("group")
group_data["source_version"] = self.context["request"].user.iaso_profile.account.default_version
group_org_units = group_data.pop("org_units", [])
group = Group.objects.create(**group_data)
group.org_units.set(group_org_units)
SubActivityScope.objects.create(subactivity=instance, group=group, **scope_data)

return super().update(instance, validated_data)


class SubActivityListDetailSerializer(serializers.ModelSerializer):
round_id = serializers.IntegerField(source="round.id", read_only=True)
scopes = SubActivityScopeSerializer(many=True)

class Meta:
model = SubActivity
fields = ["id", "round_id", "name", "start_date", "end_date", "scopes", "age_unit", "age_min", "age_max"]


class SubActivityViewSet(ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
http_method_names = ["get", "head", "options", "post", "delete", "put"]
model = SubActivity
filter_backends = [DjangoFilterBackend]
filterset_fields = {"round__campaign__obr_name": ["exact"], "round__id": ["exact"]}

def get_serializer_class(self):
if self.action in ["create", "update", "partial_update"]:
return SubActivityCreateUpdateSerializer
return SubActivityListDetailSerializer

def get_queryset(self):
return SubActivity.objects.filter(round__campaign__account=self.request.user.iaso_profile.account)

def check_object_permissions(self, request, obj):
super().check_object_permissions(request, obj)
if request.user.iaso_profile.account != obj.round.campaign.account:
self.permission_denied(request, message="Cannot access campaign")
2 changes: 2 additions & 0 deletions plugins/polio/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from plugins.polio.api.campaigns.campaign_groups import CampaignGroupViewSet
from plugins.polio.api.campaigns.campaigns import CampaignViewSet
from plugins.polio.api.campaigns.subactivities import SubActivityViewSet
from plugins.polio.api.campaigns.orgunits_per_campaigns import OrgUnitsPerCampaignViewset
from iaso.api.config import ConfigViewSet
from plugins.polio.api.country_user_groups import CountryUsersGroupViewSet
Expand Down Expand Up @@ -45,6 +46,7 @@
router = routers.SimpleRouter()
router.register(r"polio/orgunits", PolioOrgunitViewSet, basename="PolioOrgunit")
router.register(r"polio/campaigns", CampaignViewSet, basename="Campaign")
router.register(r"polio/campaigns_subactivities", SubActivityViewSet, basename="campaigns_subactivities")
router.register(r"polio/budget", BudgetProcessViewSet, basename="BudgetProcess")
router.register(r"polio/budgetsteps", BudgetStepViewSet, basename="BudgetStep")
router.register(r"polio/workflow", WorkflowViewSet, basename="BudgetWorkflow")
Expand Down
4 changes: 4 additions & 0 deletions plugins/polio/js/src/constants/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2392,6 +2392,10 @@ const MESSAGES = defineMessages({
defaultMessage: 'Go to campaign',
id: 'iaso.polio.lqas.goToCampaign',
},
subActivities: {
defaultMessage: 'Sub-activities',
id: 'iaso.polio.subActivities',
},
});

export default MESSAGES;
Loading
Loading