1- import numpy as np
2- import pandas as pd
31import requests
42from django .conf import settings
53from django .core .management .base import BaseCommand
108from api .models import Country , CronJob , CronJobStatus , NSDInitiatives
119from 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
1446class 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 )
0 commit comments