Skip to content

Commit 9d6de39

Browse files
Merge pull request #1651 from IFRCGo/feature/auto-internal-plan-load
Add internal plan to ingest_country_plan_file
2 parents d2f7e25 + f706682 commit 9d6de39

File tree

4 files changed

+124
-78
lines changed

4 files changed

+124
-78
lines changed

country_plan/management/commands/ingest_country_plan_file.py

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
from datetime import datetime
55
from django.utils.timezone import make_aware
66
from django.core.management.base import BaseCommand
7+
from django.core.files.base import File
78
from django.conf import settings
89
from django.db import models
910

10-
from api.models import CronJob, CronJobStatus
11+
from main.utils import DownloadFileManager
12+
from api.models import Country
1113
from country_plan.models import CountryPlan
1214

1315
logger = logging.getLogger(__name__)
1416

15-
NAME = 'ingest_country_plan_file'
1617
# Ref: https://github.com/IFRCGo/go-api/issues/1614
17-
SOURCE = 'https://go-api.ifrc.org/api/publicsiteappeals?AppealsTypeID=1851&Hidden=false'
18+
PUBLIC_SOURCE = 'https://go-api.ifrc.org/api/publicsiteappeals?AppealsTypeID=1851&Hidden=false'
19+
# Ref: https://github.com/IFRCGo/go-api/issues/1648
20+
INTERNAL_SOURCE = 'https://go-api.ifrc.org/Api/FedNetAppeals?AppealsTypeId=1844&Hidden=false'
1821

1922

2023
class Command(BaseCommand):
@@ -33,7 +36,15 @@ def parse_date(text: str) -> Union[datetime, None]:
3336
datetime.strptime(text, '%Y-%m-%dT%H:%M:%S')
3437
)
3538

36-
def load_for_country(self, country_data):
39+
@staticmethod
40+
def load_file_to_country_plan(country_plan: CountryPlan, url: str, filename: str, field_name: str):
41+
with DownloadFileManager(url, suffix='.pdf') as f:
42+
getattr(country_plan, field_name).save(
43+
filename,
44+
File(f),
45+
)
46+
47+
def load_for_country(self, country_data, file_field, field_inserted_date_field):
3748
country_iso2 = country_data.get('LocationCountryCode')
3849
country_name = country_data.get('LocationCountryName')
3950
public_plan_url = country_data.get('BaseDirectory') or '' + country_data.get('BaseFileName') or ''
@@ -44,69 +55,56 @@ def load_for_country(self, country_data):
4455
inserted_date is None
4556
):
4657
return
47-
country_plan = CountryPlan.objects.filter(
48-
models.Q(country__iso__iexact=country_iso2) |
49-
models.Q(country__name__iexact=country_name)
50-
).first()
58+
country_qs = Country.objects.filter(
59+
models.Q(iso__iexact=country_iso2) |
60+
models.Q(name__iexact=country_name)
61+
)
62+
country_plan = CountryPlan.objects.filter(country__in=country_qs).first()
5163
if country_plan is None:
52-
logger.warning(f'{NAME} No country_plan found for: {(country_iso2, country_name)}')
53-
return
54-
if country_plan.appeal_api_inserted_date and country_plan.appeal_api_inserted_date >= inserted_date:
64+
country = country_qs.first()
65+
# If there is no country as well, show warning and return
66+
if not country:
67+
logger.warning(f'{file_field} No country found for: {(country_iso2, country_name)}')
68+
return
69+
# Else create one and continue
70+
country_plan = CountryPlan(country=country)
71+
existing_inserted_date = getattr(country_plan, field_inserted_date_field, None)
72+
if existing_inserted_date and existing_inserted_date >= inserted_date:
5573
# No need to do anything here
5674
return
75+
self.stdout.write(f'- Saving data for country:: {country_plan.country.name}')
5776
public_plan_url = country_data['BaseDirectory'] + country_data['BaseFileName']
58-
country_plan.appeal_api_inserted_date = inserted_date
59-
country_plan.load_file_to_country_plan(
77+
setattr(country_plan, field_inserted_date_field, inserted_date)
78+
self.load_file_to_country_plan(
79+
country_plan,
6080
public_plan_url,
6181
# NOTE: File provided are PDF,
62-
f"public-plan-{country_data['BaseFileName']}.pdf",
82+
f"{file_field.replace('_', '-').replace('file', '')}-{country_data['BaseFileName']}.pdf",
83+
file_field,
6384
)
6485
country_plan.is_publish = True
6586
country_plan.save(
6687
update_fields=(
67-
'appeal_api_inserted_date',
68-
'public_plan_file', # By load_file_to_country_plan
88+
field_inserted_date_field,
89+
file_field, # By load_file_to_country_plan
6990
'is_publish',
7091
)
7192
)
7293
return True
7394

74-
def load(self):
75-
updated = 0
95+
def load(self, url: str, file_field: str, field_inserted_date_field: str):
7696
auth = (settings.APPEALS_USER, settings.APPEALS_PASS)
77-
results = requests.get(SOURCE, auth=auth, headers={'Accept': 'application/json'}).json()
97+
results = requests.get(url, auth=auth, headers={'Accept': 'application/json'}).json()
7898
for result in results:
7999
try:
80-
if self.load_for_country(result):
81-
updated += 1
82-
except Exception as ex:
100+
self.load_for_country(result, file_field, field_inserted_date_field)
101+
except Exception:
83102
logger.error('Could not Updated countries plan', exc_info=True)
84-
country_info = (
85-
result.get('LocationCountryCode'),
86-
result.get('LocationCountryName'),
87-
)
88-
CronJob.sync_cron({
89-
'name': NAME,
90-
'message': f"Could not updated country plan for {country_info}\n\nException:\n{str(ex)}",
91-
'status': CronJobStatus.ERRONEOUS,
92-
})
93-
return updated
94103

95-
def handle(self, *args, **kwargs):
96-
try:
97-
logger.info('\nFetching data for country plans:: ')
98-
countries_plan_updated = self.load()
99-
CronJob.sync_cron({
100-
'name': NAME,
101-
'message': 'Updated countries plan',
102-
'num_result': countries_plan_updated,
103-
'status': CronJobStatus.SUCCESSFUL,
104-
})
105-
logger.info('Updated countries plan')
106-
except Exception as ex:
107-
logger.error('Could not Updated countries plan', exc_info=True)
108-
CronJob.sync_cron({
109-
'name': NAME,
110-
'message': f'Could not Updated countries plan\n\nException:\n{str(ex)}',
111-
'status': CronJobStatus.ERRONEOUS,
112-
})
104+
def handle(self, **_):
105+
# Public
106+
self.stdout.write('Fetching data for country plans:: PUBLIC')
107+
self.load(PUBLIC_SOURCE, 'public_plan_file', 'public_plan_inserted_date')
108+
# Private
109+
self.stdout.write('\nFetching data for country plans:: PRIVATE')
110+
self.load(INTERNAL_SOURCE, 'internal_plan_file', 'internal_plan_inserted_date')
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 3.2.16 on 2023-02-07 08:40
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('country_plan', '0004_countryplan_appeal_api_inserted_date'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='countryplan',
15+
old_name='appeal_api_inserted_date',
16+
new_name='public_plan_inserted_date',
17+
),
18+
migrations.AddField(
19+
model_name='countryplan',
20+
name='internal_plan_inserted_date',
21+
field=models.DateTimeField(blank=True, null=True),
22+
),
23+
]

country_plan/models.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
from django.utils import timezone
44
from django.utils.translation import gettext_lazy as _
55
from django.core.validators import FileExtensionValidator
6-
from django.core.files.base import File
76

8-
from main.utils import DownloadFileManager
97
from api.models import Country
108

119

@@ -83,21 +81,12 @@ class CountryPlan(CountryPlanAbstract):
8381
is_publish = models.BooleanField(default=False, verbose_name=_('Published'))
8482

8583
# NOTE: Used to sync with Appeal API (ingest_country_plan_file.py for more info)
86-
appeal_api_inserted_date = models.DateTimeField(blank=True, null=True)
84+
public_plan_inserted_date = models.DateTimeField(blank=True, null=True)
85+
internal_plan_inserted_date = models.DateTimeField(blank=True, null=True)
8786

8887
def __str__(self):
8988
return f'{self.country}'
9089

91-
def load_file_to_country_plan(self, url: str, filename: str, commit=False):
92-
with DownloadFileManager(url, suffix='.pdf') as f:
93-
self.public_plan_file.save(
94-
filename,
95-
File(f),
96-
)
97-
if commit:
98-
self.save()
99-
100-
# NOTE: To be used by CountryPlanSerializer (From CountryPlanViewset)
10190
def full_country_plan_mc(self):
10291
all_mc = list(self.country_plan_mc.all())
10392
mc_by_ns_sector = {

country_plan/tests/test_commands.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from api.factories.country import CountryFactory
66
from country_plan.factories import CountryPlanFactory
77
from country_plan.models import CountryPlan
8-
from country_plan.management.commands.ingest_country_plan_file import SOURCE
8+
from country_plan.management.commands.ingest_country_plan_file import PUBLIC_SOURCE, INTERNAL_SOURCE
99

1010
# NOTE: Only defined used fields
1111
FILE_BASE_DIRECTORY = 'https://example.org/Download.aspx?FileId='
12-
APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [
12+
PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [
1313
{
1414
'BaseDirectory': FILE_BASE_DIRECTORY,
1515
'BaseFileName': '000000',
@@ -24,6 +24,7 @@
2424
'LocationCountryCode': 'CD',
2525
'LocationCountryName': 'Congo, The Democratic Republic Of The'
2626
},
27+
# Not included in INTERNAL
2728
{
2829
'BaseDirectory': FILE_BASE_DIRECTORY,
2930
'BaseFileName': '000002',
@@ -47,6 +48,31 @@
4748
}
4849
]
4950

51+
INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [
52+
{
53+
'BaseDirectory': FILE_BASE_DIRECTORY,
54+
'BaseFileName': '000000',
55+
'Inserted': '2022-11-29T11:24:00',
56+
'LocationCountryCode': 'SY',
57+
'LocationCountryName': 'Syrian Arab Republic'
58+
},
59+
{
60+
'BaseDirectory': FILE_BASE_DIRECTORY,
61+
'BaseFileName': '000001',
62+
'Inserted': '2022-11-29T11:24:00',
63+
'LocationCountryCode': 'CD',
64+
'LocationCountryName': 'Congo, The Democratic Republic Of The'
65+
},
66+
# Not included in PUBLIC
67+
{
68+
'BaseDirectory': FILE_BASE_DIRECTORY,
69+
'BaseFileName': '000001',
70+
'Inserted': '2022-11-29T11:24:00',
71+
'LocationCountryCode': 'NP',
72+
'LocationCountryName': 'Nepal'
73+
},
74+
]
75+
5076

5177
class MockResponse():
5278
class FileStream():
@@ -75,8 +101,10 @@ def __exit__(self, *_):
75101

76102
class CountryPlanIngestTest(APITestCase):
77103
def mock_request(url, *_, **kwargs):
78-
if url == SOURCE:
79-
return MockResponse(json=APPEAL_COUNTRY_PLAN_MOCK_RESPONSE)
104+
if url == PUBLIC_SOURCE:
105+
return MockResponse(json=PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE)
106+
if url == INTERNAL_SOURCE:
107+
return MockResponse(json=INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE)
80108
if url.startswith(FILE_BASE_DIRECTORY):
81109
return MockResponse(stream=[b''])
82110

@@ -89,17 +117,25 @@ def test_country_plan_ingest(self, *_):
89117
iso='RC',
90118
),
91119
)
92-
for country_plan_data in APPEAL_COUNTRY_PLAN_MOCK_RESPONSE[:3]:
93-
CountryPlanFactory.create(
94-
country=CountryFactory.create(
95-
name=country_plan_data['LocationCountryName'],
96-
iso=country_plan_data['LocationCountryCode'],
97-
),
98-
)
99-
assert CountryPlan.objects.count() == 4
120+
EXISTING_CP = 1
121+
for country_iso in set([
122+
item['LocationCountryCode']
123+
for item in [
124+
*PUBLIC_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE,
125+
*INTERNAL_APPEAL_COUNTRY_PLAN_MOCK_RESPONSE,
126+
]
127+
]):
128+
if country_iso == 'CD':
129+
# Not create country for this
130+
continue
131+
CountryFactory.create(iso=country_iso)
132+
# Before
133+
assert CountryPlan.objects.count() == EXISTING_CP
100134
assert CountryPlan.objects.filter(is_publish=True).count() == 0
101135
assert CountryPlan.objects.exclude(public_plan_file='').count() == 0
102136
call_command('ingest_country_plan_file')
103-
assert CountryPlan.objects.count() == 4
104-
assert CountryPlan.objects.filter(is_publish=True).count() == 3
105-
assert CountryPlan.objects.exclude(public_plan_file='').count() == 3
137+
# After
138+
assert CountryPlan.objects.count() == EXISTING_CP + 5
139+
assert CountryPlan.objects.filter(is_publish=True).count() == 5
140+
assert CountryPlan.objects.exclude(public_plan_file='').count() == 4
141+
assert CountryPlan.objects.exclude(internal_plan_file='').count() == 2

0 commit comments

Comments
 (0)