Skip to content

Commit 31b3253

Browse files
committed
Change grant approved email to use total amount instead of per-category
- Add `total_grantee_reimbursement_amount` property to Grant that excludes internal categories from the total shown to grantees - Replace `has_approved_travel`, `has_approved_accommodation`, `travel_amount` placeholders with simpler total_amount and ticket_only placeholders - Update tests to reflect new placeholder structure This allows showing grantees a single total amount they can use flexibly for travel and/or accommodation, rather than separate category amounts.
1 parent fcbab6e commit 31b3253

5 files changed

Lines changed: 43 additions & 73 deletions

File tree

backend/grants/models.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,24 @@ def has_approved_accommodation(self):
273273
).exists()
274274

275275
@property
276-
def total_allocated_amount(self):
277-
return sum(r.granted_amount for r in self.reimbursements.all())
276+
def total_allocated_amount(self) -> Decimal:
277+
"""Return total of all reimbursements including ticket."""
278+
return sum(
279+
(r.granted_amount for r in self.reimbursements.all()),
280+
start=Decimal(0),
281+
)
282+
283+
@property
284+
def total_grantee_reimbursement_amount(self) -> Decimal:
285+
"""Return total reimbursement excluding ticket."""
286+
return sum(
287+
(
288+
r.granted_amount
289+
for r in self.reimbursements.all()
290+
if r.category.category != GrantReimbursementCategory.Category.TICKET
291+
),
292+
start=Decimal(0),
293+
)
278294

279295
def has_approved(self, type_):
280296
return self.reimbursements.filter(category__category=type_).exists()

backend/grants/tasks.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,34 @@
1414
logger = logging.getLogger(__name__)
1515

1616

17-
def get_name(user: User | None, fallback: str = "<no name specified>"):
17+
def get_name(user: User | None, fallback: str = "<no name specified>") -> str:
1818
if not user:
1919
return fallback
2020

2121
return user.full_name or user.name or user.username or fallback
2222

2323

2424
@app.task
25-
def send_grant_reply_approved_email(*, grant_id, is_reminder):
25+
def send_grant_reply_approved_email(*, grant_id: int, is_reminder: bool) -> None:
2626
logger.info("Sending Reply APPROVED email for Grant %s", grant_id)
2727
grant = Grant.objects.get(id=grant_id)
2828
reply_url = urljoin(settings.FRONTEND_URL, "/grants/reply/")
2929

30+
total_amount = grant.total_grantee_reimbursement_amount
31+
ticket_only = total_amount == 0 and grant.reimbursements.exists()
32+
3033
variables = {
3134
"reply_url": reply_url,
3235
"start_date": f"{grant.conference.start:%-d %B}",
3336
"end_date": f"{grant.conference.end + timedelta(days=1):%-d %B}",
3437
"deadline_date_time": f"{grant.applicant_reply_deadline:%-d %B %Y %H:%M %Z}",
3538
"deadline_date": f"{grant.applicant_reply_deadline:%-d %B %Y}",
3639
"visa_page_link": urljoin(settings.FRONTEND_URL, "/visa"),
37-
"has_approved_travel": grant.has_approved_travel(),
38-
"has_approved_accommodation": grant.has_approved_accommodation(),
40+
"total_amount": f"{total_amount:.0f}" if total_amount > 0 else None,
41+
"ticket_only": ticket_only,
3942
"is_reminder": is_reminder,
4043
}
4144

42-
if grant.has_approved_travel():
43-
from grants.models import GrantReimbursementCategory
44-
45-
travel_reimbursements = grant.reimbursements.filter(
46-
category__category=GrantReimbursementCategory.Category.TRAVEL
47-
)
48-
travel_amount = sum(r.granted_amount for r in travel_reimbursements)
49-
50-
if not travel_amount or travel_amount == 0:
51-
raise ValueError(
52-
"Grant travel amount is set to Zero, can't send the email!"
53-
)
54-
55-
variables["travel_amount"] = f"{travel_amount:.0f}"
56-
5745
_new_send_grant_email(
5846
template_identifier=EmailTemplateIdentifier.grant_approved,
5947
grant=grant,

backend/grants/tests/test_tasks.py

Lines changed: 10 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ def test_handle_grant_reply_sent_reminder(settings, sent_emails):
166166
assert sent_email.placeholders["deadline_date"] == "1 February 2023"
167167
assert sent_email.placeholders["reply_url"] == "https://pycon.it/grants/reply/"
168168
assert sent_email.placeholders["visa_page_link"] == "https://pycon.it/visa"
169-
assert not sent_email.placeholders["has_approved_travel"]
170-
assert not sent_email.placeholders["has_approved_accommodation"]
169+
assert sent_email.placeholders["ticket_only"]
170+
assert sent_email.placeholders["total_amount"] is None
171171
assert sent_email.placeholders["is_reminder"]
172172

173173

@@ -240,51 +240,16 @@ def test_handle_grant_approved_ticket_travel_accommodation_reply_sent(
240240
)
241241
assert sent_email.placeholders["start_date"] == "2 May"
242242
assert sent_email.placeholders["end_date"] == "6 May"
243-
assert sent_email.placeholders["travel_amount"] == "680"
243+
# Total amount is 680 (travel) + 200 (accommodation) = 880, excluding ticket
244+
assert sent_email.placeholders["total_amount"] == "880"
244245
assert sent_email.placeholders["deadline_date_time"] == "1 February 2023 23:59 UTC"
245246
assert sent_email.placeholders["deadline_date"] == "1 February 2023"
246247
assert sent_email.placeholders["reply_url"] == "https://pycon.it/grants/reply/"
247248
assert sent_email.placeholders["visa_page_link"] == "https://pycon.it/visa"
248-
assert sent_email.placeholders["has_approved_travel"]
249-
assert sent_email.placeholders["has_approved_accommodation"]
249+
assert not sent_email.placeholders["ticket_only"]
250250
assert not sent_email.placeholders["is_reminder"]
251251

252252

253-
def test_handle_grant_approved_ticket_travel_accommodation_fails_with_no_amount(
254-
settings,
255-
):
256-
settings.FRONTEND_URL = "https://pycon.it"
257-
258-
conference = ConferenceFactory(
259-
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
260-
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
261-
)
262-
user = UserFactory(
263-
full_name="Marco Acierno",
264-
email="marco@placeholder.it",
265-
name="Marco",
266-
username="marco",
267-
)
268-
269-
grant = GrantFactory(
270-
conference=conference,
271-
applicant_reply_deadline=datetime(2023, 2, 1, 23, 59, tzinfo=timezone.utc),
272-
user=user,
273-
)
274-
GrantReimbursementFactory(
275-
grant=grant,
276-
category__conference=conference,
277-
category__travel=True,
278-
category__max_amount=Decimal("680"),
279-
granted_amount=Decimal("0"),
280-
)
281-
282-
with pytest.raises(
283-
ValueError, match="Grant travel amount is set to Zero, can't send the email!"
284-
):
285-
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)
286-
287-
288253
def test_handle_grant_approved_ticket_only_reply_sent(settings, sent_emails):
289254
from notifications.models import EmailTemplateIdentifier
290255
from notifications.tests.factories import EmailTemplateFactory
@@ -344,8 +309,8 @@ def test_handle_grant_approved_ticket_only_reply_sent(settings, sent_emails):
344309
assert sent_email.placeholders["deadline_date"] == "1 February 2023"
345310
assert sent_email.placeholders["reply_url"] == "https://pycon.it/grants/reply/"
346311
assert sent_email.placeholders["visa_page_link"] == "https://pycon.it/visa"
347-
assert not sent_email.placeholders["has_approved_travel"]
348-
assert not sent_email.placeholders["has_approved_accommodation"]
312+
assert sent_email.placeholders["ticket_only"]
313+
assert sent_email.placeholders["total_amount"] is None
349314
assert not sent_email.placeholders["is_reminder"]
350315

351316

@@ -415,9 +380,9 @@ def test_handle_grant_approved_travel_reply_sent(settings, sent_emails):
415380
assert sent_email.placeholders["deadline_date"] == "1 February 2023"
416381
assert sent_email.placeholders["reply_url"] == "https://pycon.it/grants/reply/"
417382
assert sent_email.placeholders["visa_page_link"] == "https://pycon.it/visa"
418-
assert sent_email.placeholders["has_approved_travel"]
419-
assert not sent_email.placeholders["has_approved_accommodation"]
420-
assert sent_email.placeholders["travel_amount"] == "400"
383+
# Total amount is 400 (travel only), excluding ticket
384+
assert sent_email.placeholders["total_amount"] == "400"
385+
assert not sent_email.placeholders["ticket_only"]
421386
assert not sent_email.placeholders["is_reminder"]
422387

423388

backend/integrations/plain_cards.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,26 @@ def create_grant_card(request, user, conference):
7676
{
7777
"componentText": {
7878
"textColor": "MUTED",
79-
"text": "Travel amount",
79+
"text": "Total reimbursement",
8080
}
8181
}
8282
],
8383
"rowAsideContent": [
8484
{
8585
"componentText": {
8686
"textColor": "NORMAL",
87-
"text": f"€{sum(r.granted_amount for r in grant.reimbursements.filter(category__category='travel'))}",
87+
"text": f"€{grant.total_grantee_reimbursement_amount}",
8888
}
8989
}
9090
],
9191
}
9292
}
93-
if grant.has_approved_travel()
93+
if grant.total_grantee_reimbursement_amount > 0
9494
else None
9595
),
9696
(
9797
{"componentSpacer": {"spacerSize": "M"}}
98-
if grant.has_approved_travel()
98+
if grant.total_grantee_reimbursement_amount > 0
9999
else None
100100
),
101101
{

backend/notifications/models.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,16 @@ class EmailTemplate(TimeStampedModel):
113113
],
114114
EmailTemplateIdentifier.grant_approved: [
115115
*BASE_PLACEHOLDERS,
116+
"conference_name",
117+
"user_name",
116118
"reply_url",
117119
"start_date",
118120
"end_date",
119121
"deadline_date_time",
120122
"deadline_date",
121123
"visa_page_link",
122-
"has_approved_travel",
123-
"has_approved_accommodation",
124-
"travel_amount",
124+
"total_amount",
125+
"ticket_only",
125126
"is_reminder",
126127
],
127128
EmailTemplateIdentifier.grant_rejected: [

0 commit comments

Comments
 (0)