Skip to content

Commit 86af124

Browse files
authored
Add confirmation email for grant application (#4177)
1 parent ee45878 commit 86af124

File tree

6 files changed

+130
-12
lines changed

6 files changed

+130
-12
lines changed

backend/api/grants/mutations.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
from api.permissions import IsAuthenticated
1717
from api.types import BaseErrorType
1818
from conferences.models.conference import Conference
19-
from grants.tasks import notify_new_grant_reply_slack
19+
from grants.tasks import (
20+
notify_new_grant_reply_slack,
21+
send_grant_application_confirmation_email,
22+
)
2023
from grants.models import Grant as GrantModel
2124
from users.models import User
2225

@@ -254,6 +257,8 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
254257
},
255258
)
256259

260+
send_grant_application_confirmation_email.delay(grant_id=instance.id)
261+
257262
# hack because we return django models
258263
instance.__strawberry_definition__ = Grant.__strawberry_definition__
259264
return instance

backend/api/grants/tests/test_send_grant.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from grants.tests.factories import GrantFactory
44
import pytest
55
from participants.models import Participant
6+
from grants.models import Grant
67

8+
from unittest.mock import call
79

810
pytestmark = pytest.mark.django_db
911

@@ -84,7 +86,10 @@ def _send_grant(client, conference, conference_code=None, **kwargs):
8486
return response
8587

8688

87-
def test_send_grant(graphql_client, user):
89+
def test_send_grant(graphql_client, user, mocker):
90+
mock_confirmation_email = mocker.patch(
91+
"api.grants.mutations.send_grant_application_confirmation_email"
92+
)
8893
graphql_client.force_login(user)
8994
conference = ConferenceFactory(active_grants=True)
9095

@@ -95,13 +100,18 @@ def test_send_grant(graphql_client, user):
95100

96101
participant = Participant.objects.get(conference=conference, user_id=user.id)
97102
assert participant.bio == "my bio"
98-
103+
grant = Grant.objects.get(id=response["data"]["sendGrant"]["id"])
104+
assert grant.conference == conference
99105
assert PrivacyPolicyAcceptanceRecord.objects.filter(
100106
user=user, conference=conference, privacy_policy="grant"
101107
).exists()
108+
mock_confirmation_email.delay.assert_called_once_with(grant_id=grant.id)
102109

103110

104-
def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user):
111+
def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user, mocker):
112+
mock_confirmation_email = mocker.patch(
113+
"api.grants.mutations.send_grant_application_confirmation_email"
114+
)
105115
graphql_client.force_login(user)
106116
conference = ConferenceFactory(active_grants=False)
107117

@@ -112,6 +122,7 @@ def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user):
112122
assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [
113123
"The grants form is not open!"
114124
]
125+
mock_confirmation_email.delay.assert_not_called()
115126

116127

117128
def test_cannot_send_a_grant_if_grants_deadline_do_not_exists(graphql_client, user):
@@ -136,7 +147,10 @@ def test_cannot_send_a_grant_as_unlogged_user(graphql_client):
136147
assert resp["errors"][0]["message"] == "User not logged in"
137148

138149

139-
def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user):
150+
def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user, mocker):
151+
mock_confirmation_email = mocker.patch(
152+
"api.grants.mutations.send_grant_application_confirmation_email"
153+
)
140154
graphql_client.force_login(user)
141155
conference = ConferenceFactory(active_grants=True)
142156
_send_grant(graphql_client, conference)
@@ -148,18 +162,29 @@ def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user):
148162
assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [
149163
"Grant already submitted!"
150164
]
165+
mock_confirmation_email.delay.assert_called_once()
151166

152167

153-
def test_can_send_two_grants_to_different_conferences(graphql_client, user):
168+
def test_can_send_two_grants_to_different_conferences(graphql_client, user, mocker):
169+
mock_confirmation_email = mocker.patch(
170+
"api.grants.mutations.send_grant_application_confirmation_email"
171+
)
154172
graphql_client.force_login(user)
155173
conference = ConferenceFactory(active_grants=True)
156174
conference_2 = ConferenceFactory(active_grants=True)
157-
_send_grant(graphql_client, conference)
158-
159-
response = _send_grant(graphql_client, conference_2)
160-
161-
assert not response.get("errors")
162-
assert response["data"]["sendGrant"]["__typename"] == "Grant"
175+
first_response = _send_grant(graphql_client, conference)
176+
177+
second_response = _send_grant(graphql_client, conference_2)
178+
179+
assert not second_response.get("errors")
180+
assert second_response["data"]["sendGrant"]["__typename"] == "Grant"
181+
mock_confirmation_email.delay.assert_has_calls(
182+
[
183+
call(grant_id=int(first_response["data"]["sendGrant"]["id"])),
184+
call(grant_id=int(second_response["data"]["sendGrant"]["id"])),
185+
],
186+
any_order=True,
187+
)
163188

164189

165190
def test_invalid_conference(graphql_client, user):

backend/grants/tasks.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,19 @@ def _new_send_grant_email(
171171

172172
grant.applicant_reply_sent_at = timezone.now()
173173
grant.save()
174+
175+
176+
@app.task
177+
def send_grant_application_confirmation_email(*, grant_id):
178+
grant = Grant.objects.get(id=grant_id)
179+
email_template = EmailTemplate.objects.for_conference(
180+
grant.conference
181+
).get_by_identifier(EmailTemplateIdentifier.grant_application_confirmation)
182+
183+
email_template.send_email(
184+
recipient=grant.user,
185+
placeholders={
186+
"user_name": get_name(grant.user, "there"),
187+
},
188+
)
189+
logger.info("Grant application confirmation email sent for grant %s", grant.id)

backend/grants/tests/test_tasks.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
send_grant_reply_approved_email,
1212
send_grant_reply_rejected_email,
1313
send_grant_reply_waiting_list_email,
14+
send_grant_application_confirmation_email,
1415
)
1516
from grants.models import Grant
1617

@@ -311,3 +312,23 @@ def test_send_grant_reply_waiting_list_update_email(settings):
311312
"reply_url": "https://pycon.it/grants/reply/",
312313
},
313314
)
315+
316+
317+
def test_send_grant_application_confirmation_email():
318+
user = UserFactory(
319+
full_name="Marco Acierno",
320+
321+
name="Marco",
322+
username="marco",
323+
)
324+
grant = GrantFactory(user=user)
325+
326+
with patch("grants.tasks.EmailTemplate") as mock_email_template:
327+
send_grant_application_confirmation_email(grant_id=grant.id)
328+
329+
mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with(
330+
recipient=user,
331+
placeholders={
332+
"user_name": "Marco Acierno",
333+
},
334+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 5.1.1 on 2024-11-24 18:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("notifications", "0017_alter_emailtemplate_identifier"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="emailtemplate",
14+
name="identifier",
15+
field=models.CharField(
16+
choices=[
17+
("proposal_accepted", "Proposal accepted"),
18+
("proposal_rejected", "Proposal rejected"),
19+
("proposal_in_waiting_list", "Proposal in waiting list"),
20+
(
21+
"proposal_scheduled_time_changed",
22+
"Proposal scheduled time changed",
23+
),
24+
("speaker_communication", "Speaker communication"),
25+
("voucher_code", "Voucher code"),
26+
("reset_password", "[System] Reset password"),
27+
(
28+
"grant_application_confirmation",
29+
"Grant application confirmation",
30+
),
31+
("grant_approved", "Grant approved"),
32+
("grant_rejected", "Grant rejected"),
33+
("grant_waiting_list", "Grant waiting list"),
34+
("grant_waiting_list_update", "Grant waiting list update"),
35+
("grant_voucher_code", "Grant voucher code"),
36+
("sponsorship_brochure", "Sponsorship brochure"),
37+
("custom", "Custom"),
38+
],
39+
max_length=200,
40+
verbose_name="identifier",
41+
),
42+
),
43+
]

backend/notifications/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class EmailTemplateIdentifier(models.TextChoices):
2929

3030
reset_password = "reset_password", _("[System] Reset password")
3131

32+
grant_application_confirmation = (
33+
"grant_application_confirmation",
34+
_("Grant application confirmation"),
35+
)
3236
grant_approved = "grant_approved", _("Grant approved")
3337
grant_rejected = "grant_rejected", _("Grant rejected")
3438
grant_waiting_list = "grant_waiting_list", _("Grant waiting list")
@@ -80,6 +84,10 @@ class EmailTemplate(TimeStampedModel):
8084
"proposal_title",
8185
"invitation_url",
8286
],
87+
EmailTemplateIdentifier.grant_application_confirmation: [
88+
*BASE_PLACEHOLDERS,
89+
"user_name",
90+
],
8391
EmailTemplateIdentifier.grant_approved: [
8492
*BASE_PLACEHOLDERS,
8593
"reply_url",

0 commit comments

Comments
 (0)