Skip to content

Commit aa8b32c

Browse files
committed
fix: redis lock handling and update translation check method
1 parent 4a70d41 commit aa8b32c

File tree

5 files changed

+113
-43
lines changed

5 files changed

+113
-43
lines changed

dref/test_views.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework import status
1212

1313
from api.models import Country, DisasterType, District, Region, RegionName
14+
from api.utils import get_model_name
1415
from deployments.factories.project import SectorFactory
1516
from deployments.factories.user import UserFactory
1617
from dref.factories.dref import (
@@ -999,9 +1000,9 @@ def test_dref_create_and_update_in_local_language(
9991000
response = self.client.patch(url, data=data_en, format="json", HTTP_ACCEPT_LANGUAGE="en")
10001001
self.assert_400(response)
10011002

1002-
@patch("dref.views.trigger_translation")
1003+
@patch("dref.views.translate_model_fields.delay")
10031004
@patch("dref.views.is_translation_complete")
1004-
def test_update_and_finalize_dref(self, mock_is_translation_complete, mock_trigger_translation):
1005+
def test_update_and_finalize_dref(self, mock_is_translation_complete, mock_translation):
10051006
dref = DrefFactory.create(
10061007
title="Título original en español",
10071008
type_of_dref=Dref.DrefType.IMMINENT,
@@ -1035,13 +1036,13 @@ def test_update_and_finalize_dref(self, mock_is_translation_complete, mock_trigg
10351036
mock_is_translation_complete.return_value = False
10361037
response = self.client.post(finalize_url)
10371038
self.assert_400(response)
1038-
mock_trigger_translation.assert_called_once_with(dref)
1039+
mock_translation.assert_called_once_with(get_model_name(type(dref)), dref.pk)
10391040
# Test finalize with Translation completion
1040-
mock_trigger_translation.reset_mock()
1041+
mock_translation.reset_mock()
10411042
mock_is_translation_complete.return_value = True
10421043
response = self.client.post(finalize_url)
10431044
self.assert_200(response)
1044-
mock_trigger_translation.assert_not_called()
1045+
mock_translation.assert_not_called()
10451046
self.assertEqual(response.data["status"], Dref.Status.FINALIZED)
10461047
self.assertEqual(response.data["translation_module_original_language"], "en")
10471048

@@ -1186,9 +1187,9 @@ def test_create_and_update_operational_update(self):
11861187
self.assert_200(response)
11871188
self.assertEqual(response.data["title"], "Titre en français")
11881189

1189-
@patch("dref.views.trigger_translation")
1190+
@patch("dref.views.translate_model_fields.delay")
11901191
@patch("dref.views.is_translation_complete")
1191-
def test_dref_operational_update_finalize(self, mock_is_translation_complete, mock_trigger_translation):
1192+
def test_dref_operational_update_finalize(self, mock_is_translation_complete, mock_translation):
11921193
# Create users
11931194
user1, user2 = UserFactory.create_batch(2)
11941195
dref = DrefFactory.create(
@@ -1226,13 +1227,13 @@ def test_dref_operational_update_finalize(self, mock_is_translation_complete, mo
12261227
mock_is_translation_complete.return_value = False
12271228
response = self.client.post(finalize_url)
12281229
self.assert_400(response)
1229-
mock_trigger_translation.assert_called_once_with(op_update)
1230+
mock_translation.assert_called_once_with(get_model_name(type(op_update)), op_update.pk)
12301231
# Test Finalize with translation complete
1231-
mock_trigger_translation.reset_mock()
1232+
mock_translation.reset_mock()
12321233
mock_is_translation_complete.return_value = True
12331234
response = self.client.post(finalize_url)
12341235
self.assert_200(response)
1235-
mock_trigger_translation.assert_not_called()
1236+
mock_translation.assert_not_called()
12361237
self.assertEqual(response.data["status"], Dref.Status.FINALIZED)
12371238
self.assertEqual(response.data["translation_module_original_language"], "en")
12381239
# Update in English
@@ -2245,9 +2246,9 @@ def test_create_and_update_final_report(self):
22452246
self.assertEqual(response.data["translation_module_original_language"], "es")
22462247
self.assertEqual(response.data["title"], "Título en español")
22472248

2248-
@patch("dref.views.trigger_translation")
2249+
@patch("dref.views.translate_model_fields.delay")
22492250
@patch("dref.views.is_translation_complete")
2250-
def test_dref_final_report_finalize(self, mock_is_translation_complete, mock_trigger_translation):
2251+
def test_dref_final_report_finalize(self, mock_is_translation_complete, mock_translation):
22512252
region = Region.objects.create(name=RegionName.AFRICA)
22522253
country = Country.objects.create(name="Test country12", region=region)
22532254
# Create users
@@ -2288,13 +2289,13 @@ def test_dref_final_report_finalize(self, mock_is_translation_complete, mock_tri
22882289
mock_is_translation_complete.return_value = False
22892290
response = self.client.post(finalize_url)
22902291
self.assert_400(response)
2291-
mock_trigger_translation.assert_called_once_with(final_report)
2292+
mock_translation.assert_called_once_with(get_model_name(type(final_report)), final_report.pk)
22922293
# Test finalize with Translation completion
2293-
mock_trigger_translation.reset_mock()
2294+
mock_translation.reset_mock()
22942295
mock_is_translation_complete.return_value = True
22952296
response = self.client.post(finalize_url)
22962297
self.assert_200(response)
2297-
mock_trigger_translation.assert_not_called()
2298+
mock_translation.assert_not_called()
22982299
self.assertEqual(response.data["status"], Dref.Status.FINALIZED)
22992300
self.assertEqual(response.data["translation_module_original_language"], "en")
23002301

dref/utils.py

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from modeltranslation.utils import build_localized_fieldname
88

99
from dref.models import Dref, DrefFinalReport, DrefOperationalUpdate
10-
from lang.tasks import translate_model_fields
11-
from main.lock import RedisLockKey, redis_lock
1210

1311
logger = logging.getLogger(__name__)
1412

@@ -63,34 +61,92 @@ def get_dref_users():
6361
return dref_users_list
6462

6563

66-
def is_translation_complete(instance, target_lang="en"):
64+
def is_translation_complete(instance, target_lang="en", visited=None):
6765
"""
68-
Check all translatable fields of a instance have been
69-
translated to the target language.
66+
Checks if instance and all related translatable fields are complete.
7067
"""
71-
original_lang = getattr(instance, "translation_module_original_language", None)
72-
if not original_lang:
68+
if visited is None:
69+
visited = set()
70+
71+
instance_id = id(instance)
72+
if instance_id in visited:
73+
return True
74+
75+
visited.add(instance_id)
76+
77+
if not _is_instance_translatable(instance):
78+
return False
79+
80+
if not _check_instance_fields(instance, target_lang):
7381
return False
82+
if not _check_related_fields(instance, target_lang, visited):
83+
return False
84+
return True
85+
86+
87+
def _is_instance_translatable(instance):
88+
"""Check if instance has translation capability."""
89+
return bool(getattr(instance, "translation_module_original_language", None))
90+
91+
92+
def _check_instance_fields(instance, target_lang):
93+
"""Check translatable fields on the instance itself."""
7494
try:
7595
opts = translator.get_options_for_model(type(instance))
7696
except Exception as e:
77-
logger.warning(f"Failed to get translation options {e}", exc_info=True)
97+
logger.warning(f"Failed to get translation options: {e}")
7898
return False
79-
for field in getattr(opts, "fields", []):
80-
original_value = getattr(instance, build_localized_fieldname(field, original_lang), None)
81-
translated_value = getattr(instance, build_localized_fieldname(field, target_lang), None)
99+
100+
original_lang = getattr(instance, "translation_module_original_language")
101+
translatable_fields = getattr(opts, "fields", [])
102+
103+
for field in translatable_fields:
104+
original_field = build_localized_fieldname(field, original_lang)
105+
target_field = build_localized_fieldname(field, target_lang)
106+
107+
original_value = getattr(instance, original_field, None)
108+
translated_value = getattr(instance, target_field, None)
109+
82110
if original_value and not translated_value:
83111
return False
112+
84113
return True
85114

86115

87-
def trigger_translation(instance):
88-
"""
89-
Trigger translation task with Redis lock.
90-
"""
91-
with redis_lock(key=RedisLockKey.DREF_TRANSLATION, id=instance.pk) as acquired:
92-
if not acquired:
93-
logger.warning(f"Translation already in progress for {instance._meta.label} (pk={instance.pk})")
94-
return
95-
translate_model_fields.delay(instance._meta.label, instance.pk)
96-
logger.info(f"Triggered translation task for {instance._meta.label} (pk={instance.pk})")
116+
def _check_related_fields(instance, target_lang, visited):
117+
"""Check all related translatable fields."""
118+
try:
119+
opts = translator.get_options_for_model(type(instance))
120+
except Exception as e:
121+
logger.warning(f"Failed to get translation options for related fields: {e}")
122+
return False
123+
124+
for related_field in getattr(opts, "related_fields", []):
125+
if not _check_related_field_translation(instance, related_field, target_lang, visited):
126+
return False
127+
128+
return True
129+
130+
131+
def _check_related_field_translation(instance, related_field, target_lang, visited):
132+
try:
133+
related_instance = getattr(instance, related_field.name, None)
134+
135+
if related_instance is None:
136+
return True
137+
138+
if hasattr(related_instance, "translation_module_original_language"):
139+
return is_translation_complete(related_instance, target_lang, visited)
140+
141+
elif hasattr(related_instance, "all"):
142+
for related_obj in related_instance.all():
143+
if hasattr(related_obj, "translation_module_original_language"):
144+
if not is_translation_complete(related_obj, target_lang, visited):
145+
return False
146+
return True
147+
148+
except Exception as e:
149+
logger.warning(f"Error checking related field {related_field.name}: {e}")
150+
return False
151+
152+
return True

dref/views.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from rest_framework.exceptions import NotFound
1818
from reversion.views import RevisionMixin
1919

20+
from api.utils import get_model_name
2021
from dref.filter_set import (
2122
ActiveDrefFilterSet,
2223
CompletedDrefOperationsFilterSet,
@@ -41,7 +42,8 @@
4142
DrefShareUserSerializer,
4243
MiniDrefSerializer,
4344
)
44-
from dref.utils import is_translation_complete, trigger_translation
45+
from dref.utils import is_translation_complete
46+
from lang.tasks import translate_model_fields
4547
from main.permissions import DenyGuestUserPermission, UseBySuperAdminOnly
4648

4749

@@ -110,7 +112,7 @@ def finalize(self, request, pk=None, version=None):
110112
if dref.status in [Dref.Status.FINALIZED, Dref.Status.APPROVED]:
111113
raise serializers.ValidationError(gettext("Cannot be finalized because it is already %s") % dref.get_status_display())
112114
if not is_translation_complete(dref):
113-
trigger_translation(dref)
115+
translate_model_fields.delay(get_model_name(type(dref)), dref.pk)
114116
raise serializers.ValidationError(
115117
gettext("The translation is currently being processed. Please wait a little while before trying again.")
116118
)
@@ -205,7 +207,7 @@ def finalize(self, request, pk=None, version=None):
205207
gettext("Cannot be finalized because it is already %s") % operational_update.get_status_display()
206208
)
207209
if not is_translation_complete(operational_update):
208-
trigger_translation(operational_update)
210+
translate_model_fields.delay(get_model_name(type(operational_update)), operational_update.pk)
209211
raise serializers.ValidationError(
210212
gettext("The translation is currently being processed. Please wait a little while before trying again.")
211213
)
@@ -270,7 +272,7 @@ def finalize(self, request, pk=None, version=None):
270272
gettext("Cannot be finalized because it is already %s") % field_report.get_status_display()
271273
)
272274
if not is_translation_complete(field_report):
273-
trigger_translation(field_report)
275+
translate_model_fields.delay(get_model_name(type(field_report)), field_report.pk)
274276
raise serializers.ValidationError(
275277
gettext("The translation is currently being processed. Please wait a little while before trying again.")
276278
)

lang/tasks.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from modeltranslation.utils import build_localized_fieldname
1414

1515
from main.celery import Queues
16+
from main.lock import RedisLockKey, redis_lock
1617
from main.translation import (
1718
TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME,
1819
TRANSLATOR_SKIP_FIELD_NAME,
@@ -203,7 +204,13 @@ def translate_remaining_models_fields():
203204
def translate_model_fields(model_name, pk):
204205
model = django_apps.get_model(model_name)
205206
obj = model.objects.get(pk=pk)
206-
ModelTranslator().translate_model_fields(obj)
207+
208+
with redis_lock(key=RedisLockKey.MODEL_TRANSLATION, id=pk, model_name=model_name) as acquired:
209+
if not acquired:
210+
logger.warning(f"Translation is already in progress for {model_name} with pk={pk}.")
211+
return
212+
ModelTranslator().translate_model_fields(obj)
213+
logger.info(f"Translation success for {model_name} with pk={pk}.")
207214

208215

209216
@shared_task(queue=Queues.HEAVY)

main/lock.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@ class RedisLockKey(models.TextChoices):
1919

2020
OPERATION_LEARNING_SUMMARY = _BASE + "-operation-learning-summary-{0}"
2121
OPERATION_LEARNING_SUMMARY_EXPORT = _BASE + "-operation-learning-summary-export-{0}"
22-
DREF_TRANSLATION = _BASE + "-dref-translation-{0}"
22+
MODEL_TRANSLATION = _BASE + "-{model_name}-translation-{id}"
2323

2424

2525
@contextmanager
2626
def redis_lock(
2727
key: RedisLockKey,
2828
id: typing.Union[int, str],
29+
model_name: typing.Optional[str] = None,
2930
lock_expire: int = settings.REDIS_DEFAULT_LOCK_EXPIRE,
3031
):
3132
"""
3233
Locking mechanism using Redis
3334
"""
34-
lock_id = key.format(id)
35+
if model_name and id:
36+
lock_id = key.format(model_name=model_name, id=id)
37+
else:
38+
lock_id = key.format(id)
3539
timeout_at = time.monotonic() + lock_expire - 3
3640
# cache.add fails if the key already exists
3741
status = cache.add(lock_id, 1, lock_expire)

0 commit comments

Comments
 (0)