diff --git a/api/test_views.py b/api/test_views.py index f4f67b945..308e5ca65 100644 --- a/api/test_views.py +++ b/api/test_views.py @@ -89,11 +89,11 @@ def test_guest_user_permission(self): go_post_apis = [ "/api/v2/dref/", "/api/v2/dref-final-report/", - f"/api/v2/dref-final-report/{id}/publish/", + f"/api/v2/dref-final-report/{id}/approve/", "/api/v2/dref-op-update/", - f"/api/v2/dref-op-update/{id}/publish/", + f"/api/v2/dref-op-update/{id}/approve/", "/api/v2/dref-share/", - f"/api/v2/dref/{id}/publish/", + f"/api/v2/dref/{id}/approve/", "/api/v2/flash-update/", "/api/v2/flash-update-file/multiple/", "/api/v2/local-units/", diff --git a/dref/admin.py b/dref/admin.py index 9d1f9a111..9e1ceb57a 100644 --- a/dref/admin.py +++ b/dref/admin.py @@ -230,7 +230,7 @@ def get_queryset(self, request): # NOTE: If the Dref Final report is unpublished, set Dref related to it as active def save_model(self, request, obj, form, change): - if not obj.is_published and obj.dref: + if obj.status != Dref.Status.APPROVED and obj.dref: obj.dref.is_active = True obj.dref.save(update_fields=["is_active"]) super().save_model(request, obj, form, change) diff --git a/dref/filter_set.py b/dref/filter_set.py index c0eff4207..2e27ebf18 100644 --- a/dref/filter_set.py +++ b/dref/filter_set.py @@ -15,7 +15,7 @@ class DrefFilter(filters.FilterSet): class Meta: model = Dref - fields = ["is_published"] + fields = ["status"] class DrefOperationalUpdateFilter(filters.FilterSet): @@ -23,7 +23,7 @@ class DrefOperationalUpdateFilter(filters.FilterSet): class Meta: model = DrefOperationalUpdate - fields = ["is_published"] + fields = ["status"] class BaseDrefFilterSet(filters.FilterSet): diff --git a/dref/migrations/0084_dref_original_language_and_more.py b/dref/migrations/0084_dref_original_language_and_more.py new file mode 100644 index 000000000..976ef71a8 --- /dev/null +++ b/dref/migrations/0084_dref_original_language_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.19 on 2025-09-16 09:09 + +from django.db import migrations, models + + +def update_status(apps, schema_editor): + """ + Update old status to new workflow: + - Not published or IN_PROGRESS → DRAFT + - Published or COMPLETED → APPROVED + Applies to Dref, DrefOperationalUpdate, and DrefFinalReport. + """ + models = [ + "Dref", + "DrefOperationalUpdate", + "DrefFinalReport", + ] + for model_name in models: + Model = apps.get_model("dref", model_name) + Model.objects.filter(is_published=True).update(status=4) # Published → APPROVED + Model.objects.filter(is_published=False).update(status=1) # Not published → DRAFT + + +class Migration(migrations.Migration): + + dependencies = [ + ("dref", "0083_dreffinalreport_total_operation_timeframe_imminent"), + ] + + operations = [ + migrations.AddField( + model_name="dref", + name="original_language", + field=models.CharField( + blank=True, + help_text="The language in which this record was first created.", + null=True, + verbose_name="Original language", + ), + ), + migrations.AddField( + model_name="dreffinalreport", + name="original_language", + field=models.CharField( + blank=True, + help_text="The language in which this record was first created.", + null=True, + verbose_name="Original language", + ), + ), + migrations.AddField( + model_name="drefoperationalupdate", + name="original_language", + field=models.CharField( + blank=True, + help_text="The language in which this record was first created.", + null=True, + verbose_name="Original language", + ), + ), + migrations.AlterField( + model_name="dref", + name="status", + field=models.IntegerField( + choices=[(1, "Draft"), (2, "Finalizing"), (3, "Finalized"), (4, "Approved")], default=1, verbose_name="status" + ), + ), + migrations.AlterField( + model_name="dreffinalreport", + name="status", + field=models.IntegerField( + choices=[(1, "Draft"), (2, "Finalizing"), (3, "Finalized"), (4, "Approved")], default=1, verbose_name="status" + ), + ), + migrations.AlterField( + model_name="drefoperationalupdate", + name="status", + field=models.IntegerField( + choices=[(1, "Draft"), (2, "Finalizing"), (3, "Finalized"), (4, "Approved")], default=1, verbose_name="status" + ), + ), + migrations.RunPython(update_status, reverse_code=migrations.RunPython.noop), + ] diff --git a/dref/migrations/0085_remove_dref_is_published_and_more.py b/dref/migrations/0085_remove_dref_is_published_and_more.py new file mode 100644 index 000000000..ef89d7065 --- /dev/null +++ b/dref/migrations/0085_remove_dref_is_published_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.19 on 2025-09-16 09:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("dref", "0084_dref_original_language_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="dref", + name="is_published", + ), + migrations.RemoveField( + model_name="dreffinalreport", + name="is_published", + ), + migrations.RemoveField( + model_name="drefoperationalupdate", + name="is_published", + ), + ] diff --git a/dref/models.py b/dref/models.py index fbfc0e1c2..c62da3396 100644 --- a/dref/models.py +++ b/dref/models.py @@ -264,8 +264,14 @@ class DisasterCategory(models.IntegerChoices): RED = 2, _("Red") class Status(models.IntegerChoices): - IN_PROGRESS = 0, _("In Progress") - COMPLETED = 1, _("Completed") + DRAFT = 1, _("Draft") + """Draft: Initial stage content is being created and is not ready for review.""" + FINALIZING = 2, _("Finalizing") + """Finalizing: Content is in the translation process from the original language into English.""" + FINALIZED = 3, _("Finalized") + """Finalized: Translation is completed, content is ready for review, and updates to the original language are locked.""" + APPROVED = 4, _("Approved") + """Approved: The content has been reviewed, accepted, and is ready for use.""" created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True) modified_at = models.DateTimeField(verbose_name=_("modified at"), default=timezone.now, null=True) @@ -329,7 +335,13 @@ class Status(models.IntegerChoices): verbose_name=_("If available please upload additional support documentation for targeting strategy"), related_name="dref_targeting_strategy_support_file", ) - status = models.IntegerField(choices=Status.choices, verbose_name=_("status"), null=True, blank=True) + status = models.IntegerField(choices=Status.choices, verbose_name=_("status"), default=Status.DRAFT) + original_language = models.CharField( + blank=True, + null=True, + verbose_name=_("Original language"), + help_text="The language in which this record was first created.", + ) # NOTE: This field is set at creation with the active language. num_assisted = models.IntegerField(verbose_name=_("number of assisted"), blank=True, null=True) num_affected = models.IntegerField(verbose_name=_("number of affected"), blank=True, null=True) estimated_number_of_affected_male = models.IntegerField( @@ -645,10 +657,6 @@ class Status(models.IntegerChoices): verbose_name=_("cover image"), related_name="cover_image_dref", ) - is_published = models.BooleanField( - default=False, - verbose_name=_("Is published"), - ) is_final_report_created = models.BooleanField( default=False, verbose_name=_("Is final report created"), @@ -748,8 +756,6 @@ def save(self, *args, **kwargs): self.budget_file_preview.save(filename, thumb_data, save=False) else: raise ValidationError({"budget_file": "Sorry cannot generate preview for empty pdf"}) - - self.status = Dref.Status.COMPLETED if self.is_published else Dref.Status.IN_PROGRESS self.__budget_file_id = self.budget_file_id super().save(*args, **kwargs) @@ -859,7 +865,13 @@ class DrefOperationalUpdate(models.Model): disaster_category = models.IntegerField( choices=Dref.DisasterCategory.choices, verbose_name=_("disaster category"), null=True, blank=True ) - status = models.IntegerField(choices=Dref.Status.choices, verbose_name=_("status"), null=True, blank=True) + status = models.IntegerField(choices=Dref.Status.choices, verbose_name=_("status"), default=Dref.Status.DRAFT) + original_language = models.CharField( + blank=True, + null=True, + verbose_name=_("Original language"), + help_text="The language in which this record was first created.", + ) # NOTE: This field is set at creation with the active language. number_of_people_targeted = models.IntegerField(verbose_name=_("Number of people targeted"), blank=True, null=True) number_of_people_affected = models.IntegerField(verbose_name=_("number of people affected"), blank=True, null=True) estimated_number_of_affected_male = models.IntegerField( @@ -1094,10 +1106,6 @@ class DrefOperationalUpdate(models.Model): null=True, ) planned_interventions = models.ManyToManyField(PlannedIntervention, verbose_name=_("planned intervention"), blank=True) - is_published = models.BooleanField( - default=False, - verbose_name=_("Is published"), - ) country = models.ForeignKey( Country, verbose_name=_("country"), @@ -1232,7 +1240,6 @@ def save(self, *args, **kwargs): raise ValidationError({"budget_file": "Sorry cannot generate preview for empty pdf"}) self.__budget_file_id = self.budget_file_id - self.status = Dref.Status.COMPLETED if self.is_published else Dref.Status.IN_PROGRESS super().save(*args, **kwargs) @staticmethod @@ -1297,7 +1304,13 @@ class DrefFinalReport(models.Model): disaster_category = models.IntegerField( choices=Dref.DisasterCategory.choices, verbose_name=_("disaster category"), null=True, blank=True ) - status = models.IntegerField(choices=Dref.Status.choices, verbose_name=_("status"), null=True, blank=True) + status = models.IntegerField(choices=Dref.Status.choices, verbose_name=_("status"), default=Dref.Status.DRAFT) + original_language = models.CharField( + blank=True, + null=True, + verbose_name=_("Original language"), + help_text="The language in which this record was first created.", + ) # NOTE: This field is set at creation with the active language. number_of_people_targeted = models.IntegerField(verbose_name=_("Number of people targeted"), blank=True, null=True) number_of_people_affected = models.IntegerField(verbose_name=_("number of people affected"), blank=True, null=True) estimated_number_of_affected_male = models.IntegerField( @@ -1481,7 +1494,6 @@ class DrefFinalReport(models.Model): verbose_name=_("Additional National Societies Actions"), null=True, blank=True ) planned_interventions = models.ManyToManyField(PlannedIntervention, verbose_name=_("planned intervention"), blank=True) - is_published = models.BooleanField(verbose_name=_("Is Published"), default=False) country = models.ForeignKey( Country, verbose_name=_("country"), @@ -1651,12 +1663,11 @@ def save(self, *args, **kwargs): else: raise ValidationError({"financial_report": "Sorry cannot generate preview for empty pdf"}) - self.status = Dref.Status.COMPLETED if self.is_published else Dref.Status.IN_PROGRESS self.__financial_report_id = self.financial_report_id super().save(*args, **kwargs) @staticmethod - def get_for(user, is_published=False): + def get_for(user, status=None): from dref.utils import get_dref_users # get the user in dref @@ -1675,6 +1686,6 @@ def get_for(user, is_published=False): final_report_created_by = DrefFinalReport.objects.filter(created_by=user).distinct() union_query = final_report_users.union(final_report_created_by) queryset = DrefFinalReport.objects.filter(id__in=union_query.values("id")).distinct() - if is_published: - return queryset.filter(is_published=True) + if status == Dref.Status.APPROVED: + return queryset.filter(status=Dref.Status.APPROVED) return queryset diff --git a/dref/permissions.py b/dref/permissions.py index d22e40ecb..7757fdb96 100644 --- a/dref/permissions.py +++ b/dref/permissions.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import Permission from rest_framework import permissions -from dref.models import DrefFinalReport, DrefOperationalUpdate +from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate from dref.utils import get_dref_users @@ -42,6 +42,6 @@ def has_object_permission(self, request, view, obj): region = obj.country.region.name codename = f"dref_region_admin_{region}" user = request.user - if Permission.objects.filter(user=user, codename=codename).exists() and not obj.is_published: + if Permission.objects.filter(user=user, codename=codename).exists() and obj.status != Dref.Status.APPROVED: return True return False diff --git a/dref/serializers.py b/dref/serializers.py index 27d0b4ce0..c2ae5c085 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -144,7 +144,6 @@ class Meta: "country_details", "application_type", "application_type_display", - "is_published", "status", "status_display", "date_of_approval", @@ -170,7 +169,6 @@ class Meta: fields = [ "id", "title", - "is_published", "national_society", "disaster_type", "type_of_dref", @@ -213,7 +211,6 @@ class Meta: fields = [ "id", "title", - "is_published", "is_dref_imminent_v2", "national_society", "disaster_type", @@ -268,10 +265,10 @@ def get_application_type_display(self, obj) -> str: return "DREF application" def get_unpublished_op_update_count(self, obj) -> int: - return DrefOperationalUpdate.objects.filter(dref_id=obj.id, is_published=False).count() + return DrefOperationalUpdate.objects.filter(dref_id=obj.id).exclude(status=Dref.Status.APPROVED).count() def get_unpublished_final_report_count(self, obj) -> int: - return DrefFinalReport.objects.filter(dref_id=obj.id, is_published=False).count() + return DrefFinalReport.objects.filter(dref_id=obj.id).exclude(status=Dref.Status.APPROVED).count() class PlannedInterventionSerializer(ModelSerializer): @@ -356,7 +353,6 @@ class Meta: fields = [ "id", "title", - "is_published", "operational_update_number", ] @@ -367,7 +363,7 @@ class Meta: fields = [ "id", "title", - "is_published", + "status", ] @@ -476,10 +472,10 @@ def validate(self, data): ) } ) - if self.instance and self.instance.is_published: - raise serializers.ValidationError("Published Dref can't be changed. Please contact Admin") - if self.instance and DrefFinalReport.objects.filter(dref=self.instance, is_published=True).exists(): - raise serializers.ValidationError(gettext("Can't Update %s dref for publish Field Report" % self.instance.id)) + if self.instance and self.instance.status == Dref.Status.APPROVED: + raise serializers.ValidationError("Approved Dref can't be changed. Please contact Admin") + if self.instance and DrefFinalReport.objects.filter(dref=self.instance, status=Dref.Status.APPROVED).exists(): + raise serializers.ValidationError(gettext("Can't Update %s dref for approved Field Report" % self.instance.id)) if operation_timeframe and is_assessment_report and operation_timeframe > 30: raise serializers.ValidationError( gettext("Operation timeframe can't be greater than %s for assessment_report" % self.MAX_OPERATION_TIMEFRAME) @@ -739,18 +735,18 @@ class Meta: def validate(self, data): dref = data.get("dref") if not self.instance and dref: - if not dref.is_published: - raise serializers.ValidationError(gettext("Can't create Operational Update for not published %s dref." % dref.id)) + if dref.status != Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Can't create Operational Update for not approved %s dref." % dref.id)) # get the latest dref_operation_update and # check whether it is published or not, exclude no operational object created so far dref_operational_update = ( DrefOperationalUpdate.objects.filter(dref=dref).order_by("-operational_update_number").first() ) - if dref_operational_update and not dref_operational_update.is_published: + if dref_operational_update and dref_operational_update.status != Dref.Status.APPROVED: raise serializers.ValidationError( gettext( "Can't create Operational Update for not \ - published Operational Update %s id and Operational Update Number %i." + approved Operational Update %s id and Operational Update Number %i." % (dref_operational_update.id, dref_operational_update.operational_update_number) ) ) @@ -1129,22 +1125,21 @@ def validate(self, data): dref = data.get("dref") # Check if dref is published and operational_update associated with it is also published if not self.instance and dref: - if not dref.is_published: - raise serializers.ValidationError(gettext("Can't create Final Report for not published dref %s." % dref.id)) - dref_operational_update = DrefOperationalUpdate.objects.filter( - dref=dref, - is_published=False, - ).values_list("id", flat=True) + if dref.status != Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Can't create Final Report for not approved dref %s." % dref.id)) + dref_operational_update = DrefOperationalUpdate.objects.filter(dref=dref, status=Dref.Status.DRAFT).values_list( + "id", flat=True + ) if dref_operational_update: raise serializers.ValidationError( gettext( - "Can't create Final Report for not published Operational Update %s ids " + "Can't create Final Report for not approved Operational Update %s ids " % ",".join(map(str, dref_operational_update)) ) ) - if self.instance and self.instance.is_published: - raise serializers.ValidationError(gettext("Can't update published final report %s." % self.instance.id)) + if self.instance and self.instance.status == Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Can't update approved final report %s." % self.instance.id)) # NOTE: Validation for type DREF Imminent if self.instance and self.instance.is_dref_imminent_v2 and data.get("type_of_dref") == Dref.DrefType.IMMINENT: @@ -1229,7 +1224,9 @@ def create(self, validated_data): # else copy from dref dref = validated_data["dref"] dref_operational_update = ( - DrefOperationalUpdate.objects.filter(dref=dref, is_published=True).order_by("-operational_update_number").first() + DrefOperationalUpdate.objects.filter(dref=dref, status=Dref.Status.APPROVED) + .order_by("-operational_update_number") + .first() ) validated_data["created_by"] = self.context["request"].user # NOTE: Checks and common fields for the new dref final reports of new dref imminents @@ -1970,7 +1967,7 @@ def get_national_society_strengthening_people_targeted(self, obj): return sum([(p.person_targeted or 0) for p in obj.planned_interventions.all() if p.title == topic]) def get_approved(self, obj): - return "Yes" if obj.is_published else "No" + return "Yes" if obj.status == Dref.Status.APPROVED else "No" def get_indicators_nested_table(self, obj): return "???" diff --git a/dref/test_views.py b/dref/test_views.py index 24229e12c..001f042bd 100644 --- a/dref/test_views.py +++ b/dref/test_views.py @@ -121,7 +121,7 @@ def test_post_dref_creation(self, send_notification): "title": "Dref test title", "type_of_onset": Dref.OnsetType.SLOW.value, "disaster_category": Dref.DisasterCategory.YELLOW.value, - "status": Dref.Status.IN_PROGRESS.value, + "status": Dref.Status.DRAFT.value, "num_assisted": 5666, "num_affected": 23, "amount_requested": 127771111, @@ -237,7 +237,7 @@ def test_event_date_in_dref(self): "title": "Dref test title", "type_of_onset": Dref.OnsetType.SLOW.value, "disaster_category": Dref.DisasterCategory.YELLOW.value, - "status": Dref.Status.IN_PROGRESS.value, + "status": Dref.Status.DRAFT.value, "national_society": national_society.id, "num_assisted": 5666, "num_affected": 23, @@ -320,7 +320,7 @@ def test_event_date_in_dref(self): def test_update_dref_image(self): file1, file2, file3, file5 = DrefFileFactory.create_batch(4, created_by=self.user) file4 = DrefFileFactory.create(created_by=self.ifrc_user) - dref = DrefFactory.create(created_by=self.user) + dref = DrefFactory.create(created_by=self.user, status=Dref.Status.DRAFT) dref.users.add(self.ifrc_user) url = f"/api/v2/dref/{dref.id}/" data = { @@ -356,27 +356,27 @@ def test_filter_dref_status(self): """ Test to filter dref status """ - DrefFactory.create(title="test", status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) - DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) - DrefFactory.create(status=Dref.Status.COMPLETED, date_of_approval="2020-10-10", created_by=self.user) - DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) - DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) + DrefFactory.create(title="test", status=Dref.Status.APPROVED, date_of_approval="2020-10-10", created_by=self.user) + DrefFactory.create(status=Dref.Status.APPROVED, date_of_approval="2020-10-10", created_by=self.user) + DrefFactory.create(status=Dref.Status.APPROVED, date_of_approval="2020-10-10", created_by=self.user) + DrefFactory.create(status=Dref.Status.DRAFT, created_by=self.user) + DrefFactory.create(status=Dref.Status.DRAFT, created_by=self.user) # filter by `In Progress` - url = f"/api/v2/dref/?status={Dref.Status.IN_PROGRESS.value}" + url = f"/api/v2/dref/?status={Dref.Status.DRAFT.value}" self.client.force_authenticate(self.user) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data["results"]), 5) + self.assertEqual(len(response.data["results"]), 2) def test_dref_country_filter(self): - country1 = Country.objects.create(name="country1") - country2 = Country.objects.create(name="country2") - DrefFactory.create(title="test", status=Dref.Status.COMPLETED, created_by=self.user, country=country1) - DrefFactory.create(status=Dref.Status.COMPLETED, created_by=self.user) - DrefFactory.create(status=Dref.Status.COMPLETED, created_by=self.user, country=country2) - DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user, country=country1) - DrefFactory.create(status=Dref.Status.IN_PROGRESS, created_by=self.user) + country1 = Country.objects.create(name="Test country1") + country2 = Country.objects.create(name="Test country2") + DrefFactory.create(title="test", status=Dref.Status.APPROVED, created_by=self.user, country=country1) + DrefFactory.create(status=Dref.Status.APPROVED, created_by=self.user) + DrefFactory.create(status=Dref.Status.APPROVED, created_by=self.user, country=country2) + DrefFactory.create(status=Dref.Status.DRAFT, created_by=self.user, country=country1) + DrefFactory.create(status=Dref.Status.DRAFT, created_by=self.user) url = f"/api/v2/dref/?country={country1.id}" self.client.force_authenticate(self.user) response = self.client.get(url) @@ -384,11 +384,12 @@ def test_dref_country_filter(self): self.assertEqual(len(response.data["results"]), 2) @mock.patch("django.utils.timezone.now") - def test_dref_is_published(self, mock_now): + def test_dref_is_approved(self, mock_now): """ - Test for dref if is_published = True + Test DREF publishing flow: + - Can only approve when status=FINALIZED + - Approve sets status=APPROVED """ - initial_now = datetime.now() mock_now.return_value = initial_now @@ -397,9 +398,11 @@ def test_dref_is_published(self, mock_now): dref = DrefFactory.create( title="test", created_by=self.user, - is_published=False, + country=country, + status=Dref.Status.DRAFT, type_of_dref=Dref.DrefType.IMMINENT, ) + # Normal PATCH url = f"/api/v2/dref/{dref.id}/" data = { "title": "New Update Title", @@ -408,14 +411,14 @@ def test_dref_is_published(self, mock_now): self.client.force_authenticate(self.user) response = self.client.patch(url, data) self.assert_200(response) - - # create new dref with is_published = False - not_published_dref = DrefFactory.create( + # create new dref with status = DRAFT + not_approved_dref = DrefFactory.create( title="test", created_by=self.user, country=country, + status=Dref.Status.DRAFT, ) - url = f"/api/v2/dref/{not_published_dref.id}/" + url = f"/api/v2/dref/{not_approved_dref.id}/" self.client.force_authenticate(self.user) data["modified_at"] = initial_now + timedelta(days=10) response = self.client.patch(url, data) @@ -424,24 +427,29 @@ def test_dref_is_published(self, mock_now): data["modified_at"] = initial_now - timedelta(seconds=10) response = self.client.patch(url, data) self.assert_400(response) - - # test dref published endpoint - url = f"/api/v2/dref/{not_published_dref.id}/publish/" + # ---- Test publishing ---- + publish_url = f"/api/v2/dref/{dref.id}/approve/" data = {} - self.client.force_authenticate(self.user) - response = self.client.post(url, data) + response = self.client.post(publish_url, data) self.assert_403(response) - - # add permission to request user + # Add permission to user self.dref_permission = Permission.objects.create( codename="dref_region_admin_0", content_type=ContentType.objects.get_for_model(Region), name="Dref Admin for 0", ) self.user.user_permissions.add(self.dref_permission) - response = self.client.post(url, data) + # Try again while DRAFT. Should fail(not finalized yet) + response = self.client.post(publish_url, data) + self.assert_400(response) + # Update status to FINALIZED, then publish should succeed + dref.status = Dref.Status.FINALIZED + dref.save(update_fields=["status"]) + dref.refresh_from_db() + response = self.client.post(publish_url, data) + dref.refresh_from_db() self.assert_200(response) - self.assertEqual(response.data["is_published"], True) + self.assertEqual(response.data["status"], Dref.Status.APPROVED) def test_dref_operation_update_create(self): """ @@ -451,7 +459,7 @@ def test_dref_operation_update_create(self): dref = DrefFactory.create( title="Test Title", created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) dref.users.add(user1) self.country1 = Country.objects.create(name="abc") @@ -474,7 +482,7 @@ def test_dref_operation_update_for_published_dref(self): dref = DrefFactory.create( title="Test Title", created_by=self.user, - is_published=False, + status=Dref.Status.DRAFT, ) dref.users.add(user1) url = "/api/v2/dref-op-update/" @@ -488,10 +496,14 @@ def test_dref_operational_create_for_parent(self): dref = DrefFactory.create( title="Test Title", created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create(dref=dref, is_published=True, operational_update_number=1) + DrefOperationalUpdateFactory.create( + dref=dref, + status=Dref.Status.APPROVED, + operational_update_number=1, + ) data = { "dref": dref.id, } @@ -506,10 +518,14 @@ def test_operational_update_create_for_not_published_parent(self): dref = DrefFactory.create( title="Test Title", created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create(dref=dref, is_published=False, operational_update_number=1) + DrefOperationalUpdateFactory.create( + dref=dref, + status=Dref.Status.DRAFT, + operational_update_number=1, + ) data = { "dref": dref.id, } @@ -523,10 +539,14 @@ def test_dref_operational_update_patch(self): dref = DrefFactory.create( title="Test Title", created_by=user1, - is_published=True, + status=Dref.Status.APPROVED, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create(dref=dref, is_published=True, operational_update_number=1) + DrefOperationalUpdateFactory.create( + dref=dref, + status=Dref.Status.APPROVED, + operational_update_number=1, + ) url = "/api/v2/dref-op-update/" data = { "dref": dref.id, @@ -556,7 +576,7 @@ def test_dref_change_on_final_report_create(self): ) DrefFinalReportFactory.create( dref=dref, - is_published=True, + status=Dref.Status.APPROVED, ) # try to patch to dref data = { @@ -575,7 +595,7 @@ def test_dref_fields_copied_to_final_report(self): dref = DrefFactory.create( title="Test Title", created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, type_of_dref=Dref.DrefType.ASSESSMENT, ) dref.users.add(user1) @@ -599,12 +619,15 @@ def test_dref_operational_update_copied_to_final_report(self): dref = DrefFactory.create( title="Test Title", created_by=user1, - is_published=True, + status=Dref.Status.APPROVED, type_of_dref=Dref.DrefType.ASSESSMENT, ) dref.users.add(user1) operational_update = DrefOperationalUpdateFactory.create( - dref=dref, title="Operational Update Title", is_published=False, operational_update_number=1 + dref=dref, + title="Operational Update Title", + status=Dref.Status.DRAFT, + operational_update_number=1, ) old_count = DrefFinalReport.objects.count() url = "/api/v2/dref-final-report/" @@ -615,8 +638,8 @@ def test_dref_operational_update_copied_to_final_report(self): response = self.client.post(url, data=data) self.assert_400(response) # update the operational_update - operational_update.is_published = True - operational_update.save(update_fields=["is_published"]) + operational_update.status = Dref.Status.APPROVED + operational_update.save(update_fields=["status"]) response = self.client.post(url, data=data) self.assert_201(response) self.assertEqual(DrefFinalReport.objects.count(), old_count + 1) @@ -643,9 +666,9 @@ def test_final_report_for_dref(self): def test_update_dref_for_final_report_created(self): user1 = UserFactory.create() dref = DrefFactory.create( - title="Test Title", + title="Test Title1", created_by=user1, - is_published=True, + status=Dref.Status.APPROVED, type_of_dref=Dref.DrefType.IMMINENT, ) url = "/api/v2/dref-final-report/" @@ -664,18 +687,18 @@ def test_final_report_update_once_published(self): title="Test Title", type_of_dref=Dref.DrefType.ASSESSMENT, created_by=user1, - is_published=True, country=country, + status=Dref.Status.APPROVED, ) final_report = DrefFinalReportFactory( title="Test title", dref=dref, country=country, type_of_dref=Dref.DrefType.RESPONSE, + status=Dref.Status.FINALIZED, ) final_report.users.set([user1]) - # try to publish this report - url = f"/api/v2/dref-final-report/{final_report.id}/publish/" + url = f"/api/v2/dref-final-report/{final_report.id}/approve/" data = {} self.client.force_authenticate(user1) response = self.client.post(url, data) @@ -690,8 +713,7 @@ def test_final_report_update_once_published(self): self.client.force_authenticate(user1) response = self.client.post(url, data) self.assert_200(response) - self.assertEqual(response.data["is_published"], True) - + self.assertEqual(response.data["status"], Dref.Status.APPROVED) # now try to patch to the final report url = f"/api/v2/dref-final-report/{final_report.id}/" data = { @@ -709,7 +731,7 @@ def test_dref_for_assessment_report(self): "type_of_onset": Dref.OnsetType.SLOW.value, "type_of_dref": Dref.DrefType.ASSESSMENT, "disaster_category": Dref.DisasterCategory.YELLOW.value, - "status": Dref.Status.IN_PROGRESS.value, + "status": Dref.Status.DRAFT.value, "num_assisted": 5666, "num_affected": 23, "amount_requested": 127771111, @@ -833,7 +855,12 @@ def test_dref_for_super_user(self): self.assertEqual(len(response.data["results"]), 0) def test_dref_latest_update(self): - dref = DrefFactory.create(title="Test Title", created_by=self.user, modified_at=datetime(2022, 4, 18, 2, 29, 39, 793615)) + dref = DrefFactory.create( + title="Test Title", + created_by=self.user, + status=Dref.Status.DRAFT, + modified_at=datetime(2022, 4, 18, 2, 29, 39, 793615), + ) url = f"/api/v2/dref/{dref.id}/" data = { "title": "New title", @@ -861,10 +888,15 @@ def test_dref_op_update_locking(self): dref = DrefFactory.create( title="Test Title", created_by=user1, - is_published=True, + status=Dref.Status.APPROVED, ) dref.users.add(user1) - DrefOperationalUpdateFactory.create(dref=dref, is_published=True, operational_update_number=1, modified_at=datetime.now()) + DrefOperationalUpdateFactory.create( + dref=dref, + status=Dref.Status.APPROVED, + operational_update_number=1, + modified_at=datetime.now(), + ) url = "/api/v2/dref-op-update/" data = { "dref": dref.id, @@ -896,7 +928,7 @@ def test_optimistic_lock_in_final_report(self): dref = DrefFactory.create( title="Test Title", created_by=user1, - is_published=True, + status=Dref.Status.APPROVED, ) final_report = DrefFinalReportFactory( title="Test title", @@ -907,7 +939,7 @@ def test_optimistic_lock_in_final_report(self): # update data without `modified_at` data = {"title": "New Updated Title"} - self.authenticate(user1) + self.client.force_authenticate(user1) response = self.client.patch(url, data) self.assert_400(response) @@ -951,10 +983,7 @@ def test_dref_permission(self): password="admin123", email="user3@test.com", ) - dref1 = DrefFactory.create( - title="Test Title", - created_by=user1, - ) + dref1 = DrefFactory.create(title="Test Title", created_by=user1, status=Dref.Status.DRAFT) dref1.users.add(user2) DrefFactory.create(title="Test Title New", created_by=user3) get_url = "/api/v2/dref/" @@ -1003,7 +1032,7 @@ def test_superuser_permission_operational_update(self): ) dref = DrefFactory.create( title="Test Title", - is_published=True, + status=Dref.Status.APPROVED, ) self.country1 = Country.objects.create(name="abc") self.district1 = District.objects.create(name="test district1", country=self.country1) @@ -1021,7 +1050,11 @@ def test_superuser_permission_operational_update(self): def test_operational_update_view_permission(self): Dref.objects.all().delete() user1, user2, user3 = UserFactory.create_batch(3) - dref = DrefFactory.create(title="Test Title", is_published=True, created_by=user1) + dref = DrefFactory.create( + title="Test Title", + status=Dref.Status.APPROVED, + created_by=user1, + ) operational_update = DrefOperationalUpdateFactory.create( dref=dref, ) @@ -1054,8 +1087,14 @@ def test_operational_update_view_permission(self): def test_concurrent_dref_operational_update(self): user1, user2, user3 = UserFactory.create_batch(3) - dref = DrefFactory.create(title="Test Title", is_published=True, created_by=user1) - operational_update = DrefOperationalUpdateFactory.create(dref=dref, operational_update_number=1, created_by=user3) + dref = DrefFactory.create( + title="Test Title", + status=Dref.Status.APPROVED, + created_by=user1, + ) + operational_update = DrefOperationalUpdateFactory.create( + dref=dref, status=Dref.Status.DRAFT, operational_update_number=1, created_by=user3 + ) operational_update.users.set([user3, user2]) self.country1 = Country.objects.create(name="abc") self.district1 = District.objects.create(name="test district1", country=self.country1) @@ -1067,7 +1106,7 @@ def test_concurrent_dref_operational_update(self): self.assert_200(response) # create another operational update from corresponding dref - operation_update2 = DrefOperationalUpdateFactory.create(dref=dref, operational_update_number=2) + operation_update2 = DrefOperationalUpdateFactory.create(dref=dref, status=Dref.Status.DRAFT, operational_update_number=2) operation_update2.users.set([user2]) # authenticate with user2 @@ -1090,7 +1129,12 @@ def test_concurrent_dref_operational_update(self): def test_dref_type_loan(self): user1, _ = UserFactory.create_batch(2) - dref = DrefFactory.create(title="Test Title", created_by=self.user, is_published=True, type_of_dref=Dref.DrefType.LOAN) + dref = DrefFactory.create( + title="Test Title", + created_by=self.user, + status=Dref.Status.APPROVED, + type_of_dref=Dref.DrefType.LOAN, + ) dref.users.add(user1) old_count = DrefFinalReport.objects.count() url = "/api/v2/dref-final-report/" @@ -1181,10 +1225,21 @@ def test_completed_dref_operations(self): # create some dref final report DrefFinalReport.objects.all().delete() - DrefFinalReportFactory.create(is_published=True, country=country_1, type_of_dref=Dref.DrefType.ASSESSMENT) - DrefFinalReportFactory.create(is_published=True, country=country_3, type_of_dref=Dref.DrefType.ASSESSMENT) + DrefFinalReportFactory.create( + status=Dref.Status.APPROVED, + country=country_1, + type_of_dref=Dref.DrefType.ASSESSMENT, + ) + DrefFinalReportFactory.create( + status=Dref.Status.APPROVED, + country=country_3, + type_of_dref=Dref.DrefType.ASSESSMENT, + ) final_report_1, final_report_2 = DrefFinalReportFactory.create_batch( - 2, is_published=True, country=country_2, type_of_dref=Dref.DrefType.LOAN + 2, + status=Dref.Status.APPROVED, + country=country_2, + type_of_dref=Dref.DrefType.LOAN, ) DrefFinalReportFactory.create(country=country_2) @@ -1216,10 +1271,18 @@ def test_filter_active_dref(self): dref_2 = DrefFactory.create(is_active=True, type_of_dref=Dref.DrefType.LOAN, country=country_2, created_by=self.root_user) # some dref final report dref_final_report = DrefFinalReportFactory.create( - is_published=False, country=country_1, type_of_dref=Dref.DrefType.ASSESSMENT, dref=dref_1, created_by=self.root_user + status=Dref.Status.DRAFT, + country=country_1, + type_of_dref=Dref.DrefType.ASSESSMENT, + dref=dref_1, + created_by=self.root_user, ) DrefFinalReportFactory.create( - is_published=False, country=country_2, type_of_dref=Dref.DrefType.LOAN, dref=dref_2, created_by=self.root_user + status=Dref.Status.DRAFT, + country=country_2, + type_of_dref=Dref.DrefType.LOAN, + dref=dref_2, + created_by=self.root_user, ) url = "/api/v2/active-dref/" @@ -1297,7 +1360,7 @@ def test_dref_imminent(self): "type_of_onset": Dref.OnsetType.SUDDEN.value, "type_of_dref": Dref.DrefType.IMMINENT, "disaster_category": Dref.DisasterCategory.YELLOW.value, - "status": Dref.Status.IN_PROGRESS.value, + "status": Dref.Status.DRAFT.value, "num_assisted": 5666, "num_affected": 23, "amount_requested": 127771111, @@ -1523,7 +1586,7 @@ def test_dref_total_allocation_for_imminent(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, sub_total_cost=75000, indirect_cost=5000, total_cost=80000, @@ -1555,7 +1618,7 @@ def test_dref_total_allocation_for_imminent(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, sub_total_cost=75000, indirect_cost=5000, total_cost=80000, @@ -1577,7 +1640,7 @@ def test_dref_operation_imminent_create(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) self.country1 = Country.objects.create(name="abc") self.district1 = District.objects.create(name="test district1", country=self.country1) @@ -1600,7 +1663,7 @@ def test_dref_operation_imminent_create(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, is_dref_imminent_v2=True, ) data = { @@ -1637,7 +1700,7 @@ def test_dref_imminent_v2_final_report(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, is_dref_imminent_v2=True, sub_total_cost=75000, indirect_cost=5800, @@ -1719,7 +1782,7 @@ def test_dref_imminent_v2_final_report(self): title="Test Title", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, is_dref_imminent_v2=True, ) # Create Operational Update for Newly created Dref of type IMMINENT @@ -1727,7 +1790,7 @@ def test_dref_imminent_v2_final_report(self): dref=dref2, type_of_dref=Dref.DrefType.RESPONSE, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, operational_update_number=1, ) data = { @@ -1744,7 +1807,7 @@ def test_dref_imminent_v2_final_report(self): title="Test Title 2", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) response = self.client.post(url, data={"dref": dref3.id}) self.assert_201(response) @@ -1754,13 +1817,13 @@ def test_dref_imminent_v2_final_report(self): title="Test Title 3", type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, ) DrefOperationalUpdateFactory.create( dref=dref4, type_of_dref=Dref.DrefType.IMMINENT, created_by=self.user, - is_published=True, + status=Dref.Status.APPROVED, operational_update_number=1, ) response = self.client.post(url, data={"dref": dref4.id}) diff --git a/dref/views.py b/dref/views.py index 5275f71e2..5661ab22c 100644 --- a/dref/views.py +++ b/dref/views.py @@ -81,16 +81,19 @@ def get_queryset(self): @action( detail=True, - url_path="publish", + url_path="approve", methods=["post"], serializer_class=DrefSerializer, permission_classes=[permissions.IsAuthenticated, PublishDrefPermission, DenyGuestUserPermission], ) - def get_published(self, request, pk=None, version=None): + def get_approved(self, request, pk=None, version=None): dref = self.get_object() - dref.is_published = True - dref.status = Dref.Status.COMPLETED - dref.save(update_fields=["is_published", "status"]) + if dref.status != Dref.Status.FINALIZED: + raise serializers.ValidationError(gettext("Must be finalized before it can be approved")) + if dref.status == Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Dref %s is already approved" % dref)) + dref.status = Dref.Status.APPROVED + dref.save(update_fields=["status"]) serializer = DrefSerializer(dref, context={"request": request}) return response.Response(serializer.data) @@ -146,16 +149,19 @@ def get_queryset(self): @action( detail=True, - url_path="publish", + url_path="approve", methods=["post"], serializer_class=DrefOperationalUpdateSerializer, permission_classes=[permissions.IsAuthenticated, PublishDrefPermission, DenyGuestUserPermission], ) - def get_published(self, request, pk=None, version=None): + def get_approved(self, request, pk=None, version=None): operational_update = self.get_object() - operational_update.is_published = True - operational_update.status = Dref.Status.COMPLETED - operational_update.save(update_fields=["is_published", "status"]) + if operational_update.status != Dref.Status.FINALIZED: + raise serializers.ValidationError(gettext("Must be finalized before it can be approved.")) + if operational_update.status == Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Operational update %s is already approved" % operational_update)) + operational_update.status = Dref.Status.APPROVED + operational_update.save(update_fields=["status"]) serializer = DrefOperationalUpdateSerializer(operational_update, context={"request": request}) return response.Response(serializer.data) @@ -178,18 +184,19 @@ def get_queryset(self): @action( detail=True, - url_path="publish", + url_path="approve", methods=["post"], serializer_class=DrefFinalReportSerializer, permission_classes=[permissions.IsAuthenticated, PublishDrefPermission, DenyGuestUserPermission], ) - def get_published(self, request, pk=None, version=None): + def get_approved(self, request, pk=None, version=None): field_report = self.get_object() - if field_report.is_published: - raise serializers.ValidationError(gettext("Final Report %s is already published" % field_report)) - field_report.is_published = True - field_report.status = Dref.Status.COMPLETED - field_report.save(update_fields=["is_published", "status"]) + if field_report.status != Dref.Status.FINALIZED: + raise serializers.ValidationError(gettext("Must be finalized before it can be approved.")) + if field_report.status == Dref.Status.APPROVED: + raise serializers.ValidationError(gettext("Final Report %s is already approved" % field_report)) + field_report.status = Dref.Status.APPROVED + field_report.save(update_fields=["status"]) field_report.dref.is_active = False field_report.date_of_approval = timezone.now().date() field_report.dref.save(update_fields=["is_active", "date_of_approval"]) @@ -231,7 +238,7 @@ class CompletedDrefOperationsViewSet(viewsets.ReadOnlyModelViewSet): DenyGuestUserPermission, ] filterset_class = CompletedDrefOperationsFilterSet - queryset = DrefFinalReport.objects.filter(is_published=True).order_by("-created_at").distinct() + queryset = DrefFinalReport.objects.filter(status=Dref.Status.APPROVED).order_by("-created_at").distinct() def get_queryset(self): user = self.request.user