Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## Unreleased

### Added
- Add potential funding (`PotentialFunding`) ingestion and expose it on Appeal/AppealHistory
- EAP: add RCRC Climate Centre to registration and lift activity limit
- Use APP_subtype instead of APD_TYP_Id in ingest_appeals
- DREF: add automated DREF Final Report alert emails and cron job
Expand Down
15 changes: 11 additions & 4 deletions api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,22 +840,28 @@ def confirm_events(self, request, queryset):
confirm_events.short_description = "Confirm emergencies as correct"

def save_model(self, request, obj, form, change):
previous_amount_funded = None
previous_funding = None
if change:
previous_amount_funded = models.Appeal.objects.filter(pk=obj.pk).values_list("amount_funded", flat=True).first()
previous_funding = (
models.Appeal.objects.filter(pk=obj.pk).values_list("amount_funded", "amount_potential_funding").first()
)

if obj.country:
obj.region = obj.country.region

super().save_model(request, obj, form, change)

if not change or previous_amount_funded == obj.amount_funded:
if not change or previous_funding == (obj.amount_funded, obj.amount_potential_funding):
return

latest_history = models.AppealHistory.objects.filter(appeal=obj).order_by("id").last()

# Avoid duplicates if another save flow already created this exact savepoint.
if latest_history and latest_history.amount_funded == obj.amount_funded:
if (
latest_history
and latest_history.amount_funded == obj.amount_funded
and latest_history.amount_potential_funding == obj.amount_potential_funding
):
return

now = timezone.now()
Expand All @@ -868,6 +874,7 @@ def save_model(self, request, obj, form, change):
num_beneficiaries=obj.num_beneficiaries,
amount_requested=obj.amount_requested,
amount_funded=obj.amount_funded,
amount_potential_funding=obj.amount_potential_funding,
valid_from=now,
valid_to=timezone.make_aware(datetime(2200, 1, 1)),
start_date=obj.start_date,
Expand Down
2 changes: 2 additions & 0 deletions api/factories/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class AppealFactory(factory.django.DjangoModelFactory):
num_beneficiaries = fuzzy.FuzzyInteger(0)
amount_requested = fuzzy.FuzzyInteger(0)
amount_funded = fuzzy.FuzzyInteger(0)
amount_potential_funding = fuzzy.FuzzyInteger(0)
event = factory.SubFactory(EventFactory)
country = factory.SubFactory(CountryFactory)

Expand All @@ -129,6 +130,7 @@ class AppealHistoryFactory(factory.django.DjangoModelFactory):
num_beneficiaries = fuzzy.FuzzyInteger(0)
amount_requested = fuzzy.FuzzyInteger(0)
amount_funded = fuzzy.FuzzyInteger(0)
amount_potential_funding = fuzzy.FuzzyInteger(0)
appeal = factory.SubFactory(AppealFactory)
country = factory.SubFactory(CountryFactory)
atype = factory.SubFactory(AppealTypeFactory)
Expand Down
7 changes: 5 additions & 2 deletions api/management/commands/ingest_appeals.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def parse_appeal_record(self, r, **options):
# instead of detail1["APD_TYP_Id"]:
atype = atypes.get(r.get("APP_subtype"), AppealType.DREF)

amount_funded = triggering_amount = 0
amount_funded = amount_potential_funding = triggering_amount = 0
for detl in details:
if self.parse_date(detl["APD_endDate"]) < end_date:
continue
Expand All @@ -290,10 +290,12 @@ def parse_appeal_record(self, r, **options):
triggering_ts = self.parse_unix_timestamp(triggering_ts)
triggering_amount += triggering_ts if triggering_ts else 0
if atype == AppealType.DREF:
# appeals are always fully-funded
amount_funded += detl["APD_amountCHF"] if detl["APD_amountCHF"] else 0
# DREF appeals are always fully-funded, we skip potential funding for them
else:
amount_funded += detl["ContributionAmount"] if detl["ContributionAmount"] else 0
# PotentialFunding may be absent on some upstream records, so fetch it defensively.
amount_potential_funding += detl.get("PotentialFunding") or 0

# for new, open appeals, if we have a country, try to guess what emergency it belongs to.
# only consider emergencies within the past 90 days
Expand Down Expand Up @@ -328,6 +330,7 @@ def parse_appeal_record(self, r, **options):
"num_beneficiaries": detail1["APD_noBeneficiaries"],
"amount_requested": detail1["APD_amountCHF"],
"amount_funded": amount_funded,
"amount_potential_funding": amount_potential_funding,
"real_data_update": modify_time,
"triggering_amount": float(int(triggering_amount) % 10**10), # to avoid overflow
}
Expand Down
23 changes: 23 additions & 0 deletions api/migrations/0002_appeal_amount_potential_funding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0001_squashed_0232'),
]

operations = [
migrations.AddField(
model_name='appeal',
name='amount_potential_funding',
field=models.FloatField(default=0.0, verbose_name='amount potential funding'),
),
migrations.AddField(
model_name='appealhistory',
name='amount_potential_funding',
field=models.FloatField(default=0.0, verbose_name='amount potential funding'),
),
]
2 changes: 2 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,7 @@ class Appeal(models.Model):
num_beneficiaries = models.IntegerField(verbose_name=_("number of beneficiaries"), default=0)
amount_requested = models.FloatField(verbose_name=_("amount requested"), default=0.00)
amount_funded = models.FloatField(verbose_name=_("amount funded"), default=0.00)
amount_potential_funding = models.FloatField(verbose_name=_("amount potential funding"), default=0.00)

start_date = models.DateTimeField(verbose_name=_("start date"), null=True)
end_date = models.DateTimeField(verbose_name=_("end date"), null=True)
Expand Down Expand Up @@ -2089,6 +2090,7 @@ class AppealHistory(models.Model):
num_beneficiaries = models.IntegerField(verbose_name=_("number of beneficiaries"), default=0)
amount_requested = models.FloatField(verbose_name=_("amount requested"), default=0.00)
amount_funded = models.FloatField(verbose_name=_("amount funded"), default=0.00)
amount_potential_funding = models.FloatField(verbose_name=_("amount potential funding"), default=0.00)
valid_from = models.DateTimeField(verbose_name=_("valid_from"), null=True)
valid_to = models.DateTimeField(verbose_name=_("valid_to"), null=True)
aid = models.CharField(verbose_name=_("appeal ID"), max_length=20)
Expand Down
5 changes: 4 additions & 1 deletion api/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def add_update_appeal_history(sender, instance, created, **kwargs):
fields_watched = [
field.name
for field in AppealHistory._meta.get_fields()
if field.name not in ["id", "appeal", "valid_from", "valid_to", "aid", "amount_funded"]
if field.name not in ["id", "appeal", "valid_from", "valid_to", "aid", "amount_funded", "amount_potential_funding"]
]
now = timezone.now()
changed = False
Expand All @@ -229,6 +229,7 @@ def add_update_appeal_history(sender, instance, created, **kwargs):
num_beneficiaries=instance.num_beneficiaries,
amount_requested=instance.amount_requested,
amount_funded=instance.amount_funded,
amount_potential_funding=instance.amount_potential_funding,
valid_from=now,
# TODO: use coalesce to fill valid_to instead of defining here.
valid_to=datetime(2200, 1, 1, tzinfo=datetime_timezone.utc),
Expand Down Expand Up @@ -261,6 +262,7 @@ def add_update_appeal_history(sender, instance, created, **kwargs):
num_beneficiaries=instance.num_beneficiaries,
amount_requested=instance.amount_requested,
amount_funded=instance.amount_funded,
amount_potential_funding=instance.amount_potential_funding,
valid_from=now,
valid_to=datetime(2200, 1, 1, tzinfo=datetime_timezone.utc),
start_date=instance.start_date,
Expand Down Expand Up @@ -290,6 +292,7 @@ def add_update_appeal_history(sender, instance, created, **kwargs):
num_beneficiaries=instance.num_beneficiaries,
amount_requested=instance.amount_requested,
amount_funded=instance.amount_funded,
amount_potential_funding=instance.amount_potential_funding,
valid_from=now,
# TODO: use coalesce to fill valid_to instead of defining here.
valid_to=datetime(2200, 1, 1, tzinfo=datetime_timezone.utc),
Expand Down
5 changes: 5 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ class Meta:
"amount_requested",
"code",
"amount_funded",
"amount_potential_funding",
"status",
"status_display",
"start_date",
Expand Down Expand Up @@ -1527,6 +1528,7 @@ class Meta:
"num_beneficiaries",
"amount_requested",
"amount_funded",
"amount_potential_funding",
"start_date",
"end_date",
"real_data_update",
Expand Down Expand Up @@ -1570,6 +1572,7 @@ class Meta:
"num_beneficiaries",
"amount_requested",
"amount_funded",
"amount_potential_funding",
"start_date",
"end_date",
"real_data_update",
Expand Down Expand Up @@ -1611,6 +1614,7 @@ class Meta:
"num_beneficiaries",
"amount_requested",
"amount_funded",
"amount_potential_funding",
"start_date",
"end_date",
"real_data_update",
Expand Down Expand Up @@ -1670,6 +1674,7 @@ class Meta:
"num_beneficiaries",
"amount_requested",
"amount_funded",
"amount_potential_funding",
"start_date",
"end_date",
"real_data_update",
Expand Down
26 changes: 25 additions & 1 deletion api/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,24 @@ def setUp(self):
event = models.Event.objects.create(name="associated event", summary="foo")
country = models.Country.objects.create(name="country")
models.Appeal.objects.create(
aid="test1", name="appeal", atype=1, code="abc", needs_confirmation=True, event=event, country=country
aid="test1",
name="appeal",
atype=1,
code="abc",
needs_confirmation=True,
event=event,
country=country,
amount_funded=1000,
amount_potential_funding=2000,
)

def test_unconfirmed_event(self):
response = self.client.get("/api/v2/appeal/?code=abc")
response = dict(dict(response.data)["results"][0])
self.assertIsNone(response["event"])
self.assertIsNotNone(response["country"])
self.assertEqual(response["amount_funded"], 1000)
self.assertEqual(response["amount_potential_funding"], 2000)

def test_history_not_duplicated_when_same_aid_is_shared(self):
country = models.Country.objects.create(name="history-country")
Expand All @@ -102,6 +112,20 @@ def test_history_not_duplicated_when_same_aid_is_shared(self):
final_history_count = models.AppealHistory.objects.filter(appeal=appeal1).count()
self.assertEqual(final_history_count, initial_history_count)

# amount_funded/amount_potential_funding are excluded from the watched fields (like amount_funded
# already was), so changing them alone should not create a new history row either. The existing
# (now stale) history row is intentionally left untouched in this case.
appeal1.amount_funded = 500
appeal1.amount_potential_funding = 750
appeal1.save(update_fields=["amount_funded", "amount_potential_funding"])

after_funding_update_count = models.AppealHistory.objects.filter(appeal=appeal1).count()
self.assertEqual(after_funding_update_count, initial_history_count)

latest_history = models.AppealHistory.objects.filter(appeal=appeal1).order_by("id").last()
self.assertEqual(latest_history.amount_funded, 0)
self.assertEqual(latest_history.amount_potential_funding, 0)


class FieldReportTest(TestCase):

Expand Down
2 changes: 1 addition & 1 deletion assets
Submodule assets updated 1 files
+6 −0 openapi-schema.yaml
Loading