Skip to content

Commit

Permalink
Add confirmation email for grant application (#4177)
Browse files Browse the repository at this point in the history
  • Loading branch information
estyxx authored Dec 1, 2024
1 parent ee45878 commit 86af124
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 12 deletions.
7 changes: 6 additions & 1 deletion backend/api/grants/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -254,6 +257,8 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
},
)

send_grant_application_confirmation_email.delay(grant_id=instance.id)

# hack because we return django models
instance.__strawberry_definition__ = Grant.__strawberry_definition__
return instance
Expand Down
47 changes: 36 additions & 11 deletions backend/api/grants/tests/test_send_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions backend/grants/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,19 @@ def _new_send_grant_email(

grant.applicant_reply_sent_at = timezone.now()
grant.save()


@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)

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)
21 changes: 21 additions & 0 deletions backend/grants/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
},
)
Original file line number Diff line number Diff line change
@@ -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",
),
),
]
8 changes: 8 additions & 0 deletions backend/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 86af124

Please sign in to comment.