members: rejection of invitations

pull/73/head
Christian Merten 1 year ago
parent d512d2b14c
commit e2d660ed1e
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -27,20 +27,37 @@ INVITE_TEXT = """Hallo {name},
wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden. wir haben gute Neuigkeiten für dich. Es ist ein Platz in der Jugendgruppe {group_name} {group_link}freigeworden.
Wir treffen uns jeden {weekday} von {start_time} bis {end_time} Uhr. Wir treffen uns jeden {weekday} von {start_time} bis {end_time} Uhr.
Wir brauchen jetzt noch ein paar Informationen von dir und deine Anmeldebestätigung. Das kannst du alles über folgenden Wir brauchen jetzt noch ein paar Informationen von dir und deine Anmeldebestätigung. Die lädst du hier herunter,
Link erledigen: lässt es von deinen Eltern ausfüllen, unterschreiben und lädst ein Foto davon in unserem Anmeldeformular hoch.
Das kannst du alles über folgenden Link erledigen:
{link} {link}
Du siehst dort auch die Daten, die du bei deiner Eintragung auf die Warteliste angegeben hast. Bitte Du siehst dort auch die Daten, die du bei deiner Eintragung auf die Warteliste angegeben hast. Bitte
überprüfe, ob die Daten noch stimmen und ändere sie bei Bedarf ab. überprüfe, ob die Daten noch stimmen und ändere sie bei Bedarf ab.
Falls du zu dem obigen Termin keine Zeit hast oder dich ganz von der Warteliste abmelden möchtest,
lehne bitte diese Einladung unter folgendem Link ab:
{invitation_reject_link}
Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s. Bei Fragen, wende dich gerne an %(RESPONSIBLE_MAIL)s.
Viele Grüße Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL } Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL }
LEAVE_WAITINGLIST_TEXT = """Hallo {name},
du hast dich erfolgreich von der Warteliste abgemeldet. Falls du zu einem späteren
Zeitpunkt wieder der Warteliste beitreten möchtest, kannst du das über unsere Webseite machen.
Falls du dich nicht selbst abgemeldet hast, wende dich bitte umgehend an %(RESPONSIBLE_MAIL)s.
Viele Grüße
Deine JDAV %(SEKTION)s""" % { 'SEKTION': SEKTION, 'RESPONSIBLE_MAIL': RESPONSIBLE_MAIL }
WAIT_CONFIRMATION_TEXT = """Hallo {name}, WAIT_CONFIRMATION_TEXT = """Hallo {name},
leider können wir dir zur Zeit noch keinen Platz in einer Jugendgruppe anbieten. Da wir leider können wir dir zur Zeit noch keinen Platz in einer Jugendgruppe anbieten. Da wir

@ -64,11 +64,14 @@ def get_echo_link(member):
return prepend_base_url("/members/echo?key={}".format(key)) return prepend_base_url("/members/echo?key={}".format(key))
def get_registration_link(waiter): def get_registration_link(key):
key = waiter.generate_registration_key()
return prepend_base_url("/members/registration?key={}".format(key)) return prepend_base_url("/members/registration?key={}".format(key))
def get_invitation_reject_link(key):
return prepend_base_url("/members/waitinglist/invitation/reject?key={}".format(key))
def get_wait_confirmation_link(waiter): def get_wait_confirmation_link(waiter):
key = waiter.generate_wait_confirmation_key() key = waiter.generate_wait_confirmation_key()
return prepend_base_url("/members/waitinglist/confirm?key={}".format(key)) return prepend_base_url("/members/waitinglist/confirm?key={}".format(key))

@ -36,7 +36,8 @@ from .models import (Member, Group, Freizeit, MemberNoteList, NewMemberOnList, K
MemberWaitingList, LJPProposal, Intervention, PermissionMember, MemberWaitingList, LJPProposal, Intervention, PermissionMember,
PermissionGroup, MemberTraining, TrainingCategory, PermissionGroup, MemberTraining, TrainingCategory,
KlettertreffAttendee, ActivityCategory, EmergencyContact, KlettertreffAttendee, ActivityCategory, EmergencyContact,
annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy) annotate_activity_score, RegistrationPassword, MemberUnconfirmedProxy,
InvitationToGroup)
from finance.models import Statement, BillOnExcursionProxy from finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings from django.conf import settings
@ -414,16 +415,28 @@ class WaiterInviteForm(forms.Form):
label=_('Group')) label=_('Group'))
class InvitationToGroupAdmin(CommonAdminInlineMixin, admin.TabularInline):
model = InvitationToGroup
fields = ['group', 'date', 'status']
readonly_fields = ['group', 'date', 'status']
extra = 0
can_delete = False
def has_add_permission(self, request, obj=None):
return False
class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin): class MemberWaitingListAdmin(CommonAdminMixin, admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'birth_date', 'gender', 'application_text', fields = ['prename', 'lastname', 'email', 'birth_date', 'gender', 'application_text',
'application_date', 'comments', 'invited_for_group', 'application_date', 'comments',
'sent_reminders'] 'sent_reminders']
list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail', list_display = ('name', 'birth_date', 'age', 'application_date', 'confirmed_mail',
'waiting_confirmed', 'sent_reminders') 'waiting_confirmed', 'sent_reminders')
search_fields = ('prename', 'lastname', 'email') search_fields = ('prename', 'lastname', 'email')
list_filter = ('confirmed_mail',) list_filter = ('confirmed_mail',)
actions = ['ask_for_registration', 'ask_for_wait_confirmation'] actions = ['ask_for_registration', 'ask_for_wait_confirmation']
readonly_fields= ['invited_for_group', 'application_date', 'sent_reminders'] inlines = [InvitationToGroupAdmin]
readonly_fields= ['application_date', 'sent_reminders']
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
return False return False

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
# Generated by Django 4.0.1 on 2024-11-17 21:32
from django.db import migrations, models
import django.db.models.deletion
import members.models
class Migration(migrations.Migration):
dependencies = [
('members', '0020_alter_freizeit_options_freizeit_description_and_more'),
]
operations = [
migrations.CreateModel(
name='InvitationToGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(auto_now=True, verbose_name='Invitation date')),
('rejected', models.BooleanField(default=False, verbose_name='Invitation rejected')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.group', verbose_name='Group')),
('waiter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.memberwaitinglist', verbose_name='Waiter')),
('key', models.CharField(default=members.models.gen_key, max_length=32)),
],
options={
'verbose_name': 'Invitation to group',
'verbose_name_plural': 'Invitations to groups',
},
),
migrations.RemoveField(
model_name='memberwaitinglist',
name='invited_for_group',
),
]

@ -14,7 +14,8 @@ from django.contrib.contenttypes.models import ContentType
from utils import RestrictedFileField from utils import RestrictedFileField
import os import os
from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\ from mailer.mailutils import send as send_mail, get_mail_confirmation_link,\
prepend_base_url, get_registration_link, get_wait_confirmation_link prepend_base_url, get_registration_link, get_wait_confirmation_link,\
get_invitation_reject_link
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -689,6 +690,35 @@ class MemberUnconfirmedProxy(Member):
return self.name return self.name
def gen_key():
return uuid.uuid4().hex
class InvitationToGroup(models.Model):
"""An invitation of a waiter to a group."""
waiter = models.ForeignKey('MemberWaitingList', verbose_name=_('Waiter'), on_delete=models.CASCADE)
group = models.ForeignKey(Group, verbose_name=_('Group'), on_delete=models.CASCADE)
date = models.DateField(auto_now=True, verbose_name=_('Invitation date'))
rejected = models.BooleanField(verbose_name=_('Invitation rejected'), default=False)
key = models.CharField(max_length=32, default=gen_key)
class Meta:
verbose_name = _('Invitation to group')
verbose_name_plural = _('Invitations to groups')
def is_expired(self):
return self.date < (timezone.now() - timezone.timedelta(days=30)).date()
def status(self):
if self.rejected:
return _('Rejected')
elif self.is_expired():
return _('Expired')
else:
return _('Undecided')
status.short_description = _('Status')
class MemberWaitingList(Person): class MemberWaitingList(Person):
"""A participant on the waiting list""" """A participant on the waiting list"""
@ -710,12 +740,6 @@ class MemberWaitingList(Person):
registration_key = models.CharField(max_length=32, default="") registration_key = models.CharField(max_length=32, default="")
registration_expire = models.DateTimeField(default=timezone.now) registration_expire = models.DateTimeField(default=timezone.now)
invited_for_group = models.ForeignKey(Group,
null=True,
blank=True,
default=None,
verbose_name=_('Invited for group'),
on_delete=models.SET_NULL)
class Meta(CommonModel.Meta): class Meta(CommonModel.Meta):
verbose_name = _('Waiter') verbose_name = _('Waiter')
verbose_name_plural = _('Waiters') verbose_name_plural = _('Waiters')
@ -783,14 +807,13 @@ class MemberWaitingList(Person):
self.save() self.save()
return self.wait_confirmation_key return self.wait_confirmation_key
def generate_registration_key(self):
self.registration_key = uuid.uuid4().hex
self.registration_expire = timezone.now() + timezone.timedelta(days=30)
self.save()
return self.registration_key
def may_register(self, key): def may_register(self, key):
return self.registration_key == key and timezone.now() < self.registration_expire print("may_register", key)
try:
invitation = InvitationToGroup.objects.get(key=key)
return self.pk == invitation.waiter.pk and timezone.now() < invitation.date + timezone.timedelta(days=30)
except InvitationToGroup.DoesNotExist:
return False
def invite_to_group(self, group): def invite_to_group(self, group):
if group.show_website: if group.show_website:
@ -801,6 +824,8 @@ class MemberWaitingList(Person):
weekday = WEEKDAYS[group.weekday][1] if group.weekday != None else WEEKDAYS[0][1] weekday = WEEKDAYS[group.weekday][1] if group.weekday != None else WEEKDAYS[0][1]
start_time = group.start_time.strftime('%H:%M') if group.start_time != None else "14:00" start_time = group.start_time.strftime('%H:%M') if group.start_time != None else "14:00"
end_time = group.end_time.strftime('%H:%M') if group.end_time != None else "16:00" end_time = group.end_time.strftime('%H:%M') if group.end_time != None else "16:00"
invitation = InvitationToGroup(group=group, waiter=self)
invitation.save()
self.send_mail(_("Invitation to trial group meeting"), self.send_mail(_("Invitation to trial group meeting"),
settings.INVITE_TEXT.format(name=self.prename, settings.INVITE_TEXT.format(name=self.prename,
weekday=weekday, weekday=weekday,
@ -808,7 +833,14 @@ class MemberWaitingList(Person):
end_time=end_time, end_time=end_time,
group_name=group.name, group_name=group.name,
group_link=group_link, group_link=group_link,
link=get_registration_link(self))) link=get_registration_link(invitation.key),
invitation_reject_link=get_invitation_reject_link(invitation.key)))
def unregister(self):
"""Delete the waiter and inform them about the deletion via email."""
self.send_mail(_("Unregistered from waiting list"),
settings.LEAVE_WAITINGLIST_TEXT.format(name=self.prename))
self.delete()
class NewMemberOnList(CommonModel): class NewMemberOnList(CommonModel):

@ -0,0 +1,15 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Reject invitation" %}
{% endblock %}
{% block content %}
<h1>{% trans "Reject invitation" %}</h1>
<p>{% trans "This invitation is invalid or expired." %}</p>
{% endblock %}

@ -0,0 +1,31 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Reject invitation" %}
{% endblock %}
{% block content %}
<h1>{% trans "Reject invitation" %}</h1>
<p>{% blocktrans %}You were invited to a trial group meeting of the group {{ groupname }}. On this
page you can reject this invitation.{% endblocktrans %}
</p>
<p>{% blocktrans %}You may either reject this specific invitation, because
the time of the group does not fit your calendear, or unregister from the mailing list altogether.{% endblocktrans %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="key" value="{{invitation.key}}">
<p>
<input type="submit" name="reject_invitation"
value="{% trans "Reject this invitation and stay on the waitinglist" %}"/>
<input type="submit" name="leave_waitinglist" value="{% trans "Leave the waitinglist" %}"/>
</p>
</form>
{% endblock %}

@ -0,0 +1,25 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Invitation rejected" %}
{% endblock %}
{% block content %}
<h1>{% trans "Invitation rejected" %}</h1>
<p>
{% if leave_waitinglist %}
{% blocktrans %}You successfully unregistered from the waitinglist. If you want to rejoin the waitinglist
at a later time, please do so on our website.
{% endblocktrans %}
{% else %}
{% blocktrans %}You successfully rejected the invitation to a trial group meeting of the group
{{ groupname }}. If there is a slot in a different group available, you will receive a new invitation.
{% endblocktrans %}
{% endif %}
</p>
{% endblock %}

@ -8,6 +8,7 @@ urlpatterns = [
re_path(r'^registration', views.invited_registration , name='registration'), re_path(r'^registration', views.invited_registration , name='registration'),
re_path(r'^register', views.register , name='register'), re_path(r'^register', views.register , name='register'),
re_path(r'^waitinglist/confirm', views.confirm_waiting , name='confirm_waiting'), re_path(r'^waitinglist/confirm', views.confirm_waiting , name='confirm_waiting'),
re_path(r'^waitinglist/invitation/reject', views.reject_invitation , name='reject_invitation'),
re_path(r'^waitinglist', views.register_waiting_list , name='register_waiting_list'), re_path(r'^waitinglist', views.register_waiting_list , name='register_waiting_list'),
re_path(r'^mail/confirm', views.confirm_mail , name='confirm_mail'), re_path(r'^mail/confirm', views.confirm_mail , name='confirm_mail'),
] ]

@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect
from django.forms import ModelForm, TextInput, DateInput, BaseInlineFormSet,\ from django.forms import ModelForm, TextInput, DateInput, BaseInlineFormSet,\
inlineformset_factory, HiddenInput, FileInput inlineformset_factory, HiddenInput, FileInput
from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy, MemberWaitingList, Group,\ from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy, MemberWaitingList, Group,\
confirm_mail_by_key, EmergencyContact confirm_mail_by_key, EmergencyContact, InvitationToGroup
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
@ -229,9 +229,10 @@ def register(request):
return render_register_wrong_password(request) return render_register_wrong_password(request)
elif waiter_key: elif waiter_key:
try: try:
waiter = MemberWaitingList.objects.get(registration_key=waiter_key) invitation = InvitationToGroup.objects.get(key=waiter_key)
group = waiter.invited_for_group waiter = invitation.waiter
except MemberWaitingList.DoesNotExist: group = invitation.group
except InvitationToGroup.DoesNotExist:
return render_register_failed(request) return render_register_failed(request)
# group must not be None # group must not be None
@ -323,14 +324,13 @@ def invited_registration(request):
if request.method == 'GET' and 'key' in request.GET: if request.method == 'GET' and 'key' in request.GET:
try: try:
key = request.GET['key'] key = request.GET['key']
waiter = MemberWaitingList.objects.get(registration_key=key) invitation = InvitationToGroup.objects.get(key=key)
if not waiter.may_register(key): waiter = invitation.waiter
raise KeyError if invitation.is_expired() or invitation.rejected:
if not waiter.invited_for_group:
raise KeyError raise KeyError
form = MemberRegistrationForm(instance=waiter) form = MemberRegistrationForm(instance=waiter)
return render_register(request, group=waiter.invited_for_group, form=form, waiter_key=key) return render_register(request, group=invitation.group, form=form, waiter_key=key)
except MemberWaitingList.DoesNotExist: except InvitationToGroup.DoesNotExist:
return render_invited_registration_failed(request, _("invalid")) return render_invited_registration_failed(request, _("invalid"))
except KeyError: except KeyError:
return render_invited_registration_failed(request, _("expired")) return render_invited_registration_failed(request, _("expired"))
@ -346,6 +346,50 @@ def render_invited_registration_failed(request, reason=""):
return render(request, 'members/invited_registration_failed.html', context) return render(request, 'members/invited_registration_failed.html', context)
def render_reject_invitation(request, invitation):
return render(request, 'members/reject_invitation.html',
{'invitation': invitation,
'groupname': invitation.group.name})
def render_reject_invalid(request):
return render(request, 'members/reject_invalid.html')
def render_reject_success(request, invitation, leave_waitinglist=False):
return render(request, 'members/reject_success.html',
{'invitation': invitation,
'leave_waitinglist': leave_waitinglist,
'groupname': invitation.group.name})
def reject_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_reject_invitation(request, invitation)
except (ValueError, InvitationToGroup.DoesNotExist):
return render_reject_invalid(request)
if request.method != 'POST' or 'key' not in request.POST:
return render_reject_invalid(request)
key = request.POST['key']
try:
invitation = InvitationToGroup.objects.get(key=key)
except InvitationToGroup.DoesNotExist:
return render_reject_invalid(request)
if 'reject_invitation' in request.POST:
invitation.rejected = True
invitation.save()
return render_reject_success(request, invitation)
elif 'leave_waitinglist' in request.POST:
invitation.waiter.unregister()
return render_reject_success(request, invitation, leave_waitinglist=True)
return render_reject_invalid(request)
def confirm_waiting(request): def confirm_waiting(request):
if request.method == 'GET' and 'key' in request.GET: if request.method == 'GET' and 'key' in request.GET:
key = request.GET['key'] key = request.GET['key']

Loading…
Cancel
Save