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-1753: split pdf repository data per vaccine per round #1830

Merged
merged 25 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0bac718
POLIO-1753: show 1 line per round per vaccine in pdf repo
quang-le Nov 22, 2024
530d328
POLIO-1716: add methods to test mixins
quang-le Nov 14, 2024
ce6636d
Add method to filter rounds by vaccine
quang-le Nov 22, 2024
c50c63d
POLIO-1753: prefetch scopes
quang-le Nov 22, 2024
7b54023
Merge branch 'main' into POLIO-1753_split_pdf_data_per_vaccine_per_round
quang-le Nov 26, 2024
09455b8
POLIO-1753: fix round duplication per vaccine
quang-le Nov 26, 2024
6d3a066
Merge branch 'add_test_methods' into POLIO-1753_split_pdf_data_per_va…
quang-le Nov 26, 2024
6d0cb7e
POLIO-1753: filter pdfs by vaccine name
quang-le Nov 26, 2024
5d018e8
Merge branch 'POLIO-1753_split_pdf_data_per_vaccine_per_round' of htt…
quang-le Nov 26, 2024
5ebb487
POLIO-1753: deduplicate mystery rounds
quang-le Nov 27, 2024
796bb0e
POLIO-1768: add migration to delete scopes
quang-le Nov 29, 2024
e5b0558
POLIO-1768: fix scopes migration
quang-le Nov 29, 2024
c134dc1
POLIO-1770: update campaigns API
quang-le Nov 29, 2024
b4f2172
black formatting
quang-le Nov 29, 2024
e1b97b3
POLIO-1770: add tests
quang-le Nov 29, 2024
d38da17
POLIO-1769: Add warning modal for scopes
quang-le Nov 29, 2024
2fd8bc1
Update plugins/polio/migrations/0206_delete_unused_scopes.py
quang-le Dec 2, 2024
86dc974
Update plugins/polio/api/campaigns/campaigns.py
quang-le Dec 2, 2024
e5ca14a
Update plugins/polio/migrations/0206_delete_unused_scopes.py
quang-le Dec 2, 2024
b2cbcd5
POLIO-1770: fix black formatting
quang-le Dec 2, 2024
af7b7fe
fix: delete unused scopes + migration
quang-le Dec 2, 2024
bbd88d6
POLIO-1753: use GROUP_BY query
quang-le Dec 2, 2024
182fa21
POLIO-1753: cleanup test and print statements
quang-le Dec 2, 2024
f6c0f29
fix: warn users when switching scope type
quang-le Dec 2, 2024
553fc1d
remove obsolete comment
quang-le Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 50 additions & 38 deletions plugins/polio/api/campaigns/campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,26 +293,35 @@ def update(self, instance: Campaign, validated_data):
rounds = validated_data.pop("rounds", [])
initial_org_unit = validated_data.get("initial_org_unit")
account = self.context["request"].user.iaso_profile.account
separate_scopes_per_round = validated_data.get("separate_scopes_per_round", instance.separate_scopes_per_round)
switch_to_scope_per_round = separate_scopes_per_round and not instance.separate_scopes_per_round
switch_to_scope_per_campaign = not separate_scopes_per_round and instance.separate_scopes_per_round
keep_scope_per_round = separate_scopes_per_round and instance.separate_scopes_per_round
keep_scope_per_campaign = not separate_scopes_per_round and not instance.separate_scopes_per_round

for scope in campaign_scopes:
vaccine = scope.get("vaccine", "")
org_units = scope.get("group", {}).get("org_units")
scope, created = instance.scopes.get_or_create(vaccine=vaccine)
source_version_id = None
name = f"scope for campaign {instance.obr_name}" + (f" - {vaccine}" if vaccine else "")
if org_units:
source_version_ids = set([ou.version_id for ou in org_units])
if len(source_version_ids) != 1:
raise serializers.ValidationError("All orgunit should be in the same source version")
source_version_id = list(source_version_ids)[0]
if not scope.group:
scope.group = Group.objects.create(name=name, source_version_id=source_version_id)
else:
scope.group.source_version_id = source_version_id
scope.group.name = name
scope.group.save()
if switch_to_scope_per_round and instance.scopes.exists():
instance.scopes.all().delete()

scope.group.org_units.set(org_units)
if switch_to_scope_per_campaign or keep_scope_per_campaign:
for scope in campaign_scopes:
vaccine = scope.get("vaccine", "")
org_units = scope.get("group", {}).get("org_units")
scope, created = instance.scopes.get_or_create(vaccine=vaccine)
source_version_id = None
name = f"scope for campaign {instance.obr_name} - {vaccine or ''}"
if org_units:
source_version_ids = set([ou.version_id for ou in org_units])
if len(source_version_ids) != 1:
raise serializers.ValidationError("All orgunit should be in the same source version")
source_version_id = list(source_version_ids)[0]
if not scope.group:
scope.group = Group.objects.create(name=name, source_version_id=source_version_id)
else:
scope.group.source_version_id = source_version_id
scope.group.name = name
scope.group.save()

scope.group.org_units.set(org_units)

round_instances = []
# find existing round either by id or number
Expand Down Expand Up @@ -355,27 +364,30 @@ def update(self, instance: Campaign, validated_data):
round_instance = round_serializer.save()
round_instances.append(round_instance)
round_datelogs = []
for scope in scopes:
vaccine = scope.get("vaccine", "")
org_units = scope.get("group", {}).get("org_units")
source_version_id = None
if org_units:
source_version_ids = set([ou.version_id for ou in org_units])
if len(source_version_ids) != 1:
raise serializers.ValidationError("All orgunit should be in the same source version")
source_version_id = list(source_version_ids)[0]
name = f"scope for round {round_instance.number} campaign {instance.obr_name}" + (
f" - {vaccine}" if vaccine else ""
)
scope, created = round_instance.scopes.get_or_create(vaccine=vaccine)
if not scope.group:
scope.group = Group.objects.create(name=name)
else:
scope.group.source_version_id = source_version_id
scope.group.name = name
scope.group.save()
if switch_to_scope_per_campaign and round.scopes.exists():
round.scopes.all().delete()
if switch_to_scope_per_round or keep_scope_per_round:
for scope in scopes:
vaccine = scope.get("vaccine", "")
org_units = scope.get("group", {}).get("org_units")
source_version_id = None
if org_units:
source_version_ids = set([ou.version_id for ou in org_units])
if len(source_version_ids) != 1:
raise serializers.ValidationError("All orgunit should be in the same source version")
source_version_id = list(source_version_ids)[0]
name = f"scope for round {round_instance.number} campaign {instance.obr_name}" + (
f" - {vaccine}" if vaccine else ""
)
scope, created = round_instance.scopes.get_or_create(vaccine=vaccine)
if not scope.group:
scope.group = Group.objects.create(name=name)
else:
scope.group.source_version_id = source_version_id
scope.group.name = name
scope.group.save()

scope.group.org_units.set(org_units)
scope.group.org_units.set(org_units)

# When some rounds need to be deleted, the payload contains only the rounds to keep.
# So we have to detect if somebody wants to delete a round to prevent deletion of
Expand Down
186 changes: 114 additions & 72 deletions plugins/polio/api/vaccines/repository.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""API endpoints and serializers for vaccine repository management."""

from datetime import datetime, timedelta

from django.db.models import Max, Min, OuterRef, Subquery
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
Expand All @@ -10,9 +9,11 @@
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet
from django.db.models import OuterRef, Subquery, Q, Value, Case, When, CharField

from iaso.api.common import Paginator
from plugins.polio.models import (
VACCINES,
CampaignType,
OutgoingStockMovement,
Round,
Expand All @@ -22,76 +23,13 @@
)


class VaccineRepositorySerializer(serializers.Serializer):
country_name = serializers.CharField(source="campaign.country.name")
campaign_obr_name = serializers.CharField(source="campaign.obr_name")
round_id = serializers.IntegerField(source="id")
round_number = serializers.IntegerField(source="number")
start_date = serializers.DateField(source="started_at")
end_date = serializers.DateField(source="ended_at")

vrf_data = serializers.SerializerMethodField()
pre_alert_data = serializers.SerializerMethodField()
form_a_data = serializers.SerializerMethodField()

def get_vrf_data(self, obj):
vrfs = VaccineRequestForm.objects.filter(campaign=obj.campaign, rounds=obj)
return [
{
"date": vrf.date_vrf_reception,
"file": vrf.document.url if vrf.document else None,
"is_missing": vrf.vrf_type == VaccineRequestFormType.MISSING,
"is_not_required": vrf.vrf_type == VaccineRequestFormType.NOT_REQUIRED,
"id": vrf.id,
}
for vrf in vrfs
]

def get_pre_alert_data(self, obj):
pre_alerts = VaccinePreAlert.objects.filter(request_form__campaign=obj.campaign, request_form__rounds=obj)
return [
{
"date": pa.date_pre_alert_reception,
"file": pa.document.url if pa.document else None,
"vrf_id": pa.request_form.id,
}
for pa in pre_alerts
]

def get_form_a_data(self, obj):
form_as = OutgoingStockMovement.objects.filter(campaign=obj.campaign, round=obj)
return [
{
"date": fa.form_a_reception_date,
"file": fa.document.url if fa.document else None,
"is_late": fa.form_a_reception_date > (obj.ended_at + timedelta(days=14))
if fa.form_a_reception_date and obj.ended_at
else None,
}
for fa in form_as
]


class VaccineReportingFilterBackend(filters.BaseFilterBackend):
"""Filter backend for vaccine reporting that handles campaign status, country, and file type filtering."""

def filter_queryset(self, request, queryset, view):
# Filter by campaign status
campaign_status = request.query_params.get("campaign_status", None)

# Get campaign dates subquery
campaign_dates = (
Round.objects.filter(campaign=OuterRef("campaign"))
.values("campaign")
.annotate(campaign_started_at=Min("started_at"), campaign_ended_at=Max("ended_at"))
)

# Add campaign dates to main queryset
queryset = queryset.annotate(
campaign_started_at=Subquery(campaign_dates.values("campaign_started_at")),
campaign_ended_at=Subquery(campaign_dates.values("campaign_ended_at")),
)

if campaign_status:
today = datetime.now().date()
if campaign_status.upper() == "ONGOING":
Expand Down Expand Up @@ -157,6 +95,65 @@ def filter_queryset(self, request, queryset, view):
return queryset.distinct()


class VaccineRepositorySerializer(serializers.Serializer):
country_name = serializers.CharField(source="campaign__country__name")
campaign_obr_name = serializers.CharField(source="campaign__obr_name")
round_id = serializers.IntegerField(source="id")
number = serializers.IntegerField()
start_date = serializers.DateField(source="started_at")
end_date = serializers.DateField(source="ended_at")
vaccine_name = serializers.CharField()

vrf_data = serializers.SerializerMethodField()
pre_alert_data = serializers.SerializerMethodField()
form_a_data = serializers.SerializerMethodField()

def get_vrf_data(self, obj):
vrfs = VaccineRequestForm.objects.filter(
campaign__id=obj["campaign__id"], rounds=obj["id"], vaccine_type=obj["vaccine_name"]
)
return [
{
"date": vrf.date_vrf_reception,
"file": vrf.document.url if vrf.document else None,
"is_missing": vrf.vrf_type == VaccineRequestFormType.MISSING,
"is_not_required": vrf.vrf_type == VaccineRequestFormType.NOT_REQUIRED,
"id": vrf.id,
}
for vrf in vrfs
]

def get_pre_alert_data(self, obj):
pre_alerts = VaccinePreAlert.objects.filter(
request_form__campaign=obj["campaign__id"],
request_form__rounds=obj["id"],
request_form__vaccine_type=obj["vaccine_name"],
)
return [
{
"date": pa.date_pre_alert_reception,
"file": pa.document.url if pa.document else None,
"vrf_id": pa.request_form.id,
}
for pa in pre_alerts
]

def get_form_a_data(self, obj):
form_as = OutgoingStockMovement.objects.filter(
campaign=obj["campaign__id"], round=obj["id"], vaccine_stock__vaccine=obj["vaccine_name"]
)
return [
{
"date": fa.form_a_reception_date,
"file": fa.document.url if fa.document else None,
"is_late": fa.form_a_reception_date > (obj["ended_at"] + timedelta(days=14))
if fa.form_a_reception_date and obj["ended_at"]
else None,
}
for fa in form_as
]


class VaccineRepositoryViewSet(GenericViewSet, ListModelMixin):
"""
ViewSet for retrieving vaccine repository data.
Expand All @@ -165,7 +162,7 @@ class VaccineRepositoryViewSet(GenericViewSet, ListModelMixin):
serializer_class = VaccineRepositorySerializer
pagination_class = Paginator
filter_backends = [OrderingFilter, SearchFilter, VaccineReportingFilterBackend]
ordering_fields = ["campaign__country__name", "campaign__obr_name", "started_at"]
ordering_fields = ["campaign__country__name", "campaign__obr_name", "started_at", "vaccine_name", "number"]
ordering = ["-started_at"]
search_fields = ["campaign__country__name", "campaign__obr_name"]
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
Expand Down Expand Up @@ -260,14 +257,59 @@ def get_queryset(self):
"""
Get the queryset for Round objects with their campaigns.
"""
rounds_queryset = (
Round.objects.filter(
campaign__isnull=False,
campaign__deleted_at__isnull=True,
campaign__campaign_types__name=CampaignType.POLIO,
)
.select_related(
"campaign",
"campaign__country",
)
.prefetch_related("scopes", "campaign__scopes")
)

rounds_queryset = Round.objects.filter(
campaign__isnull=False,
campaign__deleted_at__isnull=True,
campaign__campaign_types__name=CampaignType.POLIO,
).select_related(
"campaign",
"campaign__country",
# Get campaign dates subquery
campaign_dates = (
Round.objects.filter(campaign=OuterRef("campaign"))
.values("campaign")
.annotate(campaign_started_at=Min("started_at"), campaign_ended_at=Max("ended_at"))
)

# Add campaign dates to main queryset
rounds_queryset = rounds_queryset.annotate(
campaign_started_at=Subquery(campaign_dates.values("campaign_started_at")),
campaign_ended_at=Subquery(campaign_dates.values("campaign_ended_at")),
)

# This query assumes that campaign__scopes is empty if separate scopes per round and vice-versa
# Fixed in POLIO-1770
rounds_queryset = rounds_queryset.values(
"campaign__country__name",
"campaign__country__id",
"campaign__obr_name",
"campaign__scopes__id",
"campaign__scopes__vaccine",
"campaign__separate_scopes_per_round",
"id",
"campaign__id",
"started_at",
"ended_at",
"number",
"scopes__id",
"scopes__vaccine",
"campaign_started_at",
"campaign_ended_at",
)

# 393 results without filter
rounds_queryset = rounds_queryset.annotate(
vaccine_name=Case(
When(campaign__separate_scopes_per_round=False, then="campaign__scopes__vaccine"),
default="scopes__vaccine",
output_field=CharField(),
)
)

return rounds_queryset
20 changes: 20 additions & 0 deletions plugins/polio/js/src/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,26 @@ const MESSAGES = defineMessages({
id: 'iaso.polio.repository.title',
defaultMessage: 'Vaccine Management Repository',
},
scopesWillBeDeleted: {
id: 'iaso.polio.campaign.label.scopesWillBeDeleted',
defaultMessage: 'The previous scopes will be deleted',
},
scopeWarningTitle: {
id: 'iaso.polio.campaign.label.scopeWarningTitle',
defaultMessage: 'Scope type has been changed',
},
proceed: {
id: 'blsq.label.proceed',
defaultMessage: 'proceed',
},
doYouWantToClose: {
id: 'blsq.dialog.doYouWantToClose',
defaultMessage: 'Do you want to close?',
},
unsavedDataWillBeLost: {
id: 'blsq.dialog.unsavedDataWillBeLost',
defaultMessage: 'Unsaved data will be lost',
},
});

export default MESSAGES;
Loading
Loading