Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grants: email copy updates #3712

Merged
merged 17 commits into from
Feb 8, 2024
1 change: 1 addition & 0 deletions backend/api/conferences/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class Conference:
map: Optional[Map] = strawberry.field(resolver=resolve_map)

pretix_event_url: str
visa_application_form_link: str

@strawberry.field
def voucher(self, info, code: str) -> Optional[Voucher]:
Expand Down
1 change: 1 addition & 0 deletions backend/conferences/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class ConferenceAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin):
"topics",
"audience_levels",
"languages",
"visa_application_form_link",
)
},
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.7 on 2024-02-01 11:51

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("conferences", "0039_conference_slack_new_sponsor_lead_incoming_webhook_url"),
]

operations = [
migrations.AddField(
model_name="conference",
name="visa_application_form_link",
field=models.URLField(
blank=True, default="", verbose_name="Visa application form link"
),
),
]
4 changes: 4 additions & 0 deletions backend/conferences/models/conference.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel):
default=None,
)

visa_application_form_link = models.URLField(
_("Visa application form link"), blank=True, default=""
)

youtube_video_bottom_text = models.TextField(
default="",
blank=True,
Expand Down
18 changes: 2 additions & 16 deletions backend/grants/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,28 +160,14 @@ def _check_amounts_are_not_empty(grant: Grant, request):
)
return

if (
grant.grant_type
in (
Grant.ApprovedType.ticket_accommodation,
Grant.ApprovedType.ticket_travel_accommodation,
)
and grant.accommodation_amount is None
):
if grant.has_approved_accommodation() and grant.accommodation_amount is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not for this PR, but we have a very similar logic in the Grant save method when it calculates the amounts to set. We could refactor it to use those 2 new methods

messages.error(
request,
f"Grant for {grant.name} is missing 'Accommodation Amount'!",
)
return

if (
grant.grant_type
in (
Grant.ApprovedType.ticket_travel,
Grant.ApprovedType.ticket_travel_accommodation,
)
and grant.travel_amount is None
):
if grant.has_approved_travel() and grant.travel_amount is None:
messages.error(
request,
f"Grant for {grant.name} is missing 'Travel Amount'!",
Expand Down
12 changes: 12 additions & 0 deletions backend/grants/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,15 @@ def get_admin_url(self):
"admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name),
args=(self.pk,),
)

def has_approved_travel(self):
return (
self.approved_type == Grant.ApprovedType.ticket_travel_accommodation
or self.approved_type == Grant.ApprovedType.ticket_travel
)

def has_approved_accommodation(self):
return (
self.approved_type == Grant.ApprovedType.ticket_accommodation
or self.approved_type == Grant.ApprovedType.ticket_travel_accommodation
)
30 changes: 12 additions & 18 deletions backend/grants/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,31 @@ def send_grant_reply_approved_email(*, grant_id, is_reminder):
"Reminder: Financial Aid Update" if is_reminder else "Financial Aid Update"
)

template = None
if not grant.conference.visa_application_form_link:
raise ValueError(
"Visa Application Form Link Missing: Please ensure the link to the Visa "
"Application Form is set in the Conference admin settings."
)

template = EmailTemplate.GRANT_APPROVED
variables = {
"replyLink": reply_url,
"startDate": f"{grant.conference.start:%-d %B}",
"endDate": f"{grant.conference.end+timedelta(days=1):%-d %B}",
"deadlineDateTime": f"{grant.applicant_reply_deadline:%-d %B %Y %H:%M %Z}",
"deadlineDate": f"{grant.applicant_reply_deadline:%-d %B %Y}",
"visaApplicationFormLink": grant.conference.visa_application_form_link,
"hasApprovedTravel": grant.has_approved_travel(),
"hasApprovedAccommodation": grant.has_approved_accommodation(),
}

if grant.approved_type == Grant.ApprovedType.ticket_only:
template = EmailTemplate.GRANT_APPROVED_TICKET_ONLY
elif grant.approved_type == Grant.ApprovedType.ticket_travel:
template = EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL
if grant.travel_amount == 0:
raise ValueError(
"Grant travel amount is set to Zero, can't send the email!"
)

variables["amount"] = f"{grant.travel_amount:.0f}"
elif grant.approved_type == Grant.ApprovedType.ticket_accommodation:
template = EmailTemplate.GRANT_APPROVED_TICKET_ACCOMMODATION
elif grant.approved_type == Grant.ApprovedType.ticket_travel_accommodation:
template = EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL_ACCOMMODATION
if grant.travel_amount == 0:
if grant.has_approved_travel():
if not grant.travel_amount:
raise ValueError(
"Grant travel amount is set to Zero, can't send the email!"
)

variables["amount"] = f"{grant.travel_amount:.0f}"
else:
raise ValueError(f"Grant Approved type `{grant.approved_type}` not valid.")

_send_grant_email(template=template, subject=subject, grant=grant, **variables)

Expand Down
2 changes: 2 additions & 0 deletions backend/grants/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def test_send_reply_emails_approved_missing_amount(rf, grant_factory, mocker):
status=Grant.Status.approved,
approved_type=Grant.ApprovedType.ticket_accommodation,
total_amount=None,
conference__visa_application_form_link="https://forms.com/visa",
)
grant.total_amount = None
grant.save()
Expand All @@ -57,6 +58,7 @@ def test_send_reply_emails_approved_set_deadline_in_fourteen_days(
status=Grant.Status.approved,
approved_type=Grant.ApprovedType.ticket_accommodation,
total_amount=800,
conference__visa_application_form_link="https://forms.com/visa",
)
request = rf.get("/")

Expand Down
85 changes: 73 additions & 12 deletions backend/grants/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def test_handle_grant_reply_sent_reminder(conference_factory, grant_factory, set
conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
visa_application_form_link="https://example.com/visa-application-form",
)
user = UserFactory(
full_name="Marco Acierno",
Expand All @@ -137,7 +138,7 @@ def test_handle_grant_reply_sent_reminder(conference_factory, grant_factory, set
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=True)

email_mock.assert_called_once_with(
template=EmailTemplate.GRANT_APPROVED_TICKET_ONLY,
template=EmailTemplate.GRANT_APPROVED,
to="marco@placeholder.it",
subject=f"[{grant.conference.name}] Reminder: Financial Aid Update",
variables={
Expand All @@ -148,6 +149,9 @@ def test_handle_grant_reply_sent_reminder(conference_factory, grant_factory, set
"deadlineDateTime": "1 February 2023 23:59 UTC",
"deadlineDate": "1 February 2023",
"replyLink": "https://pycon.it/grants/reply/",
"visaApplicationFormLink": "https://example.com/visa-application-form",
"hasApprovedTravel": False,
"hasApprovedAccommodation": False,
},
reply_to=["grants@pycon.it"],
)
Expand All @@ -161,6 +165,7 @@ def test_handle_grant_approved_ticket_travel_accommodation_reply_sent(
conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
visa_application_form_link="https://example.com/visa-application-form",
)
user = UserFactory(
full_name="Marco Acierno",
Expand All @@ -181,7 +186,7 @@ def test_handle_grant_approved_ticket_travel_accommodation_reply_sent(
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)

email_mock.assert_called_once_with(
template=EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL_ACCOMMODATION,
template=EmailTemplate.GRANT_APPROVED,
to="marco@placeholder.it",
subject=f"[{grant.conference.name}] Financial Aid Update",
variables={
Expand All @@ -193,6 +198,9 @@ def test_handle_grant_approved_ticket_travel_accommodation_reply_sent(
"deadlineDateTime": "1 February 2023 23:59 UTC",
"deadlineDate": "1 February 2023",
"replyLink": "https://pycon.it/grants/reply/",
"visaApplicationFormLink": "https://example.com/visa-application-form",
"hasApprovedTravel": True,
"hasApprovedAccommodation": True,
},
reply_to=["grants@pycon.it"],
)
Expand All @@ -206,6 +214,7 @@ def test_handle_grant_approved_ticket_travel_accommodation_fails_with_no_amount(
conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
visa_application_form_link="https://example.com/visa-application-form",
)
user = UserFactory(
full_name="Marco Acierno",
Expand All @@ -228,6 +237,32 @@ def test_handle_grant_approved_ticket_travel_accommodation_fails_with_no_amount(
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)


def test_handle_grant_approved_ticket_fails_with_no_visa_application_form_link(
conference_factory, grant_factory, settings
):
settings.FRONTEND_URL = "https://pycon.it"

conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
)
user = UserFactory()
grant = grant_factory(
conference=conference,
approved_type=Grant.ApprovedType.ticket_travel_accommodation,
applicant_reply_deadline=datetime(2023, 2, 1, 23, 59, tzinfo=timezone.utc),
travel_amount=0,
user=user,
)

with pytest.raises(
ValueError,
match="Visa Application Form Link Missing: Please ensure the link to the Visa "
"Application Form is set in the Conference admin settings.",
):
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)


def test_handle_grant_approved_ticket_only_reply_sent(
conference_factory, grant_factory, settings
):
Expand All @@ -236,6 +271,7 @@ def test_handle_grant_approved_ticket_only_reply_sent(
conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
visa_application_form_link="https://example.com/visa-application-form",
)
user = UserFactory(
full_name="Marco Acierno",
Expand All @@ -256,7 +292,7 @@ def test_handle_grant_approved_ticket_only_reply_sent(
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)

email_mock.assert_called_once_with(
template=EmailTemplate.GRANT_APPROVED_TICKET_ONLY,
template=EmailTemplate.GRANT_APPROVED,
to="marco@placeholder.it",
subject=f"[{grant.conference.name}] Financial Aid Update",
variables={
Expand All @@ -267,6 +303,9 @@ def test_handle_grant_approved_ticket_only_reply_sent(
"deadlineDateTime": "1 February 2023 23:59 UTC",
"deadlineDate": "1 February 2023",
"replyLink": "https://pycon.it/grants/reply/",
"visaApplicationFormLink": "https://example.com/visa-application-form",
"hasApprovedTravel": False,
"hasApprovedAccommodation": False,
},
reply_to=["grants@pycon.it"],
)
Expand All @@ -280,6 +319,7 @@ def test_handle_grant_approved_travel_reply_sent(
conference = conference_factory(
start=datetime(2023, 5, 2, tzinfo=timezone.utc),
end=datetime(2023, 5, 5, tzinfo=timezone.utc),
visa_application_form_link="https://example.com/visa-application-form",
)
user = UserFactory(
full_name="Marco Acierno",
Expand All @@ -301,7 +341,7 @@ def test_handle_grant_approved_travel_reply_sent(
send_grant_reply_approved_email(grant_id=grant.id, is_reminder=False)

email_mock.assert_called_once_with(
template=EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL,
template=EmailTemplate.GRANT_APPROVED,
to="marco@placeholder.it",
subject=f"[{grant.conference.name}] Financial Aid Update",
variables={
Expand All @@ -312,14 +352,24 @@ def test_handle_grant_approved_travel_reply_sent(
"deadlineDateTime": "1 February 2023 23:59 UTC",
"deadlineDate": "1 February 2023",
"replyLink": "https://pycon.it/grants/reply/",
"visaApplicationFormLink": "https://example.com/visa-application-form",
"hasApprovedTravel": True,
"hasApprovedAccommodation": False,
"amount": "400",
},
reply_to=["grants@pycon.it"],
)


def test_send_grant_reply_waiting_list_update_email(sent_emails):
grant = GrantFactory()
def test_send_grant_reply_waiting_list_update_email(settings):
settings.FRONTEND_URL = "https://pycon.it"
user = UserFactory(
full_name="Marco Acierno",
email="marco@placeholder.it",
name="Marco",
username="marco",
)
grant = GrantFactory(user=user)
DeadlineFactory(
conference=grant.conference,
start=datetime(2023, 3, 1, 23, 59, tzinfo=timezone.utc),
Expand All @@ -329,14 +379,25 @@ def test_send_grant_reply_waiting_list_update_email(sent_emails):
"it": "Update Grants in Waiting List",
},
)
conference_name = grant.conference.name.localize("en")

send_grant_reply_waiting_list_update_email(
grant_id=grant.id,
)
with patch("grants.tasks.send_email") as email_mock:
send_grant_reply_waiting_list_update_email(
grant_id=grant.id,
)

assert len(sent_emails) == 1
assert sent_emails[0]["template"] == EmailTemplate.GRANT_WAITING_LIST_UPDATE
assert sent_emails[0]["to"] == grant.user.email
email_mock.assert_called_once_with(
template=EmailTemplate.GRANT_WAITING_LIST_UPDATE,
to="marco@placeholder.it",
variables={
"firstname": "Marco Acierno",
"conferenceName": conference_name,
"grantsUpdateDeadline": "1 March 2023",
"replyLink": "https://pycon.it/grants/reply/",
},
reply_to=["grants@pycon.it"],
subject=f"[{conference_name}] Financial Aid Update",
)


@override_settings(PLAIN_API=None)
Expand Down
8 changes: 4 additions & 4 deletions backend/pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ dependencies = [
"jsonschema<4.0.0,>=3.2.0",
"strawberry-graphql==0.211.1",
"Werkzeug<2.0.0,>=1.0.1",
"pythonit-toolkit==0.1.92",
"pythonit-toolkit==0.1.93-dev.1707308970",
"django-import-export<4.0.0,>=3.2.0",
"azure-identity<2.0.0,>=1.12.0",
"azure-storage-blob<13.0.0,>=12.14.1",
Expand Down
Loading
Loading