Skip to content

Commit 596b1f2

Browse files
Merge pull request #2582 from IFRCGo/fix/dref-related-field-translation
Update dref related field translation
2 parents 14a6f2f + 2d7bea6 commit 596b1f2

File tree

10 files changed

+304
-111
lines changed

10 files changed

+304
-111
lines changed

api/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,7 @@ def validate_pdf_file(self, pdf_file):
25542554
return pdf_file
25552555

25562556
def create(self, validated_data):
2557+
language = django_get_language()
25572558
export_id = validated_data.get("export_id")
25582559
export_type = validated_data.get("export_type")
25592560
country_id = validated_data.get("per_country")
@@ -2586,7 +2587,7 @@ def create(self, validated_data):
25862587
export.requested_at = timezone.now()
25872588
export.save(update_fields=["status", "requested_at"])
25882589

2589-
transaction.on_commit(lambda: generate_url.delay(export.url, export.id, user.id, title))
2590+
transaction.on_commit(lambda: generate_url.delay(export.url, export.id, user.id, title, language))
25902591
return export
25912592

25922593
def update(self, instance, validated_data):

api/tasks.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .utils import DebugPlaywright
2020

2121

22-
def build_storage_state(tmp_dir, user, token):
22+
def build_storage_state(tmp_dir, user, token, language="en"):
2323
temp_file = pathlib.Path(tmp_dir, "storage_state.json")
2424
temp_file.touch()
2525

@@ -40,7 +40,7 @@ def build_storage_state(tmp_dir, user, token):
4040
}
4141
),
4242
},
43-
{"name": "language", "value": json.dumps("en")}, # enforce all export to English
43+
{"name": "language", "value": json.dumps(language)},
4444
],
4545
}
4646
]
@@ -51,7 +51,7 @@ def build_storage_state(tmp_dir, user, token):
5151

5252

5353
@shared_task
54-
def generate_url(url, export_id, user, title):
54+
def generate_url(url, export_id, user, title, language):
5555
export = Export.objects.get(id=export_id)
5656
user = User.objects.get(id=user)
5757
token = Token.objects.filter(user=user).last()
@@ -88,11 +88,25 @@ def generate_url(url, export_id, user, title):
8888
with tempfile.TemporaryDirectory() as tmp_dir:
8989
with sync_playwright() as p:
9090
browser = p.chromium.connect(settings.PLAYWRIGHT_SERVER_URL)
91-
storage_state = build_storage_state(
92-
tmp_dir,
93-
user,
94-
token,
95-
)
91+
# NOTE: DREF Export use the language from request
92+
if export.export_type in [
93+
Export.ExportType.DREF,
94+
Export.ExportType.OPS_UPDATE,
95+
Export.ExportType.FINAL_REPORT,
96+
]:
97+
storage_state = build_storage_state(
98+
tmp_dir,
99+
user,
100+
token,
101+
language,
102+
)
103+
else:
104+
# NOTE: Other Export types use default language (en)
105+
storage_state = build_storage_state(
106+
tmp_dir,
107+
user,
108+
token,
109+
)
96110
context = browser.new_context(storage_state=storage_state)
97111
page = context.new_page()
98112
if settings.DEBUG_PLAYWRIGHT:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Generated by Django 4.2.19 on 2025-11-12 08:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dref', '0084_remove_dref_is_published_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='dreffinalreport',
15+
name='main_donors_ar',
16+
field=models.TextField(blank=True, null=True, verbose_name='Main Donors'),
17+
),
18+
migrations.AddField(
19+
model_name='dreffinalreport',
20+
name='main_donors_en',
21+
field=models.TextField(blank=True, null=True, verbose_name='Main Donors'),
22+
),
23+
migrations.AddField(
24+
model_name='dreffinalreport',
25+
name='main_donors_es',
26+
field=models.TextField(blank=True, null=True, verbose_name='Main Donors'),
27+
),
28+
migrations.AddField(
29+
model_name='dreffinalreport',
30+
name='main_donors_fr',
31+
field=models.TextField(blank=True, null=True, verbose_name='Main Donors'),
32+
),
33+
migrations.AddField(
34+
model_name='dreffinalreport',
35+
name='people_assisted_ar',
36+
field=models.TextField(blank=True, null=True, verbose_name='people assisted'),
37+
),
38+
migrations.AddField(
39+
model_name='dreffinalreport',
40+
name='people_assisted_en',
41+
field=models.TextField(blank=True, null=True, verbose_name='people assisted'),
42+
),
43+
migrations.AddField(
44+
model_name='dreffinalreport',
45+
name='people_assisted_es',
46+
field=models.TextField(blank=True, null=True, verbose_name='people assisted'),
47+
),
48+
migrations.AddField(
49+
model_name='dreffinalreport',
50+
name='people_assisted_fr',
51+
field=models.TextField(blank=True, null=True, verbose_name='people assisted'),
52+
),
53+
migrations.AddField(
54+
model_name='drefoperationalupdate',
55+
name='identified_gaps_ar',
56+
field=models.TextField(blank=True, help_text='Any identified gaps/limitations in the assessment', null=True, verbose_name='identified gaps'),
57+
),
58+
migrations.AddField(
59+
model_name='drefoperationalupdate',
60+
name='identified_gaps_en',
61+
field=models.TextField(blank=True, help_text='Any identified gaps/limitations in the assessment', null=True, verbose_name='identified gaps'),
62+
),
63+
migrations.AddField(
64+
model_name='drefoperationalupdate',
65+
name='identified_gaps_es',
66+
field=models.TextField(blank=True, help_text='Any identified gaps/limitations in the assessment', null=True, verbose_name='identified gaps'),
67+
),
68+
migrations.AddField(
69+
model_name='drefoperationalupdate',
70+
name='identified_gaps_fr',
71+
field=models.TextField(blank=True, help_text='Any identified gaps/limitations in the assessment', null=True, verbose_name='identified gaps'),
72+
),
73+
migrations.AddField(
74+
model_name='drefoperationalupdate',
75+
name='summary_of_change_ar',
76+
field=models.TextField(blank=True, null=True, verbose_name='Summary of change'),
77+
),
78+
migrations.AddField(
79+
model_name='drefoperationalupdate',
80+
name='summary_of_change_en',
81+
field=models.TextField(blank=True, null=True, verbose_name='Summary of change'),
82+
),
83+
migrations.AddField(
84+
model_name='drefoperationalupdate',
85+
name='summary_of_change_es',
86+
field=models.TextField(blank=True, null=True, verbose_name='Summary of change'),
87+
),
88+
migrations.AddField(
89+
model_name='drefoperationalupdate',
90+
name='summary_of_change_fr',
91+
field=models.TextField(blank=True, null=True, verbose_name='Summary of change'),
92+
),
93+
]

dref/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from main.fields import SecureFileField
1717

1818

19+
# NOTE: The list `TRANSLATABLE_RELATED_MODELS` is in dref/tasks.py contains models directly related to Dref.
20+
# If you add a new related model that should be translated, make sure to include it in this list.
21+
# Keeping it up-to-date is important for correct translation handling while performing the finalize action.
1922
@reversion.register()
2023
class NationalSocietyAction(models.Model):
2124
class Title(models.TextChoices):
@@ -50,7 +53,7 @@ class Meta:
5053
verbose_name_plural = _("national society actions")
5154

5255
def __str__(self) -> str:
53-
desc = self.description_en.replace("-", "").strip()[:60] + "..."
56+
desc = self.description.replace("-", "").strip()[:60] + "..."
5457
return "%d (%s) %s" % (self.id, self.title, desc)
5558

5659
@staticmethod

dref/permissions.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.contrib.auth.models import Permission
22
from rest_framework import permissions
33

4-
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
4+
from dref.models import DrefFinalReport, DrefOperationalUpdate
55
from dref.utils import get_dref_users
66

77

@@ -35,13 +35,24 @@ def has_object_permission(self, request, view, obj):
3535
return False
3636

3737

38-
class PublishDrefPermission(permissions.BasePermission):
39-
message = "You need to be regional admin to publish dref"
38+
class ApproveDrefPermission(permissions.BasePermission):
39+
message = "You need to be Superuser or Dref Regional admin to approve"
4040

4141
def has_object_permission(self, request, view, obj):
42-
region = obj.country.region.name
43-
codename = f"dref_region_admin_{region}"
42+
4443
user = request.user
45-
if Permission.objects.filter(user=user, codename=codename).exists() and obj.status != Dref.Status.APPROVED:
44+
region_id = obj.country.region_id
45+
46+
if user.is_superuser:
47+
return True
48+
49+
dref_region_admins_ids = [
50+
int(codename.replace("dref_region_admin_", ""))
51+
for codename in Permission.objects.filter(
52+
group__user=user,
53+
codename__startswith="dref_region_admin_",
54+
).values_list("codename", flat=True)
55+
]
56+
if region_id in dref_region_admins_ids:
4657
return True
4758
return False

dref/serializers.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from main.writable_nested_serializers import NestedCreateMixin, NestedUpdateMixin
3939
from utils.file_check import validate_file_type
4040

41-
from .tasks import send_dref_email
41+
from .tasks import _translate_related_objects, send_dref_email
4242

4343

4444
class RiskSecuritySerializer(ModelSerializer):
@@ -1087,6 +1087,14 @@ def create(self, validated_data):
10871087
operational_update.users.add(*dref_operational_update.users.all())
10881088
operational_update.risk_security.add(*dref_operational_update.risk_security.all())
10891089
operational_update.source_information.add(*dref_operational_update.source_information.all())
1090+
1091+
# NOTE: Sync related models with the starting language
1092+
if starting_langauge != "en":
1093+
_translate_related_objects(
1094+
instance=operational_update,
1095+
auto_translate=False,
1096+
language=starting_langauge,
1097+
)
10901098
return operational_update
10911099

10921100
def update(self, instance, validated_data):
@@ -1541,6 +1549,14 @@ def create(self, validated_data):
15411549
# also update is_final_report_created for dref
15421550
dref.is_final_report_created = True
15431551
dref.save(update_fields=["is_final_report_created"])
1552+
1553+
# NOTE: Sync related models with the starting language
1554+
if starting_langauge != "en":
1555+
_translate_related_objects(
1556+
instance=dref_final_report,
1557+
auto_translate=False,
1558+
language=starting_langauge,
1559+
)
15441560
return dref_final_report
15451561

15461562
def update(self, instance, validated_data):

dref/tasks.py

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@
66

77
from api.utils import get_model_name
88
from lang.tasks import translate_model_fields
9+
from main.translation import TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME
910
from notifications.notification import send_notification
1011

11-
from .models import Dref
12+
from .models import (
13+
Dref,
14+
DrefFile,
15+
IdentifiedNeed,
16+
NationalSocietyAction,
17+
PlannedIntervention,
18+
PlannedInterventionIndicators,
19+
ProposedAction,
20+
RiskSecurity,
21+
)
1222
from .utils import get_email_context
1323

1424
logger = logging.getLogger(__name__)
@@ -29,6 +39,20 @@ def send_dref_email(dref_id, users_emails, new_or_updated=""):
2939
return email_context
3040

3141

42+
# NOTE: Only the models directly related to Dref are included here.
43+
# The task will translate the fields of these models and update
44+
# `translation_module_original_language` to "en".
45+
TRANSLATABLE_RELATED_MODELS = [
46+
DrefFile,
47+
NationalSocietyAction,
48+
IdentifiedNeed,
49+
PlannedIntervention,
50+
RiskSecurity,
51+
ProposedAction,
52+
PlannedInterventionIndicators,
53+
]
54+
55+
3256
@shared_task
3357
def process_dref_translation(model_name, instance_pk):
3458
"""
@@ -54,7 +78,25 @@ def process_dref_translation(model_name, instance_pk):
5478
return False
5579

5680

57-
def _translate_related_objects(instance, visited=None):
81+
def _translate_related_objects(
82+
instance,
83+
visited=None,
84+
auto_translate=True,
85+
language="en",
86+
):
87+
"""
88+
Sync the relateable translation fields for the given model instance.
89+
This function ensures that the translation fields are updated correctly
90+
based on the current language settings.
91+
92+
Args:
93+
instance: The model instance whose related objects need to be translated.
94+
visited: A set to keep track of visited instances to avoid infinite recursion.
95+
auto_translate: A boolean indicating whether to auto-translate related objects.
96+
language: The language code to set for the original language field.
97+
98+
"""
99+
58100
if visited is None:
59101
visited = set()
60102

@@ -67,26 +109,28 @@ def _translate_related_objects(instance, visited=None):
67109
if not field.is_relation or field.auto_created:
68110
continue
69111

70-
try:
71-
related_value = getattr(instance, field.name, None)
72-
if related_value is None:
73-
continue
74-
75-
# Handle related objects
76-
if not field.many_to_many:
77-
if hasattr(related_value, "translation_module_original_language"):
78-
model_name = get_model_name(type(related_value))
79-
translate_model_fields(model_name, related_value.pk)
80-
_translate_related_objects(related_value, visited)
81-
82-
# Handle multiple related objects
83-
else:
84-
for related_obj in related_value.all():
85-
if hasattr(related_obj, "translation_module_original_language"):
86-
model_name = get_model_name(type(related_obj))
87-
translate_model_fields(model_name, related_obj.pk)
88-
_translate_related_objects(related_obj, visited)
89-
90-
except Exception as e:
91-
logger.warning(f"Error processing field {field.name}: {e}")
112+
related_model = field.related_model
113+
if related_model not in TRANSLATABLE_RELATED_MODELS:
92114
continue
115+
116+
related_value = getattr(instance, field.name, None)
117+
if related_value is None:
118+
continue
119+
120+
if not field.many_to_many:
121+
if hasattr(related_value, TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME):
122+
model_name = get_model_name(type(related_value))
123+
if auto_translate:
124+
translate_model_fields(model_name, related_value.id)
125+
related_value.translation_module_original_language = language
126+
related_value.save(update_fields=["translation_module_original_language"])
127+
_translate_related_objects(related_value, visited, auto_translate, language)
128+
else:
129+
for related_obj in related_value.all():
130+
if hasattr(related_obj, TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME):
131+
model_name = get_model_name(type(related_obj))
132+
if auto_translate:
133+
translate_model_fields(model_name, related_obj.id)
134+
related_obj.translation_module_original_language = language
135+
related_obj.save(update_fields=["translation_module_original_language"])
136+
_translate_related_objects(related_obj, visited, auto_translate, language)

0 commit comments

Comments
 (0)