From 7563a8c6bfb2e5c40d20aad68040240bced64083 Mon Sep 17 00:00:00 2001 From: Etty Date: Sun, 24 Nov 2024 18:59:05 +0000 Subject: [PATCH 1/2] Add confirmation email --- backend/api/grants/mutations.py | 7 ++- backend/grants/tasks.py | 14 ++++++ .../0018_alter_emailtemplate_identifier.py | 43 +++++++++++++++++++ backend/notifications/models.py | 8 ++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 backend/notifications/migrations/0018_alter_emailtemplate_identifier.py diff --git a/backend/api/grants/mutations.py b/backend/api/grants/mutations.py index 3802934cee..ee33b475e4 100644 --- a/backend/api/grants/mutations.py +++ b/backend/api/grants/mutations.py @@ -16,7 +16,10 @@ from api.permissions import IsAuthenticated from api.types import BaseErrorType from conferences.models.conference import Conference -from grants.tasks import notify_new_grant_reply_slack +from grants.tasks import ( + notify_new_grant_reply_slack, + send_grant_application_confirmation_email, +) from grants.models import Grant as GrantModel from users.models import User @@ -254,6 +257,8 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult: }, ) + send_grant_application_confirmation_email(instance) + # hack because we return django models instance.__strawberry_definition__ = Grant.__strawberry_definition__ return instance diff --git a/backend/grants/tasks.py b/backend/grants/tasks.py index db3b90f0c4..a6132625b3 100644 --- a/backend/grants/tasks.py +++ b/backend/grants/tasks.py @@ -171,3 +171,17 @@ def _new_send_grant_email( grant.applicant_reply_sent_at = timezone.now() grant.save() + + +def send_grant_application_confirmation_email(grant: Grant): + email_template = EmailTemplate.objects.for_conference( + grant.conference + ).get_by_identifier(EmailTemplateIdentifier.grant_application_confirmation) + + email_template.send_email( + recipient=grant.user, + placeholders={ + "user_name": get_name(grant.user, "there"), + }, + ) + logger.info("Grant application confirmation email sent for grant %s", grant.id) diff --git a/backend/notifications/migrations/0018_alter_emailtemplate_identifier.py b/backend/notifications/migrations/0018_alter_emailtemplate_identifier.py new file mode 100644 index 0000000000..75aea158ec --- /dev/null +++ b/backend/notifications/migrations/0018_alter_emailtemplate_identifier.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.1 on 2024-11-24 18:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("notifications", "0017_alter_emailtemplate_identifier"), + ] + + operations = [ + migrations.AlterField( + model_name="emailtemplate", + name="identifier", + field=models.CharField( + choices=[ + ("proposal_accepted", "Proposal accepted"), + ("proposal_rejected", "Proposal rejected"), + ("proposal_in_waiting_list", "Proposal in waiting list"), + ( + "proposal_scheduled_time_changed", + "Proposal scheduled time changed", + ), + ("speaker_communication", "Speaker communication"), + ("voucher_code", "Voucher code"), + ("reset_password", "[System] Reset password"), + ( + "grant_application_confirmation", + "Grant application confirmation", + ), + ("grant_approved", "Grant approved"), + ("grant_rejected", "Grant rejected"), + ("grant_waiting_list", "Grant waiting list"), + ("grant_waiting_list_update", "Grant waiting list update"), + ("grant_voucher_code", "Grant voucher code"), + ("sponsorship_brochure", "Sponsorship brochure"), + ("custom", "Custom"), + ], + max_length=200, + verbose_name="identifier", + ), + ), + ] diff --git a/backend/notifications/models.py b/backend/notifications/models.py index 13094284c0..7145183cfd 100644 --- a/backend/notifications/models.py +++ b/backend/notifications/models.py @@ -29,6 +29,10 @@ class EmailTemplateIdentifier(models.TextChoices): reset_password = "reset_password", _("[System] Reset password") + grant_application_confirmation = ( + "grant_application_confirmation", + _("Grant application confirmation"), + ) grant_approved = "grant_approved", _("Grant approved") grant_rejected = "grant_rejected", _("Grant rejected") grant_waiting_list = "grant_waiting_list", _("Grant waiting list") @@ -80,6 +84,10 @@ class EmailTemplate(TimeStampedModel): "proposal_title", "invitation_url", ], + EmailTemplateIdentifier.grant_application_confirmation: [ + *BASE_PLACEHOLDERS, + "user_name", + ], EmailTemplateIdentifier.grant_approved: [ *BASE_PLACEHOLDERS, "reply_url", From f162951151044dab3bcc45c258795c594969314f Mon Sep 17 00:00:00 2001 From: Etty Date: Sat, 30 Nov 2024 14:10:02 +0000 Subject: [PATCH 2/2] Add test --- backend/api/grants/mutations.py | 2 +- backend/api/grants/tests/test_send_grant.py | 47 ++++++++++++++++----- backend/grants/tasks.py | 4 +- backend/grants/tests/test_tasks.py | 21 +++++++++ 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/backend/api/grants/mutations.py b/backend/api/grants/mutations.py index ee33b475e4..509b02dce0 100644 --- a/backend/api/grants/mutations.py +++ b/backend/api/grants/mutations.py @@ -257,7 +257,7 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult: }, ) - send_grant_application_confirmation_email(instance) + send_grant_application_confirmation_email.delay(grant_id=instance.id) # hack because we return django models instance.__strawberry_definition__ = Grant.__strawberry_definition__ diff --git a/backend/api/grants/tests/test_send_grant.py b/backend/api/grants/tests/test_send_grant.py index 8e37b95fd6..b5ae245695 100644 --- a/backend/api/grants/tests/test_send_grant.py +++ b/backend/api/grants/tests/test_send_grant.py @@ -3,7 +3,9 @@ from grants.tests.factories import GrantFactory import pytest from participants.models import Participant +from grants.models import Grant +from unittest.mock import call pytestmark = pytest.mark.django_db @@ -84,7 +86,10 @@ def _send_grant(client, conference, conference_code=None, **kwargs): return response -def test_send_grant(graphql_client, user): +def test_send_grant(graphql_client, user, mocker): + mock_confirmation_email = mocker.patch( + "api.grants.mutations.send_grant_application_confirmation_email" + ) graphql_client.force_login(user) conference = ConferenceFactory(active_grants=True) @@ -95,13 +100,18 @@ def test_send_grant(graphql_client, user): participant = Participant.objects.get(conference=conference, user_id=user.id) assert participant.bio == "my bio" - + grant = Grant.objects.get(id=response["data"]["sendGrant"]["id"]) + assert grant.conference == conference assert PrivacyPolicyAcceptanceRecord.objects.filter( user=user, conference=conference, privacy_policy="grant" ).exists() + mock_confirmation_email.delay.assert_called_once_with(grant_id=grant.id) -def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user): +def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user, mocker): + mock_confirmation_email = mocker.patch( + "api.grants.mutations.send_grant_application_confirmation_email" + ) graphql_client.force_login(user) conference = ConferenceFactory(active_grants=False) @@ -112,6 +122,7 @@ def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user): assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [ "The grants form is not open!" ] + mock_confirmation_email.delay.assert_not_called() 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): assert resp["errors"][0]["message"] == "User not logged in" -def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user): +def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user, mocker): + mock_confirmation_email = mocker.patch( + "api.grants.mutations.send_grant_application_confirmation_email" + ) graphql_client.force_login(user) conference = ConferenceFactory(active_grants=True) _send_grant(graphql_client, conference) @@ -148,18 +162,29 @@ def test_cannot_send_two_grants_to_the_same_conference(graphql_client, user): assert response["data"]["sendGrant"]["errors"]["nonFieldErrors"] == [ "Grant already submitted!" ] + mock_confirmation_email.delay.assert_called_once() -def test_can_send_two_grants_to_different_conferences(graphql_client, user): +def test_can_send_two_grants_to_different_conferences(graphql_client, user, mocker): + mock_confirmation_email = mocker.patch( + "api.grants.mutations.send_grant_application_confirmation_email" + ) graphql_client.force_login(user) conference = ConferenceFactory(active_grants=True) conference_2 = ConferenceFactory(active_grants=True) - _send_grant(graphql_client, conference) - - response = _send_grant(graphql_client, conference_2) - - assert not response.get("errors") - assert response["data"]["sendGrant"]["__typename"] == "Grant" + first_response = _send_grant(graphql_client, conference) + + second_response = _send_grant(graphql_client, conference_2) + + assert not second_response.get("errors") + assert second_response["data"]["sendGrant"]["__typename"] == "Grant" + mock_confirmation_email.delay.assert_has_calls( + [ + call(grant_id=int(first_response["data"]["sendGrant"]["id"])), + call(grant_id=int(second_response["data"]["sendGrant"]["id"])), + ], + any_order=True, + ) def test_invalid_conference(graphql_client, user): diff --git a/backend/grants/tasks.py b/backend/grants/tasks.py index a6132625b3..4d45761f84 100644 --- a/backend/grants/tasks.py +++ b/backend/grants/tasks.py @@ -173,7 +173,9 @@ def _new_send_grant_email( grant.save() -def send_grant_application_confirmation_email(grant: Grant): +@app.task +def send_grant_application_confirmation_email(*, grant_id): + grant = Grant.objects.get(id=grant_id) email_template = EmailTemplate.objects.for_conference( grant.conference ).get_by_identifier(EmailTemplateIdentifier.grant_application_confirmation) diff --git a/backend/grants/tests/test_tasks.py b/backend/grants/tests/test_tasks.py index 8be25fef7f..d55348b071 100644 --- a/backend/grants/tests/test_tasks.py +++ b/backend/grants/tests/test_tasks.py @@ -11,6 +11,7 @@ send_grant_reply_approved_email, send_grant_reply_rejected_email, send_grant_reply_waiting_list_email, + send_grant_application_confirmation_email, ) from grants.models import Grant @@ -311,3 +312,23 @@ def test_send_grant_reply_waiting_list_update_email(settings): "reply_url": "https://pycon.it/grants/reply/", }, ) + + +def test_send_grant_application_confirmation_email(): + user = UserFactory( + full_name="Marco Acierno", + email="marco@placeholder.it", + name="Marco", + username="marco", + ) + grant = GrantFactory(user=user) + + with patch("grants.tasks.EmailTemplate") as mock_email_template: + send_grant_application_confirmation_email(grant_id=grant.id) + + mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with( + recipient=user, + placeholders={ + "user_name": "Marco Acierno", + }, + )