Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="[email protected]",
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 = (
Copy link
Member

Choose a reason for hiding this comment

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

Do you think this name is clear enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

any better ideas?

Copy link
Member

@marcoacierno marcoacierno Nov 30, 2024

Choose a reason for hiding this comment

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

can't think of a better name, "application" is what threw me off a bit but it is not a big deal

"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