From 7fa34ece4a722bd1825d9421fc8c71c699ed1605 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Wed, 17 Sep 2025 12:01:46 +0545 Subject: [PATCH 1/3] refactor(dref): refactor is published to staus --- dref/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dref/serializers.py b/dref/serializers.py index c2ae5c085..10d687cee 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -364,6 +364,7 @@ class Meta: "id", "title", "status", + "status_display", ] From b7743cb6b1d5dcd49eb79fb19127b06c7eb8ce06 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Fri, 19 Sep 2025 20:59:36 +0545 Subject: [PATCH 2/3] feat(dref): add dref finalize api --- dref/admin.py | 3 + dref/serializers.py | 34 ++++++++- dref/tasks.py | 22 ++++++ dref/test_views.py | 177 +++++++++++++++++++++++++++++++++++++++++++- dref/views.py | 28 ++++++- lang/tasks.py | 13 ++-- 6 files changed, 267 insertions(+), 10 deletions(-) diff --git a/dref/admin.py b/dref/admin.py index 9e1ceb57a..1a06a8ece 100644 --- a/dref/admin.py +++ b/dref/admin.py @@ -98,6 +98,7 @@ class DrefAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdmin): "risk_security", "proposed_action", ) + readonly_fields = ("original_language",) def get_queryset(self, request): return ( @@ -143,6 +144,7 @@ class DrefOperationalUpdateAdmin(CompareVersionAdmin, TranslationAdmin, admin.Mo "district", "risk_security", ) + readonly_fields = ("original_language",) list_filter = ["dref"] def get_queryset(self, request): @@ -199,6 +201,7 @@ class DrefFinalReportAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdm "national_society_actions", "source_information", ) + readonly_fields = ("original_language",) list_filter = ["dref"] search_fields = ["title", "national_society__name", "appeal_code"] diff --git a/dref/serializers.py b/dref/serializers.py index 10d687cee..558a408f2 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from django.db import models, transaction from django.utils import timezone -from django.utils.translation import gettext +from django.utils.translation import get_language, gettext from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -35,6 +35,7 @@ ) from dref.utils import get_dref_users from lang.serializers import ModelSerializer +from main.translation import TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME from main.writable_nested_serializers import NestedCreateMixin, NestedUpdateMixin from utils.file_check import validate_file_type @@ -348,16 +349,22 @@ def get_image_url(self, identifiedneed) -> str: class MiniOperationalUpdateSerializer(ModelSerializer): + status_display = serializers.CharField(source="get_status_display", read_only=True) + class Meta: model = DrefOperationalUpdate fields = [ "id", "title", "operational_update_number", + "status", + "status_display", ] class MiniDrefFinalReportSerializer(ModelSerializer): + status_display = serializers.CharField(source="get_status_display", read_only=True) + class Meta: model = DrefFinalReport fields = [ @@ -368,7 +375,7 @@ class Meta: ] -class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer): +class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer): SUB_TOTAL_COST = 75000 SURGE_DEPLOYMENT_COST = 10000 SURGE_INDIRECT_COST = 5800 @@ -427,6 +434,7 @@ class Meta: "created_by", "budget_file_preview", "is_dref_imminent_v2", + "original_language", ) exclude = ( "cover_image", @@ -603,6 +611,12 @@ def validate_budget_file_preview(self, budget_file_preview): validate_file_type(budget_file_preview) return budget_file_preview + def _set_original_language(self, validated_data): + current_lang = get_language() + validated_data["original_language"] = current_lang + validated_data[TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME] = current_lang + return validated_data + def create(self, validated_data): validated_data["created_by"] = self.context["request"].user validated_data["is_active"] = True @@ -620,7 +634,7 @@ def create(self, validated_data): # Event Description validated_data["event_scope"] = None validated_data["identified_gaps"] = None - # Targeted Population + # Targeted Populationtranslate_model_fields_to_english validated_data["women"] = None validated_data["men"] = None validated_data["girls"] = None @@ -631,6 +645,8 @@ def create(self, validated_data): validated_data["communication"] = None dref_assessment_report = super().create(validated_data) dref_assessment_report.needs_identified.clear() + # set original language + validated_data = self._set_original_language(validated_data) return dref_assessment_report # NOTE: Setting flag for only newly created DREF of type IMMINENT if type_of_dref == Dref.DrefType.IMMINENT: @@ -640,6 +656,8 @@ def create(self, validated_data): to = {u.email for u in validated_data["users"]} else: to = None + # set original language + validated_data = self._set_original_language(validated_data) dref = super().create(validated_data) if to: transaction.on_commit(lambda: send_dref_email.delay(dref.id, list(to), "New")) @@ -649,6 +667,14 @@ def update(self, instance, validated_data): validated_data["modified_by"] = self.context["request"].user modified_at = validated_data.pop("modified_at", None) type_of_dref = validated_data.get("type_of_dref") + current_lang = get_language() + original_lang = instance.translation_module_original_language + if instance.status == Dref.Status.FINALIZED: + if current_lang != "en": + raise serializers.ValidationError(gettext("Finalized records can only be updated in English.")) + elif current_lang != original_lang: + raise serializers.ValidationError(gettext("Only original language is supported: %s" % original_lang)) + if modified_at is None: raise serializers.ValidationError({"modified_at": "Modified At is required!"}) if type_of_dref and type_of_dref == Dref.DrefType.ASSESSMENT: @@ -724,6 +750,7 @@ class Meta: "operational_update_number", "modified_by", "created_by", + "original_language", ) exclude = ( "images", @@ -1113,6 +1140,7 @@ class Meta: "created_by", "financial_report_preview", "is_dref_imminent_v2", + "original_language", ) exclude = ( "images", diff --git a/dref/tasks.py b/dref/tasks.py index 25a289817..fd3925ea1 100644 --- a/dref/tasks.py +++ b/dref/tasks.py @@ -1,11 +1,17 @@ +import logging + from celery import shared_task +from django.apps import apps as django_apps from django.template.loader import render_to_string +from lang.tasks import ModelTranslator from notifications.notification import send_notification from .models import Dref from .utils import get_email_context +logger = logging.getLogger(__name__) + @shared_task def send_dref_email(dref_id, users_emails, new_or_updated=""): @@ -20,3 +26,19 @@ def send_dref_email(dref_id, users_emails, new_or_updated=""): send_notification(email_subject, users_emails, email_body, email_type) return email_context + + +@shared_task() +def translate_fields_to_english(model_name: str, pk: int) -> None: + model = django_apps.get_model(model_name) + obj = model.objects.get(pk=pk) + try: + ModelTranslator().translate_model_fields_to_english(obj) + obj.status = Dref.Status.FINALIZED + obj.translation_module_original_language = "en" + obj.save(update_fields=["status", "translation_module_original_language"]) + except Exception as exc: + obj.status = Dref.Status.DRAFT + obj.save(update_fields=["status"]) + logger.warning(f"Translation failed for '{model_name} {pk}': {exc}", exc_info=True) + raise exc diff --git a/dref/test_views.py b/dref/test_views.py index 001f042bd..8a5e52669 100644 --- a/dref/test_views.py +++ b/dref/test_views.py @@ -7,6 +7,7 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core import management +from django.test import TestCase from rest_framework import status from api.models import Country, DisasterType, District, Region, RegionName @@ -27,7 +28,7 @@ DrefOperationalUpdate, ProposedAction, ) -from dref.tasks import send_dref_email +from dref.tasks import send_dref_email, translate_fields_to_english from main.test_case import APITestCase @@ -883,6 +884,158 @@ def test_dref_latest_update(self): # Title should be latest since modified_at is greater than modified_at in database self.assertEqual(response.data["title"], "New title") + def test_dref_create_and_update_in_local_language( + self, + ): + national_society = Country.objects.create(name="Test country xzz") + disaster_type = DisasterType.objects.create(name="Test country abc") + + data = { + "title": "Prueba de título Dref", + "type_of_onset": Dref.OnsetType.SLOW.value, + "disaster_category": Dref.DisasterCategory.YELLOW.value, + "status": Dref.Status.DRAFT.value, + "num_assisted": 5666, + "num_affected": 23, + "amount_requested": 127771111, + "emergency_appeal_planned": False, + "event_date": "2021-08-01", + "ns_respond_date": "2021-08-01", + "event_text": "Texto de prueba para la respuesta", + "did_ns_request_fund": False, + "lessons_learned": "Texto de prueba para lecciones aprendidas", + "complete_child_safeguarding_risk": True, + "child_safeguarding_risk_level": "Muy alto", + "event_description": "Texto de prueba para descripción del evento", + "anticipatory_actions": "Texto de prueba para acciones anticipatorias", + "event_scope": "Texto de prueba para alcance del evento", + "government_requested_assistance": False, + "government_requested_assistance_date": "2021-08-01", + "national_authorities": "Texto de prueba para autoridades nacionales", + "icrc": "Texto de prueba para lecciones aprendidas", + "un_or_other_actor": "Texto de prueba para lecciones aprendidas", + "major_coordination_mechanism": "Texto de prueba para lecciones aprendidas", + "identified_gaps": "Texto de prueba para lecciones aprendidas", + "people_assisted": "Texto de prueba para lecciones aprendidas", + "selection_criteria": "Texto de prueba para lecciones aprendidas", + "entity_affected": "Texto de prueba para lecciones aprendidas", + "community_involved": "Texto de prueba para lecciones aprendidas", + "women": 344444, + "men": 5666, + "girls": 22, + "boys": 344, + "disability_people_per": "12.45", + "people_per": "10.35", + "displaced_people": 234243, + "operation_objective": "Texto de prueba para objetivo de operación", + "response_strategy": "Texto de prueba para estrategia de respuesta", + "secretariat_service": "Texto de prueba para servicio de secretaría", + "national_society_strengthening": "", + "ns_request_date": "2021-07-01", + "submission_to_geneva": "2021-07-01", + "date_of_approval": "2021-07-01", + "end_date": "2021-07-05", + "publishing_date": "2021-08-01", + "operation_timeframe": 4, + "appeal_code": "J7876", + "glide_code": "ER878", + "appeal_manager_name": "Nombre de prueba", + "appeal_manager_email": "test@gmail.com", + "project_manager_name": "Nombre de prueba", + "project_manager_email": "test@gmail.com", + "national_society_contact_name": "Nombre de prueba", + "national_society_contact_email": "test@gmail.com", + "media_contact_name": "Nombre de prueba", + "media_contact_email": "test@gmail.com", + "ifrc_emergency_name": "Nombre de prueba", + "ifrc_emergency_email": "test@gmail.com", + "originator_name": "Nombre de prueba", + "originator_email": "test@gmail.com", + "national_society": national_society.id, + "disaster_type": disaster_type.id, + "needs_identified": [{"title": "shelter_housing_and_settlements", "description": "hola"}], + "planned_interventions": [ + { + "title": "shelter_housing_and_settlements", + "description": "matriz", + "budget": 23444, + "male": 12222, + "female": 2255, + "indicators": [ + { + "title": "título de prueba", + "actual": 21232, + "target": 44444, + } + ], + } + ], + } + + url = "/api/v2/dref/" + + self.client.force_authenticate(self.user) + response = self.client.post(url, data, format="json", HTTP_ACCEPT_LANGUAGE="es") + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data["original_language"], "es") + self.assertEqual(response.data["translation_module_original_language"], "es") + self.assertEqual(response.data["title"], "Prueba de título Dref") + # Test update + dref_id = response.data["id"] + url = f"/api/v2/dref/{dref_id}/" + # update in French + data_fr = {"title": "Titre en français", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_fr, format="json", HTTP_ACCEPT_LANGUAGE="fr") + self.assert_400(response) + + # update in Arabic + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_ar, format="json", HTTP_ACCEPT_LANGUAGE="ar") + self.assert_400(response) + + # update in English + data_en = {"title": "Updated title in English", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_en, format="json", HTTP_ACCEPT_LANGUAGE="en") + self.assert_400(response) + + @mock.patch("dref.tasks.translate_fields_to_english.delay") + def test_update_and_finalize_dref(self, mock_translate): + dref = DrefFactory.create( + title="Título original en español", + type_of_dref=Dref.DrefType.IMMINENT, + created_by=self.user, + status=Dref.Status.DRAFT, + translation_module_original_language="es", + ) + + url = f"/api/v2/dref/{dref.id}/" + self.client.force_authenticate(self.user) + # update in Spanish + data_es = {"title": "en español", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_es, HTTP_ACCEPT_LANGUAGE="es") + self.assert_200(response) + self.assertEqual(response.data["title"], "en español") + # update in French + data_fr = {"title": "Titre en français", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_fr, format="json", HTTP_ACCEPT_LANGUAGE="fr") + self.assert_400(response) + # update in Arabic + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_ar, format="json", HTTP_ACCEPT_LANGUAGE="ar") + self.assert_400(response) + # update in English + data_en = {"title": "Updated title in English", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_en, HTTP_ACCEPT_LANGUAGE="en") + self.assert_400(response) + + # Finalize DREF + with self.capture_on_commit_callbacks(execute=True): + finalize_url = f"/api/v2/dref/{dref.id}/finalize/" + response = self.client.post(finalize_url) + self.assert_200(response) + self.assertEqual(response.data["status"], Dref.Status.FINALIZING) + mock_translate.assert_called_once_with("dref.Dref", dref.id) + def test_dref_op_update_locking(self): user1, _ = UserFactory.create_batch(2) dref = DrefFactory.create( @@ -1852,6 +2005,28 @@ def test_dref_imminent_v2_final_report(self): ) +class TranslateFieldsToEnglishTaskTest(TestCase): + + def test_translate_fields_to_english_task(self): + dref = DrefFactory.create( + title="Titre en français", + type_of_dref=Dref.DrefType.IMMINENT, + status=Dref.Status.DRAFT, + translation_module_original_language="fr", + ) + + with mock.patch("dref.tasks.ModelTranslator.translate_model_fields_to_english") as mock_translate: + mock_translate.return_value = None + + # Call the task + translate_fields_to_english("dref.Dref", dref.pk) + # Reload object from DB + dref.refresh_from_db() + mock_translate.assert_called_once() + self.assertEqual(dref.status, Dref.Status.FINALIZED) + self.assertEqual(dref.translation_module_original_language, "en") + + User = get_user_model() diff --git a/dref/views.py b/dref/views.py index 5661ab22c..a7f5ab602 100644 --- a/dref/views.py +++ b/dref/views.py @@ -1,6 +1,6 @@ import django.utils.timezone as timezone from django.contrib.auth.models import Permission -from django.db import models +from django.db import models, transaction from django.templatetags.static import static from django.utils.translation import gettext from drf_spectacular.utils import extend_schema @@ -17,6 +17,7 @@ from rest_framework.exceptions import NotFound from reversion.views import RevisionMixin +from api.utils import get_model_name from dref.filter_set import ( ActiveDrefFilterSet, CompletedDrefOperationsFilterSet, @@ -41,6 +42,7 @@ DrefShareUserSerializer, MiniDrefSerializer, ) +from dref.tasks import translate_fields_to_english from main.permissions import DenyGuestUserPermission, UseBySuperAdminOnly @@ -97,6 +99,30 @@ def get_approved(self, request, pk=None, version=None): serializer = DrefSerializer(dref, context={"request": request}) return response.Response(serializer.data) + @action( + detail=True, + url_path="finalize", + methods=["post"], + serializer_class=DrefSerializer, + permission_classes=[permissions.IsAuthenticated, DenyGuestUserPermission], + ) + def finalize(self, request, pk=None, version=None): + dref = self.get_object() + if dref.status in [Dref.Status.FINALIZED, Dref.Status.APPROVED]: + raise serializers.ValidationError(gettext("Cannot be finalized because it is already %s") % dref.get_status_display()) + # NOTE: If the dref original language is English, skip the translation task and update the status. + if dref.translation_module_original_language == "en": + dref.status = Dref.Status.FINALIZED + dref.save(update_fields=["status"]) + serializer = DrefSerializer(dref, context={"request": request}) + return response.Response(serializer.data) + + dref.status = Dref.Status.FINALIZING + dref.save(update_fields=["status"]) + transaction.on_commit(lambda: translate_fields_to_english.delay(get_model_name(type(dref)), dref.pk)) + serializer = DrefSerializer(dref, context={"request": request}) + return response.Response(serializer.data) + @extend_schema(request=None, responses=DrefGlobalFilesSerializer) @action( detail=False, diff --git a/lang/tasks.py b/lang/tasks.py index 0ab8dbbfa..45d909d42 100644 --- a/lang/tasks.py +++ b/lang/tasks.py @@ -32,13 +32,13 @@ def __init__(self): def translator(self): return self.default_translator - def translate_fields_object(self, obj, field): + def translate_fields_object(self, obj, field, target_languages=None): initial_lang = getattr(obj, TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME) initial_value = getattr(obj, build_localized_fieldname(field, initial_lang), None) if not initial_value or not initial_lang: return - - for lang in AVAILABLE_LANGUAGES: + target_languages = target_languages or AVAILABLE_LANGUAGES + for lang in target_languages: lang_field = build_localized_fieldname(field, lang) value = getattr(obj, lang_field, None) if value: @@ -107,15 +107,18 @@ def get_translatable_fields(cls, model): skipped_fields = set(getattr(translation_options, "skip_fields", [])) return [field for field in translation_options.fields.keys() if field not in skipped_fields] - def translate_model_fields(self, obj, translatable_fields=None): + def translate_model_fields(self, obj, translatable_fields=None, target_languages=None): if skip_auto_translation(obj): return translatable_fields = translatable_fields or self.get_translatable_fields(type(obj)) update_fields = [] for field in translatable_fields: - update_fields.extend(list(self.translate_fields_object(obj, field))) + update_fields.extend(list(self.translate_fields_object(obj, field, target_languages))) obj.save(update_fields=update_fields) + def translate_model_fields_to_english(self, obj, translatable_fields=None): + return self.translate_model_fields(obj, translatable_fields=translatable_fields, target_languages=["en"]) + @classmethod def show_characters_counts(cls, only_models: typing.Optional[typing.List[models.Model]] = None): """ From 6745b93aed93aff3d57751a91d6ee94c651f8112 Mon Sep 17 00:00:00 2001 From: sudip-khanal Date: Tue, 23 Sep 2025 12:07:25 +0545 Subject: [PATCH 3/3] feat(dref): add finalize api for Dref operational update and final report --- dref/serializers.py | 27 +++++- dref/test_views.py | 208 ++++++++++++++++++++++++++++++++++++++++---- dref/views.py | 54 ++++++++++++ 3 files changed, 270 insertions(+), 19 deletions(-) diff --git a/dref/serializers.py b/dref/serializers.py index 558a408f2..ab2b59a0a 100644 --- a/dref/serializers.py +++ b/dref/serializers.py @@ -719,7 +719,7 @@ def update(self, instance, validated_data): return dref -class DrefOperationalUpdateSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer): +class DrefOperationalUpdateSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer): MAX_NUMBER_OF_IMAGES = 4 national_society_actions = NationalSocietyActionSerializer(many=True, required=False) needs_identified = IdentifiedNeedSerializer(many=True, required=False) @@ -804,6 +804,7 @@ def validate_images_file(self, images): def create(self, validated_data): dref = validated_data["dref"] + current_language = get_language() dref_operational_update = DrefOperationalUpdate.objects.filter(dref=dref).order_by("-operational_update_number").first() validated_data["created_by"] = self.context["request"].user if not dref_operational_update: @@ -927,7 +928,8 @@ def create(self, validated_data): validated_data["is_man_made_event"] = dref.is_man_made_event validated_data["event_text"] = dref.event_text validated_data["did_national_society"] = dref.did_national_society - + validated_data["original_language"] = current_language + validated_data[TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME] = current_language operational_update = super().create(validated_data) # XXX: Copy files from DREF (Only nested serialized fields) nested_serialized_file_fields = [ @@ -1097,6 +1099,15 @@ def create(self, validated_data): def update(self, instance, validated_data): validated_data["modified_by"] = self.context["request"].user modified_at = validated_data.pop("modified_at", None) + + current_lang = get_language() + original_lang = instance.translation_module_original_language + if instance.status == Dref.Status.FINALIZED: + if current_lang != "en": + raise serializers.ValidationError(gettext("Finalized records can only be updated in English.")) + elif current_lang != original_lang: + raise serializers.ValidationError(gettext("Only original language is supported: %s" % original_lang)) + if modified_at is None: raise serializers.ValidationError({"modified_at": "Modified At is required!"}) @@ -1106,7 +1117,7 @@ def update(self, instance, validated_data): return super().update(instance, validated_data) -class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer): +class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer): MAX_NUMBER_OF_PHOTOS = 4 SUB_TOTAL_COST = 75000 national_society_actions = NationalSocietyActionSerializer(many=True, required=False) @@ -1251,6 +1262,7 @@ def create(self, validated_data): # here check if there is operational update for corresponding dref # if yes copy from the latest operational update # else copy from dref + current_language = get_language() dref = validated_data["dref"] dref_operational_update = ( DrefOperationalUpdate.objects.filter(dref=dref, status=Dref.Status.APPROVED) @@ -1258,6 +1270,8 @@ def create(self, validated_data): .first() ) validated_data["created_by"] = self.context["request"].user + validated_data["original_language"] = current_language + validated_data[TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME] = current_language # NOTE: Checks and common fields for the new dref final reports of new dref imminents if dref.type_of_dref == Dref.DrefType.IMMINENT and dref.is_dref_imminent_v2: validated_data["is_dref_imminent_v2"] = True @@ -1540,6 +1554,13 @@ def create(self, validated_data): def update(self, instance, validated_data): modified_at = validated_data.pop("modified_at", None) + current_lang = get_language() + original_lang = instance.translation_module_original_language + if instance.status == Dref.Status.FINALIZED: + if current_lang != "en": + raise serializers.ValidationError(gettext("Finalized records can only be updated in English.")) + elif current_lang != original_lang: + raise serializers.ValidationError(gettext("Only original language is supported: %s" % original_lang)) if modified_at is None: raise serializers.ValidationError({"modified_at": "Modified At is required!"}) if modified_at and instance.modified_at and modified_at < instance.modified_at: diff --git a/dref/test_views.py b/dref/test_views.py index 8a5e52669..7ba963aca 100644 --- a/dref/test_views.py +++ b/dref/test_views.py @@ -7,7 +7,6 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core import management -from django.test import TestCase from rest_framework import status from api.models import Country, DisasterType, District, Region, RegionName @@ -28,7 +27,7 @@ DrefOperationalUpdate, ProposedAction, ) -from dref.tasks import send_dref_email, translate_fields_to_english +from dref.tasks import send_dref_email from main.test_case import APITestCase @@ -1076,6 +1075,105 @@ def test_dref_op_update_locking(self): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) + def test_create_and_update_operational_update(self): + self.country = Country.objects.create(name="test ops country") + self.district = District.objects.create(name="test ops dis", country=self.country) + + user1, user2 = UserFactory.create_batch(2) + dref = DrefFactory.create( + title="Test Title", created_by=user1, status=Dref.Status.APPROVED, translation_module_original_language="en" + ) + dref2 = DrefFactory.create( + title="Test Title", created_by=user1, status=Dref.Status.DRAFT, translation_module_original_language="ar" + ) + + ops_update_data = { + "dref": dref.id, + "country": self.country.id, + "district": [self.district.id], + } + ops_update_data2 = { + "dref": dref2.id, + "country": self.country.id, + "district": [self.district.id], + } + + self.authenticate(user1) + # Test create + + self.authenticate(user2) + url = "/api/v2/dref-op-update/" + response = self.client.post(url, data=ops_update_data2) + self.assert_400(response) + + url = "/api/v2/dref-op-update/" + response = self.client.post(url, data=ops_update_data, HTTP_ACCEPT_LANGUAGE="fr") + self.assert_201(response) + self.assertEqual(response.data["translation_module_original_language"], "fr") + ops_update_id = response.data["id"] + # Test Update + update_url = f"/api/v2/dref-op-update/{ops_update_id}/" + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_ar, HTTP_ACCEPT_LANGUAGE="ar") + self.assert_400(response) + # Update in Spanish + data_es = {"title": "Título en español", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_es, HTTP_ACCEPT_LANGUAGE="es") + self.assert_400(response) + + # Update in English + data_en = {"title": "Updated title in English", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_en, HTTP_ACCEPT_LANGUAGE="en") + self.assert_400(response) + + # Update in French + data_fr = {"title": "Titre en français", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_fr, HTTP_ACCEPT_LANGUAGE="fr") + self.assert_200(response) + self.assertEqual(response.data["title"], "Titre en français") + + @mock.patch("dref.tasks.translate_fields_to_english.delay") + def test_dref_operational_update_finalize(self, mock_translate): + # Create users + user1, user2 = UserFactory.create_batch(2) + dref = DrefFactory.create( + title="Test Title", + created_by=user1, + status=Dref.Status.APPROVED, + translation_module_original_language="en", + ) + dref.users.add(user1) + op_update = DrefOperationalUpdateFactory.create( + dref=dref, + status=Dref.Status.DRAFT, + operational_update_number=1, + modified_at=datetime.now(), + translation_module_original_language="ar", + ) + + url = f"/api/v2/dref-op-update/{op_update.id}/" + self.client.force_authenticate(user1) + + # Update in Arabic (original language) + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_ar, HTTP_ACCEPT_LANGUAGE="ar") + self.assert_200(response) + self.assertEqual(response.data["title"], "العنوان بالعربية") + + # Update in English + data_en = {"title": "Updated title in English", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_en, HTTP_ACCEPT_LANGUAGE="en") + self.assert_400(response) + + # Finalize Operational Update + with self.capture_on_commit_callbacks(execute=True): + finalize_url = f"/api/v2/dref-op-update/{op_update.id}/finalize/" + response = self.client.post(finalize_url) + + self.assert_200(response) + self.assertEqual(response.data["status"], Dref.Status.FINALIZING) + mock_translate.assert_called_once_with("dref.DrefOperationalUpdate", op_update.id) + def test_optimistic_lock_in_final_report(self): user1 = UserFactory.create() dref = DrefFactory.create( @@ -2004,27 +2102,105 @@ def test_dref_imminent_v2_final_report(self): }, ) + def test_create_and_update_final_report(self): + user1, user2 = UserFactory.create_batch(2) + dref = DrefFactory.create( + title="Test Title", + created_by=self.user, + status=Dref.Status.APPROVED, + type_of_dref=Dref.DrefType.ASSESSMENT, + ) + dref2 = DrefFactory.create( + title="Test Title", + created_by=self.user, + status=Dref.Status.DRAFT, + type_of_dref=Dref.DrefType.IMMINENT, + ) + dref.users.add(user1) + url = "/api/v2/dref-final-report/" + data = { + "dref": dref2.id, + } + + self.authenticate(self.user) + response = self.client.post(url, data=data, HTTP_ACCEPT_LANGUAGE="es") + self.assert_400(response) + + url = "/api/v2/dref-final-report/" + data = { + "dref": dref.id, + } + response = self.client.post(url, data=data, HTTP_ACCEPT_LANGUAGE="es") + self.assert_201(response) + self.assertEqual(response.data["translation_module_original_language"], "es") + final_report_id = response.data["id"] + # Test Update + update_url = f"/api/v2/dref-final-report/{final_report_id}/" + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_ar, HTTP_ACCEPT_LANGUAGE="ar") + self.assert_400(response) + + # Update in English + data_en = {"title": "Updated title in English", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_en, HTTP_ACCEPT_LANGUAGE="en") + self.assert_400(response) + + # Update in French + data_fr = {"title": "Titre en français", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_fr, HTTP_ACCEPT_LANGUAGE="fr") + self.assert_400(response) -class TranslateFieldsToEnglishTaskTest(TestCase): + # Update in Spanish (original language) + data_es = {"title": "Título en español", "modified_at": datetime.now()} + response = self.client.patch(update_url, data=data_es, HTTP_ACCEPT_LANGUAGE="es") + self.assert_200(response) + self.assertEqual(response.data["translation_module_original_language"], "es") + self.assertEqual(response.data["title"], "Título en español") - def test_translate_fields_to_english_task(self): + @mock.patch("dref.tasks.translate_fields_to_english.delay") + def test_dref_final_report_finalize(self, mock_translate): + region = Region.objects.create(name=RegionName.AFRICA) + country = Country.objects.create(name="Test country12", region=region) + # Create users + user1, user2 = UserFactory.create_batch(2) dref = DrefFactory.create( - title="Titre en français", - type_of_dref=Dref.DrefType.IMMINENT, + title="Test Title", + created_by=user1, + status=Dref.Status.APPROVED, + translation_module_original_language="en", + ) + dref.users.add(user1) + final_report = DrefFinalReportFactory( + title="Título en español", + dref=dref, + country=country, + type_of_dref=Dref.DrefType.RESPONSE, status=Dref.Status.DRAFT, - translation_module_original_language="fr", + translation_module_original_language="es", ) + self.client.force_authenticate(user1) + url = f"/api/v2/dref-final-report/{final_report.id}/" - with mock.patch("dref.tasks.ModelTranslator.translate_model_fields_to_english") as mock_translate: - mock_translate.return_value = None + # Update in Arabic + data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_ar, HTTP_ACCEPT_LANGUAGE="ar") + self.assert_400(response) - # Call the task - translate_fields_to_english("dref.Dref", dref.pk) - # Reload object from DB - dref.refresh_from_db() - mock_translate.assert_called_once() - self.assertEqual(dref.status, Dref.Status.FINALIZED) - self.assertEqual(dref.translation_module_original_language, "en") + # Update in Spanish (original language) + data_es = {"title": "Título en español", "modified_at": datetime.now()} + response = self.client.patch(url, data=data_es, HTTP_ACCEPT_LANGUAGE="es") + self.assert_200(response) + self.assertEqual(response.data["title"], "Título en español") + self.assertEqual(response.data["translation_module_original_language"], "es") + + # Finalize final-report + with self.capture_on_commit_callbacks(execute=True): + finalize_url = f"/api/v2/dref-final-report/{final_report.id}/finalize/" + response = self.client.post(finalize_url) + + self.assert_200(response) + self.assertEqual(response.data["status"], Dref.Status.FINALIZING) + mock_translate.assert_called_once_with("dref.DrefFinalReport", final_report.id) User = get_user_model() diff --git a/dref/views.py b/dref/views.py index a7f5ab602..f3d2f08b7 100644 --- a/dref/views.py +++ b/dref/views.py @@ -191,6 +191,34 @@ def get_approved(self, request, pk=None, version=None): serializer = DrefOperationalUpdateSerializer(operational_update, context={"request": request}) return response.Response(serializer.data) + @action( + detail=True, + url_path="finalize", + methods=["post"], + serializer_class=DrefOperationalUpdateSerializer, + permission_classes=[permissions.IsAuthenticated, DenyGuestUserPermission], + ) + def finalize(self, request, pk=None, version=None): + operational_update = self.get_object() + if operational_update.status in [Dref.Status.FINALIZED, Dref.Status.APPROVED]: + raise serializers.ValidationError( + gettext("Cannot be finalized because it is already %s") % operational_update.get_status_display() + ) + # NOTE: If the operational update original language is English, skip the translation task and update the status. + if operational_update.translation_module_original_language == "en": + operational_update.status = Dref.Status.FINALIZED + operational_update.save(update_fields=["status"]) + serializer = DrefOperationalUpdateSerializer(operational_update, context={"request": request}) + return response.Response(serializer.data) + + operational_update.status = Dref.Status.FINALIZING + operational_update.save(update_fields=["status"]) + transaction.on_commit( + lambda: translate_fields_to_english.delay(get_model_name(type(operational_update)), operational_update.pk) + ) + serializer = DrefOperationalUpdateSerializer(operational_update, context={"request": request}) + return response.Response(serializer.data) + class DrefFinalReportViewSet(RevisionMixin, viewsets.ModelViewSet): serializer_class = DrefFinalReportSerializer @@ -229,6 +257,32 @@ def get_approved(self, request, pk=None, version=None): serializer = DrefFinalReportSerializer(field_report, context={"request": request}) return response.Response(serializer.data) + @action( + detail=True, + url_path="finalize", + methods=["post"], + serializer_class=DrefFinalReportSerializer, + permission_classes=[permissions.IsAuthenticated, DenyGuestUserPermission], + ) + def finalize(self, request, pk=None, version=None): + field_report = self.get_object() + if field_report.status in [Dref.Status.FINALIZED, Dref.Status.APPROVED]: + raise serializers.ValidationError( + gettext("Cannot be finalized because it is already %s") % field_report.get_status_display() + ) + # NOTE: If the final report original language is English, skip the translation task and update the status. + if field_report.translation_module_original_language == "en": + field_report.status = Dref.Status.FINALIZED + field_report.save(update_fields=["status"]) + serializer = DrefFinalReportSerializer(field_report, context={"request": request}) + return response.Response(serializer.data) + + field_report.status = Dref.Status.FINALIZING + field_report.save(update_fields=["status"]) + transaction.on_commit(lambda: translate_fields_to_english.delay(get_model_name(type(field_report)), field_report.pk)) + serializer = DrefFinalReportSerializer(field_report, context={"request": request}) + return response.Response(serializer.data) + class DrefFileViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): permission_classes = [permissions.IsAuthenticated, DenyGuestUserPermission]