Skip to content

Commit 441477b

Browse files
authored
Grants fix bugs email task (#4205)
1 parent 4b97241 commit 441477b

File tree

5 files changed

+141
-110
lines changed

5 files changed

+141
-110
lines changed

backend/api/grants/mutations.py

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
from conferences.models.conference import Conference
1919
from grants.tasks import (
2020
notify_new_grant_reply_slack,
21-
send_grant_application_confirmation_email,
2221
)
2322
from grants.models import Grant as GrantModel
2423
from users.models import User
24+
from grants.tasks import get_name
25+
from notifications.models import EmailTemplate, EmailTemplateIdentifier
2526

2627

2728
@strawberry.type
@@ -212,6 +213,7 @@ class SendGrantReplyError:
212213
@strawberry.type
213214
class GrantMutation:
214215
@strawberry.mutation(permission_classes=[IsAuthenticated])
216+
@transaction.atomic
215217
def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
216218
request = info.context.request
217219

@@ -220,52 +222,60 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
220222
if errors := input.validate(conference=conference, user=request.user):
221223
return errors
222224

223-
with transaction.atomic():
224-
instance = GrantModel.objects.create(
225-
**{
226-
"user_id": request.user.id,
227-
"conference": conference,
228-
"name": input.name,
229-
"full_name": input.full_name,
230-
"age_group": input.age_group,
231-
"gender": input.gender,
232-
"occupation": input.occupation,
233-
"grant_type": input.grant_type,
234-
"python_usage": input.python_usage,
235-
"been_to_other_events": input.been_to_other_events,
236-
"community_contribution": input.community_contribution,
237-
"needs_funds_for_travel": input.needs_funds_for_travel,
238-
"need_visa": input.need_visa,
239-
"need_accommodation": input.need_accommodation,
240-
"why": input.why,
241-
"notes": input.notes,
242-
"departure_country": input.departure_country,
243-
"nationality": input.nationality,
244-
"departure_city": input.departure_city,
245-
}
246-
)
225+
instance = GrantModel.objects.create(
226+
**{
227+
"user_id": request.user.id,
228+
"conference": conference,
229+
"name": input.name,
230+
"full_name": input.full_name,
231+
"age_group": input.age_group,
232+
"gender": input.gender,
233+
"occupation": input.occupation,
234+
"grant_type": input.grant_type,
235+
"python_usage": input.python_usage,
236+
"been_to_other_events": input.been_to_other_events,
237+
"community_contribution": input.community_contribution,
238+
"needs_funds_for_travel": input.needs_funds_for_travel,
239+
"need_visa": input.need_visa,
240+
"need_accommodation": input.need_accommodation,
241+
"why": input.why,
242+
"notes": input.notes,
243+
"departure_country": input.departure_country,
244+
"nationality": input.nationality,
245+
"departure_city": input.departure_city,
246+
}
247+
)
247248

248-
record_privacy_policy_acceptance(
249-
info.context.request,
250-
conference,
251-
"grant",
252-
)
249+
record_privacy_policy_acceptance(
250+
info.context.request,
251+
conference,
252+
"grant",
253+
)
253254

254-
Participant.objects.update_or_create(
255-
user_id=request.user.id,
256-
conference=instance.conference,
257-
defaults={
258-
"bio": input.participant_bio,
259-
"website": input.participant_website,
260-
"twitter_handle": input.participant_twitter_handle,
261-
"instagram_handle": input.participant_instagram_handle,
262-
"linkedin_url": input.participant_linkedin_url,
263-
"facebook_url": input.participant_facebook_url,
264-
"mastodon_handle": input.participant_mastodon_handle,
265-
},
266-
)
255+
Participant.objects.update_or_create(
256+
user_id=request.user.id,
257+
conference=instance.conference,
258+
defaults={
259+
"bio": input.participant_bio,
260+
"website": input.participant_website,
261+
"twitter_handle": input.participant_twitter_handle,
262+
"instagram_handle": input.participant_instagram_handle,
263+
"linkedin_url": input.participant_linkedin_url,
264+
"facebook_url": input.participant_facebook_url,
265+
"mastodon_handle": input.participant_mastodon_handle,
266+
},
267+
)
268+
269+
email_template = EmailTemplate.objects.for_conference(
270+
conference
271+
).get_by_identifier(EmailTemplateIdentifier.grant_application_confirmation)
267272

268-
send_grant_application_confirmation_email.delay(grant_id=instance.id)
273+
email_template.send_email(
274+
recipient=request.user,
275+
placeholders={
276+
"user_name": get_name(request.user, "there"),
277+
},
278+
)
269279

270280
# hack because we return django models
271281
instance.__strawberry_definition__ = Grant.__strawberry_definition__

backend/api/grants/tests/test_send_grant.py

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import pytest
55
from participants.models import Participant
66
from grants.models import Grant
7+
from notifications.models import EmailTemplateIdentifier
8+
from notifications.tests.factories import EmailTemplateFactory
79

8-
from unittest.mock import call
910

1011
pytestmark = pytest.mark.django_db
1112

@@ -90,32 +91,43 @@ def _send_grant(client, conference, conference_code=None, **kwargs):
9091
return response
9192

9293

93-
def test_send_grant(graphql_client, user, mocker):
94-
mock_confirmation_email = mocker.patch(
95-
"api.grants.mutations.send_grant_application_confirmation_email"
96-
)
94+
def test_send_grant(graphql_client, user, mocker, django_capture_on_commit_callbacks):
95+
mock_email_template = mocker.patch("api.grants.mutations.EmailTemplate")
9796
graphql_client.force_login(user)
9897
conference = ConferenceFactory(active_grants=True)
98+
EmailTemplateFactory(
99+
conference=conference,
100+
identifier=EmailTemplateIdentifier.grant_application_confirmation,
101+
)
99102

100-
response = _send_grant(graphql_client, conference)
103+
with django_capture_on_commit_callbacks(execute=True):
104+
response = _send_grant(graphql_client, conference)
101105

106+
# The API response is successful
102107
assert response["data"]["sendGrant"]["__typename"] == "Grant"
103108
assert response["data"]["sendGrant"]["id"]
104109

110+
# A participant is created
105111
participant = Participant.objects.get(conference=conference, user_id=user.id)
106112
assert participant.bio == "my bio"
113+
114+
# A grant object is created
107115
grant = Grant.objects.get(id=response["data"]["sendGrant"]["id"])
108116
assert grant.conference == conference
109117
assert PrivacyPolicyAcceptanceRecord.objects.filter(
110118
user=user, conference=conference, privacy_policy="grant"
111119
).exists()
112-
mock_confirmation_email.delay.assert_called_once_with(grant_id=grant.id)
120+
121+
# An email is sent to the user
122+
mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with(
123+
recipient=user,
124+
placeholders={
125+
"user_name": user.full_name,
126+
},
127+
)
113128

114129

115130
def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user, mocker):
116-
mock_confirmation_email = mocker.patch(
117-
"api.grants.mutations.send_grant_application_confirmation_email"
118-
)
119131
graphql_client.force_login(user)
120132
conference = ConferenceFactory(active_grants=False)
121133

@@ -126,7 +138,6 @@ def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user, mocker):
126138
assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [
127139
"The grants form is not open!"
128140
]
129-
mock_confirmation_email.delay.assert_not_called()
130141

131142

132143
def test_cannot_send_a_grant_if_grants_deadline_do_not_exists(graphql_client, user):
@@ -152,11 +163,12 @@ def test_cannot_send_a_grant_as_unlogged_user(graphql_client):
152163

153164

154165
def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user, mocker):
155-
mock_confirmation_email = mocker.patch(
156-
"api.grants.mutations.send_grant_application_confirmation_email"
157-
)
158166
graphql_client.force_login(user)
159167
conference = ConferenceFactory(active_grants=True)
168+
EmailTemplateFactory(
169+
conference=conference,
170+
identifier=EmailTemplateIdentifier.grant_application_confirmation,
171+
)
160172
_send_grant(graphql_client, conference)
161173

162174
response = _send_grant(graphql_client, conference)
@@ -166,29 +178,30 @@ def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user, moc
166178
assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [
167179
"Grant already submitted!"
168180
]
169-
mock_confirmation_email.delay.assert_called_once()
170181

171182

172-
def test_can_send_two_grants_to_different_conferences(graphql_client, user, mocker):
173-
mock_confirmation_email = mocker.patch(
174-
"api.grants.mutations.send_grant_application_confirmation_email"
175-
)
183+
def test_can_send_two_grants_to_different_conferences(
184+
graphql_client, user, mocker, django_capture_on_commit_callbacks
185+
):
176186
graphql_client.force_login(user)
177187
conference = ConferenceFactory(active_grants=True)
178188
conference_2 = ConferenceFactory(active_grants=True)
189+
EmailTemplateFactory(
190+
conference=conference,
191+
identifier=EmailTemplateIdentifier.grant_application_confirmation,
192+
)
193+
EmailTemplateFactory(
194+
conference=conference_2,
195+
identifier=EmailTemplateIdentifier.grant_application_confirmation,
196+
)
179197
first_response = _send_grant(graphql_client, conference)
198+
assert not first_response.get("errors")
199+
assert first_response["data"]["sendGrant"]["__typename"] == "Grant"
180200

181201
second_response = _send_grant(graphql_client, conference_2)
182202

183203
assert not second_response.get("errors")
184204
assert second_response["data"]["sendGrant"]["__typename"] == "Grant"
185-
mock_confirmation_email.delay.assert_has_calls(
186-
[
187-
call(grant_id=int(first_response["data"]["sendGrant"]["id"])),
188-
call(grant_id=int(second_response["data"]["sendGrant"]["id"])),
189-
],
190-
any_order=True,
191-
)
192205

193206

194207
def test_invalid_conference(graphql_client, user):
@@ -260,3 +273,36 @@ def test_cannot_send_grant_with_empty_values(
260273
assert response["data"]["sendGrant"]["errors"]["validationFullName"] == [
261274
"full_name: Cannot be empty"
262275
]
276+
277+
278+
def test_submit_grant_with_existing_participant(graphql_client, user):
279+
graphql_client.force_login(user)
280+
conference = ConferenceFactory(
281+
active_grants=True,
282+
)
283+
EmailTemplateFactory(
284+
conference=conference,
285+
identifier=EmailTemplateIdentifier.grant_application_confirmation,
286+
)
287+
participant = Participant.objects.create(
288+
conference=conference, user_id=user.id, bio="old bio"
289+
)
290+
291+
response = _send_grant(
292+
graphql_client,
293+
conference,
294+
participantBio="my bio",
295+
participantWebsite="https://sushi.com",
296+
)
297+
298+
assert response["data"]["sendGrant"]["__typename"] == "Grant"
299+
assert response["data"]["sendGrant"]["id"]
300+
301+
grant = Grant.objects.get(id=response["data"]["sendGrant"]["id"])
302+
assert grant.status == Grant.Status.pending
303+
assert grant.conference == conference
304+
assert grant.user_id == user.id
305+
306+
participant.refresh_from_db()
307+
assert participant.bio == "my bio"
308+
assert participant.website == "https://sushi.com"

backend/grants/tasks.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,3 @@ 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: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
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,
1514
)
1615
from grants.models import Grant
1716

@@ -312,23 +311,3 @@ def test_send_grant_reply_waiting_list_update_email(settings):
312311
"reply_url": "https://pycon.it/grants/reply/",
313312
},
314313
)
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-
)

frontend/src/components/grant-form/index.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,11 @@ export const GrantForm = ({
490490
>
491491
<Select {...select("needVisa")} required={true}>
492492
<FormattedMessage id="global.selectOption">
493-
{(msg) => <option value="">{msg}</option>}
493+
{(msg) => (
494+
<option value="" disabled={true}>
495+
{msg}
496+
</option>
497+
)}
494498
</FormattedMessage>
495499
<FormattedMessage id="global.no">
496500
{(msg) => <option value="false">{msg}</option>}
@@ -512,7 +516,11 @@ export const GrantForm = ({
512516
>
513517
<Select {...select("needAccommodation")} required={true}>
514518
<FormattedMessage id="global.selectOption">
515-
{(msg) => <option value="">{msg}</option>}
519+
{(msg) => (
520+
<option value="" disabled={true}>
521+
{msg}
522+
</option>
523+
)}
516524
</FormattedMessage>
517525
<FormattedMessage id="global.no">
518526
{(msg) => <option value="false">{msg}</option>}
@@ -561,7 +569,11 @@ export const GrantForm = ({
561569
>
562570
<Select {...select("needsFundsForTravel")} required={true}>
563571
<FormattedMessage id="global.selectOption">
564-
{(msg) => <option value="">{msg}</option>}
572+
{(msg) => (
573+
<option value="" disabled={true}>
574+
{msg}
575+
</option>
576+
)}
565577
</FormattedMessage>
566578
<FormattedMessage id="global.no">
567579
{(msg) => <option value="false">{msg}</option>}

0 commit comments

Comments
 (0)