From d913c8049de3894087fee4ae4d1b24a67336ffbf Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sun, 6 Apr 2025 15:57:25 +0200 Subject: [PATCH] feat(members/waitinglist): add confirm link in invitation mail and more notifications --- .../jdav_web/settings/components/texts.py | 28 ++++++- jdav_web/mailer/mailutils.py | 4 + .../members/locale/de/LC_MESSAGES/django.po | 77 +++++++++++++++---- jdav_web/members/models.py | 45 +++++++++-- .../templates/members/confirm_invalid.html | 15 ++++ .../templates/members/confirm_invitation.html | 29 +++++++ .../templates/members/confirm_success.html | 23 ++++++ jdav_web/members/urls.py | 1 + jdav_web/members/views.py | 41 ++++++++++ 9 files changed, 244 insertions(+), 19 deletions(-) create mode 100644 jdav_web/members/templates/members/confirm_invalid.html create mode 100644 jdav_web/members/templates/members/confirm_invitation.html create mode 100644 jdav_web/members/templates/members/confirm_success.html diff --git a/jdav_web/jdav_web/settings/components/texts.py b/jdav_web/jdav_web/settings/components/texts.py index e5366b9..69743dc 100644 --- a/jdav_web/jdav_web/settings/components/texts.py +++ b/jdav_web/jdav_web/settings/components/texts.py @@ -40,6 +40,28 @@ aber weiterhin auf der Warteliste. Viele Grüße Dein KOMPASS""") + +GROUP_INVITATION_CONFIRMED_TEXT = get_text('group_invitation_confirmed', + default="""Hallo {name}, + +{waiter} hat die Einladung zu einer Schnupperstunde bei der Gruppe {group} angenommen. + +Viele Grüße +Dein KOMPASS""") + + +TRIAL_GROUP_MEETING_CONFIRMED_TEXT = get_text('trial_group_meeting_confirmed', + default="""Hallo {name}, + +deine Teilnahme an der Schnupperstunde der Gruppe {group} wurde erfolgreich bestätigt. +{timeinfo} + +Für alle weiteren Absprachen, kontaktiere bitte die Jugendleiter*innen der Gruppe +unter {contact_email}. + +Viele Grüße +Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION }) + GROUP_TIME_AVAILABLE_TEXT = get_text('group_time_available', default="""Die Gruppenstunde findet jeden {weekday} von {start_time} bis {end_time} Uhr statt.""") @@ -51,7 +73,11 @@ INVITE_TEXT = get_text('invite', default="""Hallo {{name}}, wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden. {group_time} -Bitte kontaktiere die Gruppenleitung ({contact_email}) für alle weiteren Absprachen. +Wenn du an der Schnupperstunde teilnehmen möchtest, bestätige deine Teilnahme bitte unter folgendem Link: + +{{invitation_confirm_link}} + +Für alle weiteren Absprachen, kontaktiere bitte die Gruppenleitung ({contact_email}). Wenn du nach der Schnupperstunde beschließt der Gruppe beizutreten, benötigen wir noch ein paar Informationen und eine schriftliche Anmeldebestätigung von dir. Das kannst du alles über folgenden Link erledigen: diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index ba5cc67..b78cb1d 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -76,6 +76,10 @@ def get_invitation_reject_link(key): return prepend_base_url("/members/waitinglist/invitation/reject?key={}".format(key)) +def get_invitation_confirm_link(key): + return prepend_base_url("/members/waitinglist/invitation/confirm?key={}".format(key)) + + def get_wait_confirmation_link(waiter): key = waiter.generate_wait_confirmation_key() return prepend_base_url("/members/waitinglist/confirm?key={}".format(key)) diff --git a/jdav_web/members/locale/de/LC_MESSAGES/django.po b/jdav_web/members/locale/de/LC_MESSAGES/django.po index 9b46b2b..e2d3d42 100644 --- a/jdav_web/members/locale/de/LC_MESSAGES/django.po +++ b/jdav_web/members/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-06 14:02+0200\n" +"POT-Creation-Date: 2025-04-06 15:38+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -805,6 +805,15 @@ msgstr "%(waiter)s hat die Warteliste verlassen" msgid "Group invitation rejected by %(waiter)s" msgstr "Einladung zur Schnupperstunde von %(waiter)s abgelehnt" +#: members/models.py +#, python-format +msgid "Group invitation confirmed by %(waiter)s" +msgstr "Teilnahme an Schnupperstunde von %(waiter)s bestätigt" + +#: members/models.py +msgid "Trial group meeting confirmed" +msgstr "Teilnahme an Schnupperstunde bestätigt" + #: members/models.py msgid "Do you want to tell us something else?" msgstr "Möchtest du uns noch etwas mitteilen?" @@ -1655,6 +1664,60 @@ msgstr "Fähigkeitsniveau" msgid "Save and confirm registration" msgstr "Speichern und Registrierung bestätigen" +#: members/templates/members/confirm_invalid.html +msgid "Confirm invitation" +msgstr "Teilnahme bestätigen" + +#: members/templates/members/confirm_invalid.html +#: members/templates/members/reject_invalid.html members/tests.py +msgid "This invitation is invalid or expired." +msgstr "Diese Einladung ist ungültig oder abgelaufen." + +#: members/templates/members/confirm_invitation.html +msgid "Confirm trial group meeting invitation" +msgstr "Teilnahme bestätigen" + +#: members/templates/members/confirm_invitation.html +#, python-format +msgid "You were invited to a trial group meeting of the group %(groupname)s." +msgstr "" +"Du wurdest zu einer Schnupperstunde in der Gruppe %(groupname)s eingeladen." + +#: members/templates/members/confirm_invitation.html +msgid "" +"Do you want to take part in the trial group meeting? If yes, please confirm " +"your attendance by clicking on the following button." +msgstr "" +"Möchtest du an der Schnupperstunde teilnehmen? Falls ja, bitte bestätige " +"deine Teilnahme durch das Betätigen des folgenden Knopfes." + +#: members/templates/members/confirm_invitation.html +msgid "Confirm trial group meeting" +msgstr "Teilnahme bestätigen" + +#: members/templates/members/confirm_success.html +msgid "Invitation confirmed" +msgstr "Teilnahme bestätigt" + +#: members/templates/members/confirm_success.html +#, python-format +msgid "" +"You successfully confirmed the invitation to the trial group meeting of the " +"group %(groupname)s." +msgstr "" +"Deine Teilnahme an der Schnupperstunde der Gruppe %(groupname)s wurde " +"erfolgreich bestätigt." + +#: members/templates/members/confirm_success.html +msgid "" +"We have informed the group leaders about your confirmation. If for some " +"reason you can not make it,\n" +"please contact the group leaders at" +msgstr "" +"Wir haben die Jugendleiter*innen der Jugendgruppe über deine Bestätigung " +"informiert. Falls du doch nicht zur Schnupperstunde kommen kannst, " +"informiere bitte die Jugendleiter*innen unter" + #: members/templates/members/echo.html #: members/templates/members/echo_failed.html #: members/templates/members/echo_password.html @@ -1942,10 +2005,6 @@ msgstr "" msgid "Reject invitation" msgstr "Einladung ablehnen" -#: members/templates/members/reject_invalid.html members/tests.py -msgid "This invitation is invalid or expired." -msgstr "Diese Einladung ist ungültig oder abgelaufen." - #: members/templates/members/reject_invitation.html #, python-format msgid "" @@ -2240,14 +2299,6 @@ msgstr "Ungültige Notfallkontakte" #~ msgid "Good conduct certificate presentation needed" #~ msgstr "Vorlage Führungszeugnis notwendig" -#, python-format -#~ msgid "" -#~ "Do you want to reject the invitation to a trial group meeting of the\n" -#~ "group %(invitation.group.name)s?" -#~ msgstr "" -#~ "Möchtest du die Einladung zur Schnupperstunde bei der Gruppe " -#~ "%(invitation.group.name)s wirklich ablehnen?" - #~ msgid "Yes, reject invitation" #~ msgstr "Ja, Einladung ablehnen." diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index a1f179d..7e6eecc 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -18,7 +18,8 @@ from utils import RestrictedFileField, normalize_name import os from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\ prepend_base_url, get_registration_link, get_wait_confirmation_link,\ - get_invitation_reject_link, get_invite_as_user_key, get_leave_waitinglist_link + get_invitation_reject_link, get_invite_as_user_key, get_leave_waitinglist_link,\ + get_invitation_confirm_link from django.contrib.auth.models import User from django.conf import settings from django.core.validators import MinValueValidator @@ -109,6 +110,14 @@ class Group(models.Model): # return if the group has all relevant time slot information filled return self.weekday and self.start_time and self.end_time + def get_time_info(self): + if self.has_time_info(): + return settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[self.weekday][1], + start_time=self.start_time.strftime('%H:%M'), + end_time=self.end_time.strftime('%H:%M')) + else: + return "" + def get_invitation_text_template(self): """The text template used to invite waiters to this group. This contains placeholders for the name of the waiter and personalized links.""" @@ -117,9 +126,7 @@ class Group(models.Model): else: group_link = '' if self.has_time_info(): - group_time = settings.GROUP_TIME_AVAILABLE_TEXT.format(weekday=WEEKDAYS[self.weekday][1], - start_time=self.start_time.strftime('%H:%M'), - end_time=self.end_time.strftime('%H:%M')) + group_time = self.get_time_info() else: group_time = settings.GROUP_TIME_UNAVAILABLE_TEXT.format(contact_email=self.contact_email) return settings.INVITE_TEXT.format(group_time=group_time, @@ -912,6 +919,21 @@ class InvitationToGroup(models.Model): settings.DEFAULT_SENDING_MAIL, recipient.email) + def send_confirm_notification_to(self, recipient): + send_mail(_('Group invitation confirmed by %(waiter)s') % {'waiter': self.waiter}, + settings.GROUP_INVITATION_CONFIRMED_TEXT.format(name=recipient.prename, + waiter=self.waiter, + group=self.group), + settings.DEFAULT_SENDING_MAIL, + recipient.email) + + def send_confirm_confirmation(self): + self.waiter.send_mail(_('Trial group meeting confirmed'), + settings.TRIAL_GROUP_MEETING_CONFIRMED_TEXT.format(name=self.waiter.prename, + group=self.group, + contact_email=self.group.contact_email, + timeinfo=self.group.get_time_info())) + def notify_left_waitinglist(self): """ Inform youth leaders of the group and the inviter that the waiter left the waitinglist, @@ -932,6 +954,18 @@ class InvitationToGroup(models.Model): for jl in self.group.leiters.all(): self.send_reject_notification_to(jl) + def confirm(self): + """Confirm this invitation. Informs the youth leaders of the group of the invitation.""" + self.rejected = False + self.save() + # confirm the confirmation + self.send_confirm_confirmation() + # send notifications + if self.created_by: + self.send_confirm_notification_to(self.created_by) + for jl in self.group.leiters.all(): + self.send_confirm_notification_to(jl) + class MemberWaitingList(Person): """A participant on the waiting list""" @@ -1063,7 +1097,8 @@ class MemberWaitingList(Person): self.send_mail(_("Invitation to trial group meeting"), text_template.format(name=self.prename, link=get_registration_link(invitation.key), - invitation_reject_link=get_invitation_reject_link(invitation.key)), + invitation_reject_link=get_invitation_reject_link(invitation.key), + invitation_confirm_link=get_invitation_confirm_link(invitation.key)), cc=group.contact_email.email) def unregister(self): diff --git a/jdav_web/members/templates/members/confirm_invalid.html b/jdav_web/members/templates/members/confirm_invalid.html new file mode 100644 index 0000000..8448e06 --- /dev/null +++ b/jdav_web/members/templates/members/confirm_invalid.html @@ -0,0 +1,15 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Confirm invitation" %} +{% endblock %} + +{% block content %} + +

{% trans "Confirm invitation" %}

+ +

{% trans "This invitation is invalid or expired." %}

+ +{% endblock %} diff --git a/jdav_web/members/templates/members/confirm_invitation.html b/jdav_web/members/templates/members/confirm_invitation.html new file mode 100644 index 0000000..f2af2c6 --- /dev/null +++ b/jdav_web/members/templates/members/confirm_invitation.html @@ -0,0 +1,29 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Confirm trial group meeting invitation" %} +{% endblock %} + +{% block content %} + +

{% trans "Confirm trial group meeting invitation" %}

+ +

+{% blocktrans %}You were invited to a trial group meeting of the group {{ groupname }}.{% endblocktrans %} +{{ timeinfo }} +

+ +

+{% blocktrans %}Do you want to take part in the trial group meeting? If yes, please confirm your attendance by clicking on the following button.{% endblocktrans %} +

+ +
+ {% csrf_token %} + + +
+ +{% endblock %} diff --git a/jdav_web/members/templates/members/confirm_success.html b/jdav_web/members/templates/members/confirm_success.html new file mode 100644 index 0000000..213355a --- /dev/null +++ b/jdav_web/members/templates/members/confirm_success.html @@ -0,0 +1,23 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Invitation confirmed" %} +{% endblock %} + +{% block content %} + +

{% trans "Invitation confirmed" %}

+ +

+{% blocktrans %}You successfully confirmed the invitation to the trial group meeting of the group {{ groupname }}.{% endblocktrans %} +{{ timeinfo }} +

+

+{% blocktrans %}We have informed the group leaders about your confirmation. If for some reason you can not make it, +please contact the group leaders at{% endblocktrans %} +{{ contact_email }}. +

+ +{% endblock %} diff --git a/jdav_web/members/urls.py b/jdav_web/members/urls.py index 7390159..1fdedfb 100644 --- a/jdav_web/members/urls.py +++ b/jdav_web/members/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ re_path(r'^waitinglist/confirm', views.confirm_waiting , name='confirm_waiting'), re_path(r'^waitinglist/leave', views.leave_waitinglist , name='leave_waitinglist'), re_path(r'^waitinglist/invitation/reject', views.reject_invitation , name='reject_invitation'), + re_path(r'^waitinglist/invitation/confirm', views.confirm_invitation , name='confirm_invitation'), re_path(r'^waitinglist', views.register_waiting_list , name='register_waiting_list'), re_path(r'^mail/confirm', views.confirm_mail , name='confirm_mail'), ] diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py index 3cb22ea..7f9abee 100644 --- a/jdav_web/members/views.py +++ b/jdav_web/members/views.py @@ -479,6 +479,47 @@ def reject_invitation(request): return render_reject_invalid(request) +def render_confirm_invitation(request, invitation): + return render(request, 'members/confirm_invitation.html', + {'invitation': invitation, + 'groupname': invitation.group.name, + 'contact_email': invitation.group.contact_email, + 'timeinfo': invitation.group.get_time_info()}) + + +def render_confirm_invalid(request): + return render(request, 'members/confirm_invalid.html') + + +def render_confirm_success(request, invitation): + return render(request, 'members/confirm_success.html', + {'invitation': invitation, + 'groupname': invitation.group.name, + 'contact_email': invitation.group.contact_email, + 'timeinfo': invitation.group.get_time_info()}) + + +def confirm_invitation(request): + if request.method == 'GET' and 'key' in request.GET: + key = request.GET['key'] + try: + invitation = InvitationToGroup.objects.get(key=key) + if invitation.rejected or invitation.is_expired(): + raise ValueError + return render_confirm_invitation(request, invitation) + except (ValueError, InvitationToGroup.DoesNotExist): + return render_confirm_invalid(request) + if request.method != 'POST' or 'key' not in request.POST: + return render_confirm_invalid(request) + key = request.POST['key'] + try: + invitation = InvitationToGroup.objects.get(key=key) + except InvitationToGroup.DoesNotExist: + return render_confirm_invalid(request) + invitation.confirm() + return render_confirm_success(request, invitation) + + def confirm_waiting(request): if request.method == 'GET' and 'key' in request.GET: key = request.GET['key']