Skip to content

Commit fae7093

Browse files
Merge pull request #2549 from IFRCGo/feature/remote-id-to-nsia-esf-cbf
Remote id to NSIA, ESF, CBF
2 parents 5d76f63 + d64bf3b commit fae7093

File tree

4 files changed

+179
-62
lines changed

4 files changed

+179
-62
lines changed
Lines changed: 111 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import numpy as np
2-
import pandas as pd
31
import requests
42
from django.conf import settings
53
from django.core.management.base import BaseCommand
@@ -10,6 +8,40 @@
108
from api.models import Country, CronJob, CronJobStatus, NSDInitiatives
119
from main.sentry import SentryMonitor
1210

11+
DEFAULT_COUNTRY_ID = 289 # IFRC
12+
13+
14+
def get_country(element):
15+
country = Country.objects.filter(iso__iexact=element["ISO"]).first()
16+
if not country: # Fallback to IFRC, but this does not happen in practice
17+
country = Country.objects.get(pk=DEFAULT_COUNTRY_ID)
18+
return country
19+
20+
21+
def get_funding_period(element):
22+
funding_period = element.get("FundingPeriodInMonths")
23+
if funding_period is None and element.get("FundingPeriodInYears") is not None:
24+
funding_period = element["FundingPeriodInYears"] * 12
25+
return funding_period
26+
27+
28+
def get_defaults(element, country, funding_period, lang):
29+
defaults = {
30+
"country": country,
31+
"year": element.get("Year"),
32+
"fund_type": (
33+
f"{element.get('Fund')}{element.get('FundingType')}" if element.get("FundingType") else element.get("Fund")
34+
),
35+
"categories": element.get("Categories"),
36+
"allocation": element.get("AllocationInCHF"),
37+
"funding_period": funding_period,
38+
"translation_module_original_language": lang,
39+
"translation_module_skip_auto_translation": True,
40+
}
41+
title_field = f"title_{lang}"
42+
defaults[title_field] = element.get("InitiativeTitle")
43+
return defaults, title_field
44+
1345

1446
class Command(BaseCommand):
1547
help = "Add ns initiatives"
@@ -25,72 +57,92 @@ def handle(self, *args, **kwargs):
2557
logger.info("No proper api-keys are provided. Quitting.")
2658
return
2759

28-
if production:
29-
urls = [
30-
# languageCode can be en, es, fr, ar. If omitted, defaults to en.
31-
f"https://data.ifrc.org/NSIA_API/api/approvedApplications?languageCode=en&apiKey={api_keys[0]}",
32-
f"https://data.ifrc.org/ESF_API/api/approvedApplications?languageCode=en&apiKey={api_keys[1]}",
33-
f"https://data.ifrc.org/CBF_API/api/approvedApplications?languageCode=en&apiKey={api_keys[2]}",
34-
]
35-
else:
36-
urls = [
37-
f"https://data-staging.ifrc.org/NSIA_API/api/approvedApplications?languageCode=en&apiKey={api_keys[0]}",
38-
f"https://data-staging.ifrc.org/ESF_API/api/approvedApplications?languageCode=en&apiKey={api_keys[1]}",
39-
f"https://data-staging.ifrc.org/CBF_API/api/approvedApplications?languageCode=en&apiKey={api_keys[2]}",
40-
]
60+
LANGUAGES = ["en", "es", "fr", "ar"]
61+
urls = []
62+
63+
# Build URLs for all languages and all subsystems
64+
for lang in LANGUAGES:
65+
if production:
66+
urls += [
67+
f"https://data.ifrc.org/NSIA_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[0]}",
68+
f"https://data.ifrc.org/ESF_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[1]}",
69+
f"https://data.ifrc.org/CBF_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[2]}",
70+
]
71+
else:
72+
urls += [
73+
f"https://data-staging.ifrc.org/NSIA_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[0]}",
74+
f"https://data-staging.ifrc.org/ESF_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[1]}",
75+
f"https://data-staging.ifrc.org/CBF_API/api/approvedApplications?languageCode={lang}&apiKey={api_keys[2]}",
76+
]
4177

4278
responses = []
79+
# Fetch all responses and pair them with their language
4380
for url in urls:
81+
lang = url.split("languageCode=")[1].split("&")[0]
4482
response = requests.get(url)
4583
if response.status_code == 200:
46-
responses.append(response.json())
84+
responses.append((lang, response.json()))
4785

4886
added = 0
49-
50-
flatList = [element for innerList in responses for element in innerList]
51-
funding_data = pd.DataFrame(
52-
flatList,
53-
columns=[
54-
"NationalSociety",
55-
"Year",
56-
"Fund",
57-
"InitiativeTitle",
58-
"Categories",
59-
"AllocationInCHF",
60-
"FundingPeriodInMonths",
61-
"FundingType",
62-
"FundingPeriodInYears",
63-
],
64-
)
65-
funding_data = funding_data.replace({np.nan: None})
87+
updated_remote_ids = set()
6688
created_ns_initiatives_pk = []
67-
for data in funding_data.values.tolist():
68-
# TODO: Filter not by society name
69-
country = Country.objects.filter(society_name__iexact=data[0]).first()
70-
if country:
71-
nsd_initiatives, created = NSDInitiatives.objects.get_or_create(
72-
country=country,
73-
year=data[1],
74-
fund_type=f"{data[2]}{data[7]}" if data[7] else data[2],
75-
defaults={
76-
"title": data[3],
77-
"categories": data[4],
78-
"allocation": data[5],
79-
"funding_period": data[6] if data[6] else data[8] * 12,
80-
},
81-
)
82-
if not created:
83-
nsd_initiatives.title = data[3]
84-
nsd_initiatives.categories = data[4]
85-
nsd_initiatives.allocation = data[5]
86-
nsd_initiatives.funding_period = data[6]
87-
nsd_initiatives.save(update_fields=["title", "categories", "allocation", "funding_period"])
88-
created_ns_initiatives_pk.append(nsd_initiatives.pk)
89-
added += 1
90-
# NOTE: Delete the NSDInitiatives that are not in the source
89+
90+
for lang, resp in responses:
91+
for element in resp:
92+
try:
93+
remote_id = int(element["Id"]) if element.get("Id") is not None else None
94+
except (ValueError, TypeError):
95+
logger.warning(f"Invalid Id value for element: {element.get('Id')!r}. Skipping element.")
96+
continue
97+
if not remote_id:
98+
continue
99+
100+
country = get_country(element)
101+
funding_period = get_funding_period(element)
102+
defaults, title_field = get_defaults(element, country, funding_period, lang)
103+
104+
if lang == "en":
105+
ni, created = NSDInitiatives.objects.get_or_create(
106+
remote_id=remote_id,
107+
defaults=defaults,
108+
)
109+
if created:
110+
added += 1
111+
else:
112+
for field, value in defaults.items():
113+
setattr(ni, field, value)
114+
ni.save(update_fields=defaults.keys())
115+
updated_remote_ids.add(remote_id) # Mark as updated, only for EN entries
116+
created_ns_initiatives_pk.append(ni.pk)
117+
else:
118+
try:
119+
# We could use ISO also to identify the entry, but remote_id is more robust
120+
ni = NSDInitiatives.objects.get(remote_id=remote_id)
121+
setattr(ni, title_field, element.get("InitiativeTitle"))
122+
ni.save(update_fields=[title_field])
123+
except NSDInitiatives.DoesNotExist:
124+
# Should not happen – only if EN entry is missing
125+
ni = NSDInitiatives.objects.create(
126+
remote_id=remote_id,
127+
**defaults,
128+
)
129+
added += 1
130+
created_ns_initiatives_pk.append(ni.pk)
131+
logger.warning(f"Created non-EN entry: {remote_id} / {lang}")
132+
133+
# Remove old entries not present in the latest fetch
91134
NSDInitiatives.objects.exclude(id__in=created_ns_initiatives_pk).delete()
92135

93-
text_to_log = "%s Ns initiatives added" % added
136+
updated = len(updated_remote_ids)
137+
if added:
138+
text_to_log = f"{added} NS initiatives added, {updated} updated"
139+
else:
140+
text_to_log = f"{updated} NS initiatives updated, no new initiatives added"
94141
logger.info(text_to_log)
95-
body = {"name": "ingest_ns_initiatives", "message": text_to_log, "num_result": added, "status": CronJobStatus.SUCCESSFUL}
142+
body = {
143+
"name": "ingest_ns_initiatives",
144+
"message": text_to_log,
145+
"num_result": added + updated,
146+
"status": CronJobStatus.SUCCESSFUL,
147+
}
96148
CronJob.sync_cron(body)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 4.2.19 on 2025-09-10 09:25
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("api", "0220_event_ifrc_severity_level_update_date_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterUniqueTogether(
14+
name="nsdinitiatives",
15+
unique_together=set(),
16+
),
17+
migrations.AddField(
18+
model_name="nsdinitiatives",
19+
name="remote_id",
20+
field=models.IntegerField(blank=True, db_index=True, null=True, unique=True),
21+
),
22+
migrations.AddField(
23+
model_name="nsdinitiatives",
24+
name="title_ar",
25+
field=models.CharField(max_length=255, null=True, verbose_name="Title"),
26+
),
27+
migrations.AddField(
28+
model_name="nsdinitiatives",
29+
name="title_en",
30+
field=models.CharField(max_length=255, null=True, verbose_name="Title"),
31+
),
32+
migrations.AddField(
33+
model_name="nsdinitiatives",
34+
name="title_es",
35+
field=models.CharField(max_length=255, null=True, verbose_name="Title"),
36+
),
37+
migrations.AddField(
38+
model_name="nsdinitiatives",
39+
name="title_fr",
40+
field=models.CharField(max_length=255, null=True, verbose_name="Title"),
41+
),
42+
migrations.AddField(
43+
model_name="nsdinitiatives",
44+
name="translation_module_original_language",
45+
field=models.CharField(
46+
choices=[("en", "English"), ("es", "Spanish"), ("fr", "French"), ("ar", "Arabic")],
47+
default="en",
48+
help_text="Language used to create this entity",
49+
max_length=2,
50+
verbose_name="Entity Original language",
51+
),
52+
),
53+
migrations.AddField(
54+
model_name="nsdinitiatives",
55+
name="translation_module_skip_auto_translation",
56+
field=models.BooleanField(
57+
default=False, help_text="Skip auto translation operation for this entity?", verbose_name="Skip auto translation"
58+
),
59+
),
60+
]

api/models.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,7 @@ class NSDInitiatives(models.Model):
427427
year = models.CharField(verbose_name=_("Year"), max_length=20)
428428
funding_period = models.IntegerField(verbose_name=_("Funding Period in Month"))
429429
categories = ArrayField(models.CharField(max_length=255), verbose_name=_("Funding categories"), default=list, null=True)
430-
431-
class Meta:
432-
unique_together = ("country", "year", "fund_type")
430+
remote_id = models.IntegerField(db_index=True, unique=True, null=True, blank=True)
433431

434432
def __str__(self):
435433
return f"{self.country.name} - {self.title}"

api/translation.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
GeneralDocument,
1919
KeyFigure,
2020
MainContact,
21+
NSDInitiatives,
2122
Region,
2223
RegionEmergencySnippet,
2324
RegionKeyFigure,
@@ -102,6 +103,12 @@ class MainContactTO(TranslationOptions):
102103
fields = ("extent",)
103104

104105

106+
@register(NSDInitiatives)
107+
class NSDInitiativesTO(TranslationOptions):
108+
fields = ("title",)
109+
skip_fields = ("title",)
110+
111+
105112
@register(Region)
106113
class RegionTO(TranslationOptions):
107114
fields = (

0 commit comments

Comments
 (0)