Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grants: email copy updates #3712

Merged
merged 17 commits into from
Feb 8, 2024
1 change: 1 addition & 0 deletions backend/api/conferences/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class Conference:
map: Optional[Map] = strawberry.field(resolver=resolve_map)

pretix_event_url: str
visa_application_form_link: str

@strawberry.field
def voucher(self, info, code: str) -> Optional[Voucher]:
Expand Down
2 changes: 1 addition & 1 deletion backend/api/grants/tests/test_send_grant_reply.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_status_is_not_updated_when_the_reply_is_need_info(
assert grant.status == Grant.Status.waiting_for_confirmation


def test_status_is_updated_when_reply_is_confrimed(graphql_client, user, grant_factory):
def test_status_is_updated_when_reply_is_confirmed(graphql_client, user, grant_factory):
graphql_client.force_login(user)
grant = grant_factory(user_id=user.id, status=Grant.Status.waiting_for_confirmation)

Expand Down
1 change: 1 addition & 0 deletions backend/conferences/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class ConferenceAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin):
"topics",
"audience_levels",
"languages",
"visa_application_form_link",
)
},
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.7 on 2024-02-01 11:51

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("conferences", "0039_conference_slack_new_sponsor_lead_incoming_webhook_url"),
]

operations = [
migrations.AddField(
model_name="conference",
name="visa_application_form_link",
field=models.URLField(
blank=True, default="", verbose_name="Visa application form link"
),
),
]
4 changes: 4 additions & 0 deletions backend/conferences/models/conference.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel):
default=None,
)

visa_application_form_link = models.URLField(
_("Visa application form link"), blank=True, default=""
)

youtube_video_bottom_text = models.TextField(
default="",
blank=True,
Expand Down
1 change: 1 addition & 0 deletions backend/conferences/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ConferenceFactory(DjangoModelFactory):

pretix_organizer_id = "base-pretix-organizer-id"
pretix_event_id = "base-pretix-event-id"
visa_application_form_link = factory.Faker("url")

@classmethod
def _create(cls, model_class, *args, **kwargs):
Expand Down
82 changes: 46 additions & 36 deletions backend/grants/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .models import Grant
from django.db.models import Exists, OuterRef

from functools import wraps
from django.contrib.admin import SimpleListFilter

EXPORT_GRANTS_FIELDS = (
Expand Down Expand Up @@ -158,39 +159,58 @@ def _check_amounts_are_not_empty(grant: Grant, request):
request,
f"Grant for {grant.name} is missing 'Total Amount'!",
)
return
return False

if (
grant.grant_type
in (
Grant.ApprovedType.ticket_accommodation,
Grant.ApprovedType.ticket_travel_accommodation,
)
and grant.accommodation_amount is None
):
if grant.has_approved_accommodation() and grant.accommodation_amount is None:
Copy link
Member

Choose a reason for hiding this comment

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

not for this PR, but we have a very similar logic in the Grant save method when it calculates the amounts to set. We could refactor it to use those 2 new methods

messages.error(
request,
f"Grant for {grant.name} is missing 'Accommodation Amount'!",
)
return
return False

if (
grant.grant_type
in (
Grant.ApprovedType.ticket_travel,
Grant.ApprovedType.ticket_travel_accommodation,
)
and grant.travel_amount is None
):
if grant.has_approved_travel() and grant.travel_amount is None:
messages.error(
request,
f"Grant for {grant.name} is missing 'Travel Amount'!",
)
return
return False

return True


def validate_single_conference_selection(func):
"""
Ensure all selected grants in the queryset belong to the same conference.
"""

@wraps(func)
def wrapper(modeladmin, request, queryset):
is_filtered_by_conference = (
queryset.values_list("conference_id").distinct().count() == 1
)

if not is_filtered_by_conference:
messages.error(request, "Please select only one conference")
return

return func(modeladmin, request, queryset)

return wrapper


@admin.action(description="Send Approved/Waiting List/Rejected reply emails")
@validate_single_conference_selection
def send_reply_emails(modeladmin, request, queryset):
conference = queryset.first().conference

if not conference.visa_application_form_link:
messages.error(
request,
"Visa Application Form Link Missing: Please ensure the link to the Visa "
"Application Form is set in the Conference admin settings.",
)
return

queryset = queryset.filter(
status__in=(
Grant.Status.approved,
Expand All @@ -207,6 +227,7 @@ def send_reply_emails(modeladmin, request, queryset):
return

for grant in queryset:

if grant.status in (Grant.Status.approved,):
if grant.approved_type is None:
messages.error(
Expand All @@ -215,7 +236,8 @@ def send_reply_emails(modeladmin, request, queryset):
)
return

_check_amounts_are_not_empty(grant, request)
if not _check_amounts_are_not_empty(grant, request):
return

now = timezone.now()
grant.applicant_reply_deadline = timezone.datetime(
Expand All @@ -239,6 +261,7 @@ def send_reply_emails(modeladmin, request, queryset):


@admin.action(description="Send reminder to waiting confirmation grants")
@validate_single_conference_selection
def send_grant_reminder_to_waiting_for_confirmation(modeladmin, request, queryset):
queryset = queryset.filter(
status__in=(Grant.Status.waiting_for_confirmation,),
Expand All @@ -261,6 +284,7 @@ def send_grant_reminder_to_waiting_for_confirmation(modeladmin, request, queryse


@admin.action(description="Send Waiting List update email")
@validate_single_conference_selection
def send_reply_email_waiting_list_update(modeladmin, request, queryset):
queryset = queryset.filter(
status__in=(
Expand All @@ -275,15 +299,8 @@ def send_reply_email_waiting_list_update(modeladmin, request, queryset):


@admin.action(description="Send voucher via email")
@validate_single_conference_selection
def send_voucher_via_email(modeladmin, request, queryset):
is_filtered_by_conference = (
queryset.values_list("conference_id").distinct().count() == 1
)

if not is_filtered_by_conference:
messages.error(request, "Please select only one conference")
return

count = 0
for grant in queryset.filter(pretix_voucher_id__isnull=False):
send_grant_voucher_email.delay(grant_id=grant.id)
Expand All @@ -299,15 +316,8 @@ def _generate_voucher_code(prefix: str) -> str:


@admin.action(description="Create grant vouchers on Pretix")
@validate_single_conference_selection
def create_grant_vouchers_on_pretix(modeladmin, request, queryset):
is_filtered_by_conference = (
queryset.values_list("conference_id").distinct().count() == 1
)

if not is_filtered_by_conference:
messages.error(request, "Please select only one conference")
return

conference = queryset.first().conference

if not conference.pretix_speaker_voucher_quota_id:
Expand Down
12 changes: 12 additions & 0 deletions backend/grants/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,15 @@ def get_admin_url(self):
"admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name),
args=(self.pk,),
)

def has_approved_travel(self):
return (
self.approved_type == Grant.ApprovedType.ticket_travel_accommodation
or self.approved_type == Grant.ApprovedType.ticket_travel
)

def has_approved_accommodation(self):
return (
self.approved_type == Grant.ApprovedType.ticket_accommodation
or self.approved_type == Grant.ApprovedType.ticket_travel_accommodation
)
30 changes: 12 additions & 18 deletions backend/grants/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,31 @@ def send_grant_reply_approved_email(*, grant_id, is_reminder):
"Reminder: Financial Aid Update" if is_reminder else "Financial Aid Update"
)

template = None
if not grant.conference.visa_application_form_link:
raise ValueError(
"Visa Application Form Link Missing: Please ensure the link to the Visa "
"Application Form is set in the Conference admin settings."
)

template = EmailTemplate.GRANT_APPROVED
variables = {
"replyLink": reply_url,
"startDate": f"{grant.conference.start:%-d %B}",
"endDate": f"{grant.conference.end+timedelta(days=1):%-d %B}",
"deadlineDateTime": f"{grant.applicant_reply_deadline:%-d %B %Y %H:%M %Z}",
"deadlineDate": f"{grant.applicant_reply_deadline:%-d %B %Y}",
"visaApplicationFormLink": grant.conference.visa_application_form_link,
"hasApprovedTravel": grant.has_approved_travel(),
"hasApprovedAccommodation": grant.has_approved_accommodation(),
}

if grant.approved_type == Grant.ApprovedType.ticket_only:
template = EmailTemplate.GRANT_APPROVED_TICKET_ONLY
elif grant.approved_type == Grant.ApprovedType.ticket_travel:
template = EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL
if grant.travel_amount == 0:
raise ValueError(
"Grant travel amount is set to Zero, can't send the email!"
)

variables["amount"] = f"{grant.travel_amount:.0f}"
elif grant.approved_type == Grant.ApprovedType.ticket_accommodation:
template = EmailTemplate.GRANT_APPROVED_TICKET_ACCOMMODATION
elif grant.approved_type == Grant.ApprovedType.ticket_travel_accommodation:
template = EmailTemplate.GRANT_APPROVED_TICKET_TRAVEL_ACCOMMODATION
if grant.travel_amount == 0:
if grant.has_approved_travel():
if not grant.travel_amount:
raise ValueError(
"Grant travel amount is set to Zero, can't send the email!"
)

variables["amount"] = f"{grant.travel_amount:.0f}"
else:
raise ValueError(f"Grant Approved type `{grant.approved_type}` not valid.")

_send_grant_email(template=template, subject=subject, grant=grant, **variables)

Expand Down
Loading
Loading