Skip to content
Snippets Groups Projects
Verified Commit 17cbacd5 authored by Andreas Ellewsen's avatar Andreas Ellewsen
Browse files

Notify sponsors about ending roles

The setup has five parts:
- a new email template type
- a new django-q task for notifying
- a management command for scheduling the task periodically
- a new settings variable NOTIFIER_LIMIT for controlling the number of
  days into the future the notifier script should use for end date of
  roles.
- a new settings variable NOTIFIER_SCHEDULE_TYPE for controlling how
  often the notifier task should be scheduled when using the management
  command.

Resolves: GREG-162
parent df3607c0
No related branches found
No related tags found
1 merge request!227Notify sponsors about ending roles
Pipeline #108421 passed
"""
Command for scheduling the django-q task for notifying sponsors of
expiring roles.
Instead of having a task that gets scheduled, we could have had a
management command that was run as a cronjob. However, that would mean
another cronjob
"""
import logging
import logging.config
from django.conf import settings
from django.core.management.base import BaseCommand
from django_q.tasks import schedule
logging.config.dictConfig(settings.LOGGING)
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Schedule notification of sponsors"
def handle(self, *args, **options):
logger.info("Scheduling role end notifier task...")
schedule(
func="tasks.notify_sponsors_roles_ending",
schedule_type=settings.NOTIFIER_SCHEDULE_TYPE,
)
logger.info("Role end notifier task scheduled.")
"""
Collection of tasks designed to be run by django-q
"""
import datetime
from collections import defaultdict
from django.conf import settings
from greg.models import Role, Sponsor
from gregui import mailutils
def notify_sponsors_roles_ending() -> None:
"""
This task notifies sponsors of roles (that are accessible to them)
that are about to expire.
It is meant to be run at set intervals. Typically every week or
two. The script will notify of all roles ending between today and
the number of days set in the NOTIFIER_LIMIT setting.
While it is sensible to set the run interval to the same value as
the NOTIFIER_LIMIT, it is not mandatory, as there is a use case for
setting the run interval to e.g 1 week and the limit to 2. This
would ensure that the sponsor gets 2 notifications about the same
role ending. (Be careful not to swap them though, as that would
ensure some roles ending without a notification being sent to the
sponsor).
"""
# Map ending roles to units
today = datetime.date.today()
ending_roles = Role.objects.filter(
end_date__gte=today,
end_date__lte=today + datetime.timedelta(days=settings.NOTIFIER_LIMIT),
)
unit2roles = defaultdict(list)
for role in ending_roles:
unit2roles[role.orgunit.id].append(role)
# Map sponsors with email to units
sponsors = Sponsor.objects.filter(work_email__isnull=False)
sp2unit = {s.id: list(s.get_allowed_units()) for s in sponsors}
# Map sponsors to ending roles
sp2roles = defaultdict(list)
for sp, units in sp2unit.items():
for unit in units:
for role in unit2roles[unit.id]:
sp2roles[sp].append(role)
# Send emails to sponsors
for sp, roles in sp2roles.items():
mailutils.send_role_ending_mail(
mail_to=sponsors.get(id=sp).work_email, # type: ignore
num_roles=len(roles),
)
import datetime
import logging import logging
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
...@@ -115,7 +116,10 @@ def person_bar() -> Person: ...@@ -115,7 +116,10 @@ def person_bar() -> Person:
@pytest.fixture @pytest.fixture
def sponsor_guy() -> Sponsor: def sponsor_guy() -> Sponsor:
sp = Sponsor.objects.create( sp = Sponsor.objects.create(
feide_id="guy@example.org", first_name="Sponsor", last_name="Guy" feide_id="guy@example.org",
first_name="Sponsor",
last_name="Guy",
work_email="sponsor_guy@example.com",
) )
return Sponsor.objects.get(id=sp.id) return Sponsor.objects.get(id=sp.id)
...@@ -183,6 +187,24 @@ def role_person_foo( ...@@ -183,6 +187,24 @@ def role_person_foo(
return Role.objects.get(id=role.id) return Role.objects.get(id=role.id)
@pytest.fixture
def role_person_foo2(
person_foo: Person,
role_type_test_guest: RoleType,
sponsor_guy: Sponsor,
unit_foo: OrganizationalUnit,
) -> Role:
role = Role.objects.create(
person=person_foo,
type=role_type_test_guest,
start_date=datetime.date.today(),
end_date=datetime.date.today() + datetime.timedelta(days=5),
sponsor=sponsor_guy,
orgunit=unit_foo,
)
return Role.objects.get(id=role.id)
@pytest.fixture @pytest.fixture
def role_type_foo() -> RoleType: def role_type_foo() -> RoleType:
rt = RoleType.objects.create(identifier="role_foo", name_en="Role Foo") rt = RoleType.objects.create(identifier="role_foo", name_en="Role Foo")
......
import pytest
from django.core import mail
from greg.tasks import notify_sponsors_roles_ending
from gregui.models import EmailTemplate
@pytest.fixture
def role_end_reminder_template():
et = EmailTemplate.objects.create(
template_key=EmailTemplate.EmailType.ROLE_END_REMINDER,
subject="role end reminder",
body="""Dette er en automatisk generert melding fra gjesteregistreringstjenesten.
Du administrerer {{ num_roles }} gjesteroller som snart utløper.
Dersom det er ønskelig å forlenge rollene kan dette gjøres i webgrensesnittet.
This message has been automatically generated by the guest registration system.
You can maintan {{ num_roles }} guest roles about to expire.
To extend the guest role please use the web interface.""",
)
return EmailTemplate.objects.get(id=et.id)
@pytest.mark.django_db
def test_notify_sponsors_roles_ending(
role_end_reminder_template, role_person_foo2, sponsor_org_unit
):
mail.outbox = []
notify_sponsors_roles_ending()
assert len(mail.outbox) == 1
assert mail.outbox[0].to == ["sponsor_guy@example.com"]
assert (
mail.outbox[0].body
== """Dette er en automatisk generert melding fra gjesteregistreringstjenesten.
Du administrerer 1 gjesteroller som snart utløper.
Dersom det er ønskelig å forlenge rollene kan dette gjøres i webgrensesnittet.
This message has been automatically generated by the guest registration system.
You can maintan 1 guest roles about to expire.
To extend the guest role please use the web interface."""
)
...@@ -316,3 +316,10 @@ Q_CLUSTER = { ...@@ -316,3 +316,10 @@ Q_CLUSTER = {
"bulk": 10, "bulk": 10,
"orm": "default", "orm": "default",
} }
# Number of days into the future to notify about ending roles. Used by the task that
# notifies sponsors about ending roles.
NOTIFIER_LIMIT = 7
# Default schedule for the task notifying about ending roles. Used by the management
# command that triggers the task.
NOTIFIER_SCHEDULE_TYPE = "W"
...@@ -60,6 +60,14 @@ def confirmation_template(guest: str, mail_to: str) -> dict[str, Union[str, list ...@@ -60,6 +60,14 @@ def confirmation_template(guest: str, mail_to: str) -> dict[str, Union[str, list
return prepare_arguments(template, context, mail_to) return prepare_arguments(template, context, mail_to)
def reminder_template(mail_to: str, num_roles: int) -> dict[str, Union[str, list[str]]]:
template = EmailTemplate.objects.get(
template_key=EmailTemplate.EmailType.ROLE_END_REMINDER
)
context = Context({"num_roles": num_roles})
return prepare_arguments(template, context, mail_to)
def send_registration_mail(mail_to: str, sponsor: str) -> str: def send_registration_mail(mail_to: str, sponsor: str) -> str:
arguments = registration_template(settings.INSTANCE_NAME, sponsor, mail_to) arguments = registration_template(settings.INSTANCE_NAME, sponsor, mail_to)
return async_task("django.core.mail.send_mail", **arguments) return async_task("django.core.mail.send_mail", **arguments)
...@@ -70,6 +78,11 @@ def send_confirmation_mail(mail_to: str, guest: str) -> str: ...@@ -70,6 +78,11 @@ def send_confirmation_mail(mail_to: str, guest: str) -> str:
return async_task("django.core.mail.send_mail", **arguments) return async_task("django.core.mail.send_mail", **arguments)
def send_role_ending_mail(mail_to: str, num_roles: int) -> str:
arguments = reminder_template(mail_to, num_roles)
return async_task("django.core.mail.send_mail", **arguments)
def send_invite_mail(link: InvitationLink) -> Optional[str]: def send_invite_mail(link: InvitationLink) -> Optional[str]:
email_address = link.invitation.role.person.private_email email_address = link.invitation.role.person.private_email
if not email_address: if not email_address:
......
# Generated by Django 4.0 on 2022-01-04 09:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gregui", "0002_emailtemplate"),
]
operations = [
migrations.AlterField(
model_name="emailtemplate",
name="template_key",
field=models.CharField(
choices=[
("guest_registration", "Guest Registration"),
("sponsor_confirmation", "Sponsor Confirmation"),
("role_end_reminder", "Role End Reminder"),
],
max_length=64,
unique=True,
),
),
]
...@@ -33,6 +33,10 @@ class EmailTemplate(BaseModel): ...@@ -33,6 +33,10 @@ class EmailTemplate(BaseModel):
Only one template of each type is allowed. To introduce new ones, simply add a new Only one template of each type is allowed. To introduce new ones, simply add a new
EmailType. EmailType.
GUEST_REGISTRATION is for informing a guest that they have been invited
SPONSOR_CONFIRMATION is for informing the sponsor they must verify the guest's information
ROLE_END_REMINDER is used when reminding the sponsor if their ending roles in the near future
""" """
class EmailType(models.TextChoices): class EmailType(models.TextChoices):
...@@ -40,6 +44,7 @@ class EmailTemplate(BaseModel): ...@@ -40,6 +44,7 @@ class EmailTemplate(BaseModel):
GUEST_REGISTRATION = "guest_registration" GUEST_REGISTRATION = "guest_registration"
SPONSOR_CONFIRMATION = "sponsor_confirmation" SPONSOR_CONFIRMATION = "sponsor_confirmation"
ROLE_END_REMINDER = "role_end_reminder"
template_key = models.CharField( template_key = models.CharField(
max_length=64, choices=EmailType.choices, unique=True max_length=64, choices=EmailType.choices, unique=True
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment