Skip to content

Commit

Permalink
Merge pull request #1313 from BLSQ/POLIO-1540-create-new-tab-sub-acti…
Browse files Browse the repository at this point in the history
…vities-for-all-campaigns-polio-and-non-polio

POLIO-1540 Create new tab sub activities for all campaigns polio and non polio
  • Loading branch information
madewulf authored Jun 4, 2024
2 parents bd49828 + 3b7c90e commit d184c54
Show file tree
Hide file tree
Showing 41 changed files with 1,528 additions and 73 deletions.
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[];
};
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

0 comments on commit d184c54

Please sign in to comment.