Skip to content

Commit 9eb017e

Browse files
authored
Merge pull request #1624 from IFRCGo/feature/country-plan-auto-fetch
Feature/country plan auto fetch
2 parents 40af6ae + 73911d3 commit 9eb017e

File tree

14 files changed

+306
-1
lines changed

14 files changed

+306
-1
lines changed

api/management/commands/ingest_appeals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def get_new_or_modified_appeals(self):
8787
# get latest BILATERALS
8888
logger.info('Querying appeals API for new appeals data (bilateral)')
8989
url = 'http://go-api.ifrc.org/api/appealbilaterals'
90-
auth = (os.getenv('APPEALS_USER'), os.getenv('APPEALS_PASS'))
90+
auth = (settings.APPEALS_USER, settings.APPEALS_PASS)
9191

9292
adapter = HTTPAdapter(max_retries=settings.RETRY_STRATEGY)
9393
sess = Session()

api/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,6 +1925,7 @@ def __str__(self):
19251925
return '%s | %s | %s' % (self.name, self.get_status_display(), str(self.created_at)[5:16]) # omit irrelevant 0
19261926

19271927
# Given a request containing new CronJob log row, validate and add the CronJob log row.
1928+
@staticmethod
19281929
def sync_cron(body):
19291930
new = []
19301931
errors = []

country_plan/factories.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import factory
2+
3+
from .models import CountryPlan
4+
5+
6+
class CountryPlanFactory(factory.django.DjangoModelFactory):
7+
class Meta:
8+
model = CountryPlan
File renamed without changes.

country_plan/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import logging
2+
import requests
3+
from typing import Union
4+
from datetime import datetime
5+
from django.utils.timezone import make_aware
6+
from django.core.management.base import BaseCommand
7+
from django.conf import settings
8+
from django.db import models
9+
10+
from api.models import CronJob, CronJobStatus
11+
from country_plan.models import CountryPlan
12+
13+
logger = logging.getLogger(__name__)
14+
15+
NAME = 'ingest_country_plan_file'
16+
# Ref: https://github.com/IFRCGo/go-api/issues/1614
17+
SOURCE = 'https://go-api.ifrc.org/api/publicsiteappeals?AppealsTypeID=1851&Hidden=false'
18+
19+
20+
class Command(BaseCommand):
21+
@staticmethod
22+
def parse_date(text: str) -> Union[datetime, None]:
23+
"""
24+
Convert Appeal API datetime into django datetime
25+
Parameters
26+
----------
27+
text : str
28+
Datetime eg: 2022-11-29T11:24:00
29+
"""
30+
if text:
31+
return make_aware(
32+
# NOTE: Format is assumed by looking at the data from Appeal API
33+
datetime.strptime(text, '%Y-%m-%dT%H:%M:%S')
34+
)
35+
36+
def load_for_country(self, country_data):
37+
country_iso2 = country_data.get('LocationCountryCode')
38+
country_name = country_data.get('LocationCountryName')
39+
public_plan_url = country_data.get('BaseDirectory') or '' + country_data.get('BaseFileName') or ''
40+
inserted_date = self.parse_date(country_data.get('Inserted'))
41+
if (
42+
(country_iso2 is None and country_name is None) or
43+
public_plan_url is None or
44+
inserted_date is None
45+
):
46+
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()
51+
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:
55+
# No need to do anything here
56+
return
57+
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(
60+
public_plan_url,
61+
# NOTE: File provided are PDF,
62+
f"public-plan-{country_data['BaseFileName']}.pdf",
63+
)
64+
country_plan.is_publish = True
65+
country_plan.save(
66+
update_fields=(
67+
'appeal_api_inserted_date',
68+
'public_plan_file', # By load_file_to_country_plan
69+
'is_publish',
70+
)
71+
)
72+
return True
73+
74+
def load(self):
75+
updated = 0
76+
auth = (settings.APPEALS_USER, settings.APPEALS_PASS)
77+
results = requests.get(SOURCE, auth=auth, headers={'Accept': 'application/json'}).json()
78+
for result in results:
79+
try:
80+
if self.load_for_country(result):
81+
updated += 1
82+
except Exception as ex:
83+
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
94+
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+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.16 on 2023-01-02 07:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('country_plan', '0003_auto_20221128_0831'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='countryplan',
15+
name='appeal_api_inserted_date',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
]

country_plan/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
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
67

8+
from main.utils import DownloadFileManager
79
from api.models import Country
810

911

@@ -80,9 +82,21 @@ class CountryPlan(CountryPlanAbstract):
8082
people_targeted = models.IntegerField(verbose_name=_('People Targeted'), blank=True, null=True)
8183
is_publish = models.BooleanField(default=False, verbose_name=_('Published'))
8284

85+
# 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)
87+
8388
def __str__(self):
8489
return f'{self.country}'
8590

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+
86100
# NOTE: To be used by CountryPlanSerializer (From CountryPlanViewset)
87101
def full_country_plan_mc(self):
88102
all_mc = list(self.country_plan_mc.all())

country_plan/tests/__init__.py

Whitespace-only changes.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from unittest import mock
2+
from django.core.management import call_command
3+
4+
from main.test_case import APITestCase
5+
from api.factories.country import CountryFactory
6+
from country_plan.factories import CountryPlanFactory
7+
from country_plan.models import CountryPlan
8+
from country_plan.management.commands.ingest_country_plan_file import SOURCE
9+
10+
# NOTE: Only defined used fields
11+
FILE_BASE_DIRECTORY = 'https://example.org/Download.aspx?FileId='
12+
APPEAL_COUNTRY_PLAN_MOCK_RESPONSE = [
13+
{
14+
'BaseDirectory': FILE_BASE_DIRECTORY,
15+
'BaseFileName': '000000',
16+
'Inserted': '2022-11-29T11:24:00',
17+
'LocationCountryCode': 'SY',
18+
'LocationCountryName': 'Syrian Arab Republic'
19+
},
20+
{
21+
'BaseDirectory': FILE_BASE_DIRECTORY,
22+
'BaseFileName': '000001',
23+
'Inserted': '2022-11-29T11:24:00',
24+
'LocationCountryCode': 'CD',
25+
'LocationCountryName': 'Congo, The Democratic Republic Of The'
26+
},
27+
{
28+
'BaseDirectory': FILE_BASE_DIRECTORY,
29+
'BaseFileName': '000002',
30+
'Inserted': '2022-11-29T11:24:00',
31+
'LocationCountryCode': 'MM',
32+
'LocationCountryName': 'Myanmar'
33+
},
34+
{
35+
'BaseDirectory': FILE_BASE_DIRECTORY,
36+
'BaseFileName': '000003',
37+
'Inserted': '2022-11-29T11:24:00',
38+
'LocationCountryCode': 'TM',
39+
'LocationCountryName': 'Turkmenistan'
40+
},
41+
{
42+
'BaseDirectory': FILE_BASE_DIRECTORY,
43+
'BaseFileName': '000004',
44+
'Inserted': '2022-11-29T11:24:00',
45+
'LocationCountryCode': 'GR',
46+
'LocationCountryName': 'Greece'
47+
}
48+
]
49+
50+
51+
class MockResponse():
52+
class FileStream():
53+
def __init__(self, stream):
54+
self.stream = stream
55+
56+
def raise_for_status(self):
57+
pass
58+
59+
def iter_content(self, **_):
60+
return self.stream
61+
62+
def __init__(self, json=None, stream=None):
63+
self._json = json
64+
self.stream = stream
65+
66+
def json(self):
67+
return self._json
68+
69+
def __enter__(self):
70+
return MockResponse.FileStream(self.stream)
71+
72+
def __exit__(self, *_):
73+
pass
74+
75+
76+
class CountryPlanIngestTest(APITestCase):
77+
def mock_request(url, *_, **kwargs):
78+
if url == SOURCE:
79+
return MockResponse(json=APPEAL_COUNTRY_PLAN_MOCK_RESPONSE)
80+
if url.startswith(FILE_BASE_DIRECTORY):
81+
return MockResponse(stream=[b''])
82+
83+
@mock.patch('country_plan.management.commands.ingest_country_plan_file.requests.get', side_effect=mock_request)
84+
@mock.patch('main.utils.requests.get', side_effect=mock_request)
85+
def test_country_plan_ingest(self, *_):
86+
CountryPlanFactory.create(
87+
country=CountryFactory.create(
88+
name='Random Country',
89+
iso='RC',
90+
),
91+
)
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
100+
assert CountryPlan.objects.filter(is_publish=True).count() == 0
101+
assert CountryPlan.objects.exclude(public_plan_file='').count() == 0
102+
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

0 commit comments

Comments
 (0)