Skip to content

Commit a4c5781

Browse files
Merge pull request #1670 from IFRCGo/develop
2023.02 Release
2 parents 54afcbb + ea93947 commit a4c5781

File tree

21 files changed

+1672
-838
lines changed

21 files changed

+1672
-838
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Add issues to Backlog
2+
3+
on:
4+
issues:
5+
types:
6+
- opened
7+
8+
jobs:
9+
add-to-project:
10+
name: Add issue to project
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/[email protected]
14+
with:
15+
project-url: https://github.com/orgs/IFRCGo/projects/12
16+
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77
## Unreleased
88

9+
## 1.1.469
10+
11+
### Added
12+
- Ingest country plan and internal plan files
13+
- Bump up cryptography and django modules
14+
- Surge Alert statuses: Open, Closed, Stood down
15+
916
## 1.1.468
1017

1118
### Added
@@ -2143,7 +2150,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
21432150

21442151
## 0.1.20
21452152

2146-
[Unreleased]: https://github.com/IFRCGo/go-api/compare/1.1.468...HEAD
2153+
[Unreleased]: https://github.com/IFRCGo/go-api/compare/1.1.469...HEAD
2154+
[1.1.469]: https://github.com/IFRCGo/go-api/compare/1.1.468...1.1.469
21472155
[1.1.468]: https://github.com/IFRCGo/go-api/compare/1.1.467...1.1.468
21482156
[1.1.467]: https://github.com/IFRCGo/go-api/compare/1.1.466...1.1.467
21492157
[1.1.466]: https://github.com/IFRCGo/go-api/compare/1.1.465...1.1.466

api/management/commands/sync_molnix.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def get_datetime(datetime_string):
107107
return None
108108
return date_parser.parse(datetime_string)
109109

110+
110111
def get_status_message(positions_messages, deployments_messages, positions_warnings, deployments_warnings):
111112
msg = ''
112113
msg += 'Summary of Open Positions Import:\n\n'
@@ -125,26 +126,21 @@ def get_status_message(positions_messages, deployments_messages, positions_warni
125126
msg += '\n\n'
126127
return msg
127128

128-
def add_tags_to_obj(obj, tags):
129-
# We clear all tags first, and then re-add them
130-
tag_molnix_ids = [t['id'] for t in tags]
131-
obj.molnix_tags.clear()
132-
for molnix_id in tag_molnix_ids:
133-
try:
134-
t = MolnixTag.objects.get(molnix_id=molnix_id)
135-
except:
136-
logger.error('ERROR - tag not found: %d' % molnix_id)
137-
continue
138-
obj.molnix_tags.add(t)
139-
obj.save()
140129

130+
def add_tags_to_obj(obj, tags):
131+
_ids = [int(t['id']) for t in tags]
132+
tags = list( # Fetch all at once
133+
MolnixTag.objects.filter(molnix_id__in=_ids)
134+
)
135+
if len(tags) != len(_ids): # Show warning if all tags are not available
136+
missing_tag_ids = list(set(_ids) - set([tag.id for tag in tags]))
137+
logger.warning(f'Missing _ids: {missing_tag_ids}')
138+
# or ^^^^^^^ logger.error if we need to add molnix tags manually.
139+
obj.molnix_tags.set(tags) # Add new ones, remove old ones
141140

142141

143142
def sync_deployments(molnix_deployments, molnix_api, countries):
144-
#import json
145-
#print(json.dumps(molnix_deployments, indent=2))
146143
molnix_ids = [d['id'] for d in molnix_deployments]
147-
148144
warnings = []
149145
messages = []
150146
successful_creates = 0

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)