Skip to content

Commit 4801652

Browse files
committed
feat(dref): add dref finalize api
1 parent 8bdbfb2 commit 4801652

File tree

6 files changed

+267
-10
lines changed

6 files changed

+267
-10
lines changed

dref/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class DrefAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdmin):
9898
"risk_security",
9999
"proposed_action",
100100
)
101+
readonly_fields = ("original_language",)
101102

102103
def get_queryset(self, request):
103104
return (
@@ -143,6 +144,7 @@ class DrefOperationalUpdateAdmin(CompareVersionAdmin, TranslationAdmin, admin.Mo
143144
"district",
144145
"risk_security",
145146
)
147+
readonly_fields = ("original_language",)
146148
list_filter = ["dref"]
147149

148150
def get_queryset(self, request):
@@ -199,6 +201,7 @@ class DrefFinalReportAdmin(CompareVersionAdmin, TranslationAdmin, admin.ModelAdm
199201
"national_society_actions",
200202
"source_information",
201203
)
204+
readonly_fields = ("original_language",)
202205
list_filter = ["dref"]
203206
search_fields = ["title", "national_society__name", "appeal_code"]
204207

dref/serializers.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django.contrib.auth.models import User
77
from django.db import models, transaction
88
from django.utils import timezone
9-
from django.utils.translation import gettext
9+
from django.utils.translation import get_language, gettext
1010
from django.utils.translation import gettext_lazy as _
1111
from drf_spectacular.utils import extend_schema_field
1212
from rest_framework import serializers
@@ -35,6 +35,7 @@
3535
)
3636
from dref.utils import get_dref_users
3737
from lang.serializers import ModelSerializer
38+
from main.translation import TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME
3839
from main.writable_nested_serializers import NestedCreateMixin, NestedUpdateMixin
3940
from utils.file_check import validate_file_type
4041

@@ -348,16 +349,22 @@ def get_image_url(self, identifiedneed) -> str:
348349

349350

350351
class MiniOperationalUpdateSerializer(ModelSerializer):
352+
status_display = serializers.CharField(source="get_status_display", read_only=True)
353+
351354
class Meta:
352355
model = DrefOperationalUpdate
353356
fields = [
354357
"id",
355358
"title",
356359
"operational_update_number",
360+
"status",
361+
"status_display",
357362
]
358363

359364

360365
class MiniDrefFinalReportSerializer(ModelSerializer):
366+
status_display = serializers.CharField(source="get_status_display", read_only=True)
367+
361368
class Meta:
362369
model = DrefFinalReport
363370
fields = [
@@ -368,7 +375,7 @@ class Meta:
368375
]
369376

370377

371-
class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer):
378+
class DrefSerializer(NestedUpdateMixin, NestedCreateMixin, serializers.ModelSerializer):
372379
SUB_TOTAL_COST = 75000
373380
SURGE_DEPLOYMENT_COST = 10000
374381
SURGE_INDIRECT_COST = 5800
@@ -427,6 +434,7 @@ class Meta:
427434
"created_by",
428435
"budget_file_preview",
429436
"is_dref_imminent_v2",
437+
"original_language",
430438
)
431439
exclude = (
432440
"cover_image",
@@ -603,6 +611,12 @@ def validate_budget_file_preview(self, budget_file_preview):
603611
validate_file_type(budget_file_preview)
604612
return budget_file_preview
605613

614+
def _set_original_language(self, validated_data):
615+
current_lang = get_language()
616+
validated_data["original_language"] = current_lang
617+
validated_data[TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME] = current_lang
618+
return validated_data
619+
606620
def create(self, validated_data):
607621
validated_data["created_by"] = self.context["request"].user
608622
validated_data["is_active"] = True
@@ -620,7 +634,7 @@ def create(self, validated_data):
620634
# Event Description
621635
validated_data["event_scope"] = None
622636
validated_data["identified_gaps"] = None
623-
# Targeted Population
637+
# Targeted Populationtranslate_model_fields_to_english
624638
validated_data["women"] = None
625639
validated_data["men"] = None
626640
validated_data["girls"] = None
@@ -631,6 +645,8 @@ def create(self, validated_data):
631645
validated_data["communication"] = None
632646
dref_assessment_report = super().create(validated_data)
633647
dref_assessment_report.needs_identified.clear()
648+
# set original language
649+
validated_data = self._set_original_language(validated_data)
634650
return dref_assessment_report
635651
# NOTE: Setting flag for only newly created DREF of type IMMINENT
636652
if type_of_dref == Dref.DrefType.IMMINENT:
@@ -640,6 +656,8 @@ def create(self, validated_data):
640656
to = {u.email for u in validated_data["users"]}
641657
else:
642658
to = None
659+
# set original language
660+
validated_data = self._set_original_language(validated_data)
643661
dref = super().create(validated_data)
644662
if to:
645663
transaction.on_commit(lambda: send_dref_email.delay(dref.id, list(to), "New"))
@@ -649,6 +667,14 @@ def update(self, instance, validated_data):
649667
validated_data["modified_by"] = self.context["request"].user
650668
modified_at = validated_data.pop("modified_at", None)
651669
type_of_dref = validated_data.get("type_of_dref")
670+
current_lang = get_language()
671+
original_lang = instance.translation_module_original_language
672+
if instance.status == Dref.Status.FINALIZED:
673+
if current_lang != "en":
674+
raise serializers.ValidationError(gettext("Finalized records can only be updated in English."))
675+
elif current_lang != original_lang:
676+
raise serializers.ValidationError(gettext("Only original language is supported: %s" % original_lang))
677+
652678
if modified_at is None:
653679
raise serializers.ValidationError({"modified_at": "Modified At is required!"})
654680
if type_of_dref and type_of_dref == Dref.DrefType.ASSESSMENT:
@@ -724,6 +750,7 @@ class Meta:
724750
"operational_update_number",
725751
"modified_by",
726752
"created_by",
753+
"original_language",
727754
)
728755
exclude = (
729756
"images",
@@ -1113,6 +1140,7 @@ class Meta:
11131140
"created_by",
11141141
"financial_report_preview",
11151142
"is_dref_imminent_v2",
1143+
"original_language",
11161144
)
11171145
exclude = (
11181146
"images",

dref/tasks.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import logging
2+
13
from celery import shared_task
4+
from django.apps import apps as django_apps
25
from django.template.loader import render_to_string
36

7+
from lang.tasks import ModelTranslator
48
from notifications.notification import send_notification
59

610
from .models import Dref
711
from .utils import get_email_context
812

13+
logger = logging.getLogger(__name__)
14+
915

1016
@shared_task
1117
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=""):
2026

2127
send_notification(email_subject, users_emails, email_body, email_type)
2228
return email_context
29+
30+
31+
@shared_task()
32+
def translate_fields_to_english(model_name: str, pk: int) -> None:
33+
model = django_apps.get_model(model_name)
34+
obj = model.objects.get(pk=pk)
35+
try:
36+
ModelTranslator().translate_model_fields_to_english(obj)
37+
obj.status = Dref.Status.FINALIZED
38+
obj.translation_module_original_language = "en"
39+
obj.save(update_fields=["status", "translation_module_original_language"])
40+
except Exception as exc:
41+
obj.status = Dref.Status.DRAFT
42+
obj.save(update_fields=["status"])
43+
logger.warning(f"Translation failed for '{model_name} {pk}': {exc}", exc_info=True)
44+
raise exc

dref/test_views.py

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.contrib.auth.models import Permission
88
from django.contrib.contenttypes.models import ContentType
99
from django.core import management
10+
from django.test import TestCase
1011
from rest_framework import status
1112

1213
from api.models import Country, DisasterType, District, Region, RegionName
@@ -27,7 +28,7 @@
2728
DrefOperationalUpdate,
2829
ProposedAction,
2930
)
30-
from dref.tasks import send_dref_email
31+
from dref.tasks import send_dref_email, translate_fields_to_english
3132
from main.test_case import APITestCase
3233

3334

@@ -883,6 +884,158 @@ def test_dref_latest_update(self):
883884
# Title should be latest since modified_at is greater than modified_at in database
884885
self.assertEqual(response.data["title"], "New title")
885886

887+
def test_dref_create_and_update_in_local_language(
888+
self,
889+
):
890+
national_society = Country.objects.create(name="Test country xzz")
891+
disaster_type = DisasterType.objects.create(name="Test country abc")
892+
893+
data = {
894+
"title": "Prueba de título Dref",
895+
"type_of_onset": Dref.OnsetType.SLOW.value,
896+
"disaster_category": Dref.DisasterCategory.YELLOW.value,
897+
"status": Dref.Status.DRAFT.value,
898+
"num_assisted": 5666,
899+
"num_affected": 23,
900+
"amount_requested": 127771111,
901+
"emergency_appeal_planned": False,
902+
"event_date": "2021-08-01",
903+
"ns_respond_date": "2021-08-01",
904+
"event_text": "Texto de prueba para la respuesta",
905+
"did_ns_request_fund": False,
906+
"lessons_learned": "Texto de prueba para lecciones aprendidas",
907+
"complete_child_safeguarding_risk": True,
908+
"child_safeguarding_risk_level": "Muy alto",
909+
"event_description": "Texto de prueba para descripción del evento",
910+
"anticipatory_actions": "Texto de prueba para acciones anticipatorias",
911+
"event_scope": "Texto de prueba para alcance del evento",
912+
"government_requested_assistance": False,
913+
"government_requested_assistance_date": "2021-08-01",
914+
"national_authorities": "Texto de prueba para autoridades nacionales",
915+
"icrc": "Texto de prueba para lecciones aprendidas",
916+
"un_or_other_actor": "Texto de prueba para lecciones aprendidas",
917+
"major_coordination_mechanism": "Texto de prueba para lecciones aprendidas",
918+
"identified_gaps": "Texto de prueba para lecciones aprendidas",
919+
"people_assisted": "Texto de prueba para lecciones aprendidas",
920+
"selection_criteria": "Texto de prueba para lecciones aprendidas",
921+
"entity_affected": "Texto de prueba para lecciones aprendidas",
922+
"community_involved": "Texto de prueba para lecciones aprendidas",
923+
"women": 344444,
924+
"men": 5666,
925+
"girls": 22,
926+
"boys": 344,
927+
"disability_people_per": "12.45",
928+
"people_per": "10.35",
929+
"displaced_people": 234243,
930+
"operation_objective": "Texto de prueba para objetivo de operación",
931+
"response_strategy": "Texto de prueba para estrategia de respuesta",
932+
"secretariat_service": "Texto de prueba para servicio de secretaría",
933+
"national_society_strengthening": "",
934+
"ns_request_date": "2021-07-01",
935+
"submission_to_geneva": "2021-07-01",
936+
"date_of_approval": "2021-07-01",
937+
"end_date": "2021-07-05",
938+
"publishing_date": "2021-08-01",
939+
"operation_timeframe": 4,
940+
"appeal_code": "J7876",
941+
"glide_code": "ER878",
942+
"appeal_manager_name": "Nombre de prueba",
943+
"appeal_manager_email": "[email protected]",
944+
"project_manager_name": "Nombre de prueba",
945+
"project_manager_email": "[email protected]",
946+
"national_society_contact_name": "Nombre de prueba",
947+
"national_society_contact_email": "[email protected]",
948+
"media_contact_name": "Nombre de prueba",
949+
"media_contact_email": "[email protected]",
950+
"ifrc_emergency_name": "Nombre de prueba",
951+
"ifrc_emergency_email": "[email protected]",
952+
"originator_name": "Nombre de prueba",
953+
"originator_email": "[email protected]",
954+
"national_society": national_society.id,
955+
"disaster_type": disaster_type.id,
956+
"needs_identified": [{"title": "shelter_housing_and_settlements", "description": "hola"}],
957+
"planned_interventions": [
958+
{
959+
"title": "shelter_housing_and_settlements",
960+
"description": "matriz",
961+
"budget": 23444,
962+
"male": 12222,
963+
"female": 2255,
964+
"indicators": [
965+
{
966+
"title": "título de prueba",
967+
"actual": 21232,
968+
"target": 44444,
969+
}
970+
],
971+
}
972+
],
973+
}
974+
975+
url = "/api/v2/dref/"
976+
977+
self.client.force_authenticate(self.user)
978+
response = self.client.post(url, data, format="json", HTTP_ACCEPT_LANGUAGE="es")
979+
self.assertEqual(response.status_code, 201)
980+
self.assertEqual(response.data["original_language"], "es")
981+
self.assertEqual(response.data["translation_module_original_language"], "es")
982+
self.assertEqual(response.data["title"], "Prueba de título Dref")
983+
# Test update
984+
dref_id = response.data["id"]
985+
url = f"/api/v2/dref/{dref_id}/"
986+
# update in French
987+
data_fr = {"title": "Titre en français", "modified_at": datetime.now()}
988+
response = self.client.patch(url, data=data_fr, format="json", HTTP_ACCEPT_LANGUAGE="fr")
989+
self.assert_400(response)
990+
991+
# update in Arabic
992+
data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()}
993+
response = self.client.patch(url, data=data_ar, format="json", HTTP_ACCEPT_LANGUAGE="ar")
994+
self.assert_400(response)
995+
996+
# update in English
997+
data_en = {"title": "Updated title in English", "modified_at": datetime.now()}
998+
response = self.client.patch(url, data=data_en, format="json", HTTP_ACCEPT_LANGUAGE="en")
999+
self.assert_400(response)
1000+
1001+
@mock.patch("dref.tasks.translate_fields_to_english.delay")
1002+
def test_update_and_finalize_dref(self, mock_translate):
1003+
dref = DrefFactory.create(
1004+
title="Título original en español",
1005+
type_of_dref=Dref.DrefType.IMMINENT,
1006+
created_by=self.user,
1007+
status=Dref.Status.DRAFT,
1008+
translation_module_original_language="es",
1009+
)
1010+
1011+
url = f"/api/v2/dref/{dref.id}/"
1012+
self.client.force_authenticate(self.user)
1013+
# update in Spanish
1014+
data_es = {"title": "en español", "modified_at": datetime.now()}
1015+
response = self.client.patch(url, data=data_es, HTTP_ACCEPT_LANGUAGE="es")
1016+
self.assert_200(response)
1017+
self.assertEqual(response.data["title"], "en español")
1018+
# update in French
1019+
data_fr = {"title": "Titre en français", "modified_at": datetime.now()}
1020+
response = self.client.patch(url, data=data_fr, format="json", HTTP_ACCEPT_LANGUAGE="fr")
1021+
self.assert_400(response)
1022+
# update in Arabic
1023+
data_ar = {"title": "العنوان بالعربية", "modified_at": datetime.now()}
1024+
response = self.client.patch(url, data=data_ar, format="json", HTTP_ACCEPT_LANGUAGE="ar")
1025+
self.assert_400(response)
1026+
# update in English
1027+
data_en = {"title": "Updated title in English", "modified_at": datetime.now()}
1028+
response = self.client.patch(url, data=data_en, HTTP_ACCEPT_LANGUAGE="en")
1029+
self.assert_400(response)
1030+
1031+
# Finalize DREF
1032+
with self.capture_on_commit_callbacks(execute=True):
1033+
finalize_url = f"/api/v2/dref/{dref.id}/finalize/"
1034+
response = self.client.post(finalize_url)
1035+
self.assert_200(response)
1036+
self.assertEqual(response.data["status"], Dref.Status.FINALIZING)
1037+
mock_translate.assert_called_once_with("dref.Dref", dref.id)
1038+
8861039
def test_dref_op_update_locking(self):
8871040
user1, _ = UserFactory.create_batch(2)
8881041
dref = DrefFactory.create(
@@ -1852,6 +2005,28 @@ def test_dref_imminent_v2_final_report(self):
18522005
)
18532006

18542007

2008+
class TranslateFieldsToEnglishTaskTest(TestCase):
2009+
2010+
def test_translate_fields_to_english_task(self):
2011+
dref = DrefFactory.create(
2012+
title="Titre en français",
2013+
type_of_dref=Dref.DrefType.IMMINENT,
2014+
status=Dref.Status.DRAFT,
2015+
translation_module_original_language="fr",
2016+
)
2017+
2018+
with mock.patch("dref.tasks.ModelTranslator.translate_model_fields_to_english") as mock_translate:
2019+
mock_translate.return_value = None
2020+
2021+
# Call the task
2022+
translate_fields_to_english("dref.Dref", dref.pk)
2023+
# Reload object from DB
2024+
dref.refresh_from_db()
2025+
mock_translate.assert_called_once()
2026+
self.assertEqual(dref.status, Dref.Status.FINALIZED)
2027+
self.assertEqual(dref.translation_module_original_language, "en")
2028+
2029+
18552030
User = get_user_model()
18562031

18572032

0 commit comments

Comments
 (0)