Skip to content

Commit 0ab922c

Browse files
committed
NS initiatives categories translated
1 parent 46021ac commit 0ab922c

File tree

5 files changed

+181
-72
lines changed

5 files changed

+181
-72
lines changed

api/management/commands/ingest_ns_initiatives.py

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ def handle(self, *args, **kwargs):
9494
updated_remote_ids = set()
9595
created_ns_initiatives_pk = []
9696

97+
# In-memory alignment helpers
98+
category_by_remote_index = {} # remote_id -> list[NSDInitiativesCategory]
99+
pending_translations = {} # remote_id -> list[(lang, index, label)]
100+
97101
for lang, resp in responses:
98102
for element in resp:
99103
try:
@@ -119,44 +123,99 @@ def handle(self, *args, **kwargs):
119123
for field, value in defaults.items():
120124
setattr(ni, field, value)
121125
ni.save(update_fields=defaults.keys())
122-
updated_remote_ids.add(remote_id) # Mark as updated, only for EN entries
126+
updated_remote_ids.add(remote_id)
123127
created_ns_initiatives_pk.append(ni.pk)
124-
else:
125-
try:
126-
# We could use ISO also to identify the entry, but remote_id is more robust
127-
ni = NSDInitiatives.objects.get(remote_id=remote_id)
128-
setattr(ni, title_field, element.get("InitiativeTitle"))
129-
setattr(ni, risk_field, element.get("Risk"))
130-
ni.save(update_fields=[title_field, risk_field])
131-
except NSDInitiatives.DoesNotExist:
132-
# Should not happen – only if EN entry is missing
133-
ni = NSDInitiatives.objects.create(
134-
remote_id=remote_id,
135-
**defaults,
136-
)
137-
added += 1
138-
created_ns_initiatives_pk.append(ni.pk)
139-
logger.warning(f"Created non-EN entry: {remote_id} / {lang}")
140128

141-
# Handle categories (language-aware)
142-
raw_categories = element.get("Categories") or []
143-
if isinstance(raw_categories, (list, tuple)):
129+
# Establish baseline categories from EN by index
130+
raw_categories = element.get("Categories") or []
131+
cats_en = []
144132
cat_objs = []
145-
for c in raw_categories:
146-
if not c:
147-
continue
148-
name = str(c).strip()
149-
if not name:
150-
continue
151-
# Create / fetch category for this specific language
152-
obj, _ = NSDInitiativesCategory.objects.get_or_create(
153-
name=name,
154-
lang=lang,
155-
)
156-
cat_objs.append(obj)
157-
158-
if cat_objs:
159-
ni.categories.add(*cat_objs)
133+
if isinstance(raw_categories, (list, tuple)):
134+
for idx, raw in enumerate(raw_categories):
135+
label = (raw or "").strip()
136+
if not label:
137+
cats_en.append(None)
138+
continue
139+
# Reuse/create a global category by English label
140+
cat = NSDInitiativesCategory.objects.filter(name_en__iexact=label).first()
141+
if not cat:
142+
cat = NSDInitiativesCategory.objects.create(name=label, name_en=label)
143+
else:
144+
# Ensure plain 'name' mirrors EN for convenience
145+
to_update = []
146+
if getattr(cat, "name_en", None) != label:
147+
cat.name_en = label
148+
to_update.append("name_en")
149+
if getattr(cat, "name", None) != label:
150+
cat.name = label
151+
to_update.append("name")
152+
if to_update:
153+
cat.save(update_fields=to_update)
154+
cats_en.append(cat)
155+
cat_objs.append(cat)
156+
157+
if cat_objs:
158+
ni.categories.set(cat_objs)
159+
else:
160+
ni.categories.clear()
161+
162+
# Save baseline for this remote_id
163+
category_by_remote_index[remote_id] = cats_en
164+
165+
# Apply any pending translations queued before EN
166+
for item in pending_translations.pop(remote_id, []):
167+
plang, pidx, plabel = item
168+
if 0 <= pidx < len(cats_en) and cats_en[pidx]:
169+
field = f"name_{plang}"
170+
cat = cats_en[pidx]
171+
if getattr(cat, field, None) != plabel:
172+
setattr(cat, field, plabel)
173+
cat.save(update_fields=[field])
174+
continue # Done with EN row; go next element
175+
176+
# Non-EN branch: update fields and queue/apply category translations
177+
try:
178+
ni = NSDInitiatives.objects.get(remote_id=remote_id)
179+
setattr(ni, title_field, element.get("InitiativeTitle"))
180+
setattr(ni, risk_field, element.get("Risk"))
181+
ni.save(update_fields=[title_field, risk_field])
182+
except NSDInitiatives.DoesNotExist:
183+
# Should not happen – only if EN entry is missing
184+
ni = NSDInitiatives.objects.create(
185+
remote_id=remote_id,
186+
**defaults,
187+
)
188+
added += 1
189+
created_ns_initiatives_pk.append(ni.pk)
190+
logger.warning(f"Created non-EN entry: {remote_id} / {lang}")
191+
192+
# Align categories by index to the EN baseline for this remote_id
193+
raw_categories = element.get("Categories") or []
194+
if not isinstance(raw_categories, (list, tuple)):
195+
continue
196+
197+
cats_en = category_by_remote_index.get(remote_id)
198+
if cats_en is None:
199+
# Baseline not processed yet; queue these translations
200+
q = pending_translations.setdefault(remote_id, [])
201+
for idx, raw in enumerate(raw_categories):
202+
label = (raw or "").strip()
203+
if label:
204+
q.append((lang, idx, label))
205+
continue
206+
207+
# Baseline exists: update translated fields by index
208+
field = f"name_{lang}"
209+
for idx, raw in enumerate(raw_categories):
210+
label = (raw or "").strip()
211+
if not label or idx >= len(cats_en):
212+
continue
213+
cat = cats_en[idx]
214+
if not cat:
215+
continue
216+
if getattr(cat, field, None) != label:
217+
setattr(cat, field, label)
218+
cat.save(update_fields=[field])
160219

161220
# Remove old entries not present in the latest fetch
162221
NSDInitiatives.objects.exclude(id__in=created_ns_initiatives_pk).delete()

api/migrations/0226_nsdinitiativescategory_and_more.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,62 @@
44

55

66
class Migration(migrations.Migration):
7-
87
dependencies = [
9-
('api', '0225_nsdinitiatives_nsia_risk_ar_and_more'),
8+
("api", "0225_nsdinitiatives_nsia_risk_ar_and_more"),
109
]
1110

1211
operations = [
1312
migrations.CreateModel(
14-
name='NSDInitiativesCategory',
13+
name="NSDInitiativesCategory",
1514
fields=[
16-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17-
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
15+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
16+
("name", models.CharField(max_length=255, verbose_name="Name")),
17+
("name_en", models.CharField(max_length=255, null=True)),
18+
("name_es", models.CharField(max_length=255, null=True)),
19+
("name_fr", models.CharField(max_length=255, null=True)),
20+
("name_ar", models.CharField(max_length=255, null=True)),
21+
(
22+
"translation_module_original_language",
23+
models.CharField(
24+
choices=[("en", "English"), ("es", "Spanish"), ("fr", "French"), ("ar", "Arabic")],
25+
default="en",
26+
help_text="Language used to create this entity",
27+
max_length=2,
28+
verbose_name="Entity Original language",
29+
),
30+
),
31+
(
32+
"translation_module_skip_auto_translation",
33+
models.BooleanField(
34+
default=False,
35+
help_text="Skip auto translation operation for this entity?",
36+
verbose_name="Skip auto translation",
37+
),
38+
),
1839
],
1940
options={
20-
'verbose_name': 'NSD initiative category',
21-
'verbose_name_plural': 'NSD initiative categories',
22-
'ordering': ('name',),
41+
"verbose_name": "NSD initiative category",
42+
"verbose_name_plural": "NSD initiative categories",
43+
"ordering": ("name",),
2344
},
2445
),
2546
migrations.RemoveField(
26-
model_name='nsdinitiatives',
27-
name='categories',
47+
model_name="nsdinitiatives",
48+
name="categories", # remove old ArrayField
2849
),
2950
migrations.AddField(
30-
model_name='nsdinitiatives',
31-
name='categories',
32-
field=models.ManyToManyField(blank=True, related_name='initiatives', to='api.nsdinitiativescategory', verbose_name='Funding categories'),
33-
),
34-
migrations.AddField(
35-
model_name='nsdinitiativescategory',
36-
name='lang',
37-
field=models.CharField(choices=[('en', 'English'), ('es', 'Spanish'), ('fr', 'French'), ('ar', 'Arabic')], db_index=True, default='en', max_length=5),
51+
model_name="nsdinitiatives",
52+
name="categories",
53+
field=models.ManyToManyField(
54+
blank=True,
55+
related_name="initiatives",
56+
to="api.nsdinitiativescategory",
57+
verbose_name="Funding categories",
58+
),
3859
),
3960
migrations.AlterField(
4061
model_name='nsdinitiativescategory',
4162
name='name',
4263
field=models.CharField(max_length=255),
4364
),
44-
migrations.AlterUniqueTogether(
45-
name='nsdinitiativescategory',
46-
unique_together={('name', 'lang')},
47-
),
4865
]

api/models.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -419,26 +419,16 @@ class CountryOrganizationalCapacity(models.Model):
419419
financial_capacity = models.TextField(verbose_name=_("Financial Capacity"), null=True, blank=True)
420420

421421

422-
LANG_CHOICES = (
423-
("en", "English"),
424-
("es", "Spanish"),
425-
("fr", "French"),
426-
("ar", "Arabic"),
427-
)
428-
429-
430422
class NSDInitiativesCategory(models.Model):
431423
name = models.CharField(max_length=255)
432-
lang = models.CharField(max_length=5, choices=LANG_CHOICES, db_index=True, default="en")
433424

434425
class Meta:
435426
ordering = ("name",)
436-
unique_together = ("name", "lang")
437427
verbose_name = _("NSD initiative category")
438428
verbose_name_plural = _("NSD initiative categories")
439429

440430
def __str__(self):
441-
return f"{self.name} ({self.lang})"
431+
return f"{self.name}"
442432

443433

444434
class NSDInitiatives(models.Model):

api/serializers.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ class Meta:
658658

659659

660660
class NSDInitiativesSerializer(ModelSerializer):
661+
# Return translated category names (current active language) – like title does.
661662
categories = serializers.SerializerMethodField()
662663

663664
class Meta:
@@ -666,10 +667,45 @@ class Meta:
666667

667668
@staticmethod
668669
def get_categories(obj):
669-
out = {}
670-
for lang, name in obj.categories.values_list("lang", "name"):
671-
out.setdefault(lang, []).append(name)
672-
return out
670+
"""
671+
Return category names in the currently active language (like title does),
672+
avoiding duplicates and avoiding showing the same semantic category in
673+
multiple languages (caused by legacy per-language rows).
674+
675+
Strategy:
676+
- Active language value (name_<lang>) wins.
677+
- If missing, fall back to English ONLY if no row already provided a
678+
translated value for that semantic slot.
679+
- Ignore rows that have neither an active-language value nor an English fallback.
680+
"""
681+
from django.utils.translation import get_language
682+
683+
lang = (get_language() or "en")[:2]
684+
lang_field = f"name_{lang}"
685+
686+
cats = obj.categories.all()
687+
# First collect all explicit translations in the active language
688+
explicit_lang_values = {
689+
getattr(c, lang_field).strip() for c in cats if getattr(c, lang_field, None) and getattr(c, lang_field).strip()
690+
}
691+
692+
seen = set()
693+
result = []
694+
for c in cats:
695+
val_lang = getattr(c, lang_field, None)
696+
val_lang = val_lang.strip() if val_lang else ""
697+
if val_lang:
698+
if val_lang not in seen:
699+
seen.add(val_lang)
700+
result.append(val_lang)
701+
continue
702+
# fallback to English only if there is NO active-language version in any row
703+
val_en = getattr(c, "name_en", None)
704+
val_en = val_en.strip() if val_en else ""
705+
if val_en and not explicit_lang_values and val_en not in seen:
706+
seen.add(val_en)
707+
result.append(val_en)
708+
return result
673709

674710

675711
class CountryCapacityStrengtheningSerializer(ModelSerializer):

api/translation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
KeyFigure,
2020
MainContact,
2121
NSDInitiatives,
22+
NSDInitiativesCategory,
2223
Region,
2324
RegionEmergencySnippet,
2425
RegionKeyFigure,
@@ -103,6 +104,12 @@ class MainContactTO(TranslationOptions):
103104
fields = ("extent",)
104105

105106

107+
@register(NSDInitiativesCategory)
108+
class NSDInitiativesCategoryTO(TranslationOptions):
109+
fields = ("name",)
110+
skip_fields = ("name",)
111+
112+
106113
@register(NSDInitiatives)
107114
class NSDInitiativesTO(TranslationOptions):
108115
fields = ("title", "nsia_risk")

0 commit comments

Comments
 (0)