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-1770, POLIO-1768: delete unused scopes + migration #1837

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}" + (f" - {vaccine}" if vaccine else "")
quang-le marked this conversation as resolved.
Show resolved Hide resolved
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
37 changes: 37 additions & 0 deletions plugins/polio/migrations/0206_delete_unused_scopes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.16 on 2024-11-29 09:51

from django.db import migrations


def delete_unused_scopes(apps, schema_editor):
Round = apps.get_model("polio", "Round")
Campaign = apps.get_model("polio", "Campaign")
CampaignScope = apps.get_model("polio", "CampaignScope")
RoundScope = apps.get_model("polio", "RoundScope")

round_scopes = RoundScope.objects.all().prefetch_related("round", "round__campaign")
campaign_scopes = CampaignScope.objects.all().prefetch_related("campaign")

campaign_scopes_ids = []
round_scopes_ids = []

for scope in campaign_scopes:
if scope.campaign.separate_scopes_per_round:
campaign_scopes_ids.append(scope.id)
for scope in round_scopes:
# Rounds without campaigns should be deleted in separate migration
if not scope.round.campaign:
continue
if not scope.round.campaign.separate_scopes_per_round:
round_scopes_ids.append(scope.id)

CampaignScope.objects.filter(id__in=campaign_scopes_ids).delete()
RoundScope.objects.filter(id__in=round_scopes_ids).delete()
quang-le marked this conversation as resolved.
Show resolved Hide resolved


class Migration(migrations.Migration):
dependencies = [
("polio", "0205_remove_campaign_budget_requested_at_wfeditable_old_and_more"),
]

operations = [migrations.RunPython(delete_unused_scopes, migrations.RunPython.noop)]
quang-le marked this conversation as resolved.
Show resolved Hide resolved
55 changes: 54 additions & 1 deletion plugins/polio/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
from iaso.test import APITestCase
from plugins.polio.models import CampaignType, Round
from plugins.polio.preparedness.spreadsheet_manager import *
from plugins.polio.tests.api.test import PolioTestCaseMixin


class PolioAPITestCase(APITestCase):
class PolioAPITestCase(APITestCase, PolioTestCaseMixin):
data_source: m.DataSource
source_version_1: m.SourceVersion
org_unit: m.OrgUnit
Expand All @@ -27,6 +28,9 @@ def setUpTestData(cls):
cls.account = polio_account = Account.objects.create(name="polio", default_version=cls.source_version_1)
cls.yoda = cls.create_user_with_profile(username="yoda", account=polio_account, permissions=["iaso_forms"])

cls.country_type = m.OrgUnitType.objects.create(name="COUNTRY", short_name="country")
cls.district_type = m.OrgUnitType.objects.create(name="DISTRICT", short_name="district")

cls.org_unit = m.OrgUnit.objects.create(
org_unit_type=m.OrgUnitType.objects.create(name="Jedi Council", short_name="Cnc"),
version=cls.source_version_1,
Expand Down Expand Up @@ -548,6 +552,55 @@ def test_create_campaign_with_round_scopes(self):
],
)

def test_changing_scope_type_deletes_old_scopes(self):
# Create a new campaign with scope per campaign
test_campaign, _, _, _, _, _ = self.create_campaign(
obr_name="TEST_CAMPAIGN",
account=self.account,
source_version=self.source_version_1,
country_ou_type=self.country_type,
district_ou_type=self.district_type,
)

# Test that separate_scopes_per_round is False and campaign has scope
self.client.force_authenticate(self.yoda)
response = self.client.get(f"/api/polio/campaigns/{test_campaign.id}/")
data = self.assertJSONResponse(response, 200)
self.assertFalse(data["separate_scopes_per_round"])
self.assertEqual(len(data["scopes"]), 1)
self.assertEqual(len(data["scopes"][0]["group"]["org_units"]), 1)
for r in data["rounds"]:
self.assertEqual(len(r["scopes"]), 0)

old_payload = {**data}

# Format payload for campaign with round level scope (only on round 1)
new_round_1 = data["rounds"][0]
new_round_1["scopes"] = data["scopes"]
new_rounds = [new_round_1, data["rounds"][1], data["rounds"][2]]
payload = {**data, "separate_scopes_per_round": True, "rounds": new_rounds, "description": "Yabadabadoo"}

# Test that scope is on round and not on campaign
response = self.client.put(f"/api/polio/campaigns/{test_campaign.id}/", payload, format="json")
data = self.assertJSONResponse(response, 200)
self.assertTrue(data["separate_scopes_per_round"])
self.assertEqual(len(data["scopes"]), 0)
self.assertEqual(len(data["rounds"][0]["scopes"]), 1)
self.assertEqual(len(data["rounds"][0]["scopes"][0]["group"]["org_units"]), 1)
self.assertEqual(data["description"], "Yabadabadoo")
for index, r in enumerate(data["rounds"]):
if index > 0:
self.assertEqual(len(r["scopes"]), 0)

# Switch scope back to campaign level
response = self.client.put(f"/api/polio/campaigns/{test_campaign.id}/", old_payload, format="json")
data = self.assertJSONResponse(response, 200)
self.assertFalse(data["separate_scopes_per_round"])
self.assertEqual(len(data["scopes"]), 1)
self.assertEqual(len(data["scopes"][0]["group"]["org_units"]), 1)
for r in data["rounds"]:
self.assertEqual(len(r["scopes"]), 0)

@skip("Skipping as long as PATCH is disabled for campaigns")
def test_update_campaign_with_vaccine_data(self):
self.client.force_authenticate(self.yoda)
Expand Down
Loading