waiting list: add waiting status confirmation mechanism and automation with celery beat

v1-0-stable
Christian Merten 3 years ago
parent a362f963b4
commit ed8d8b0434
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

3
.gitignore vendored

@ -108,3 +108,6 @@ jdav_web/static/jet/css/themes/*/.sass-cache/*
# test config
config
# celerybeat schedule database
jdav_web/celerybeat-schedule.db

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

@ -8,7 +8,7 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jdav_web.settings')
app = Celery()
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(settings.INSTALLED_APPS)
app.autodiscover_tasks()
if __name__ == '__main__':
app.start()

@ -58,6 +58,7 @@ INSTALLED_APPS = [
'ludwigsburgalpin.apps.LudwigsburgalpinConfig',
#'easy_select2',
'djcelery_email',
'django_celery_beat',
'jet',
'django.contrib.admin',
'django.contrib.auth',
@ -203,6 +204,13 @@ JET_SIDE_MENU_ITEMS = [
{'name': 'group', 'permissions': ['auth.group'] },
{'name': 'user', 'permissions': ['auth.user']},
]},
{'app_label': 'django_celery_beat', 'permissions': ['django_celery_beat'], 'items': [
{'name': 'crontabschedule'},
{'name': 'clockedschedule'},
{'name': 'intervalschedule'},
{'name': 'periodictask'},
{'name': 'solarschedule'},
]},
{'app_label': 'ludwigsburgalpin', 'permissions': ['ludwigsburgalpin'], 'items': [
{'name': 'termin'},
]},
@ -229,3 +237,9 @@ JET_SIDE_MENU_ITEMS = [
{ 'label': 'Packlisten und Co.', 'url': 'https://cloud.jdav-ludwigsburg.de/index.php/s/qxQCTR8JqYSXXCQ'}
]},
]
# Waiting list configuration parameters, all numbers are in days
GRACE_PERIOD_WAITING_CONFIRMATION = 30
WAITING_CONFIRMATION_FREQUENCY = 90

@ -77,6 +77,11 @@ def get_registration_link(waiter):
return prepend_base_url("/members/registration?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))
def get_mail_confirmation_link(key):
return prepend_base_url("/members/mail/confirm?key={}".format(key))

@ -248,15 +248,24 @@ class WaiterInviteForm(forms.Form):
class MemberWaitingListAdmin(admin.ModelAdmin):
fields = ['prename', 'lastname', 'email', 'email_parents', 'birth_date', 'comments', 'invited_for_group']
list_display = ('name', 'birth_date', 'age', 'confirmed_mail', 'confirmed_mail_parents')
list_display = ('name', 'birth_date', 'age', 'confirmed_mail', 'confirmed_mail_parents',
'waiting_confirmed')
search_fields = ('prename', 'lastname', 'email')
list_filter = ('confirmed_mail', 'confirmed_mail_parents')
actions = ['request_mail_confirmation', 'ask_for_registration']
actions = ['ask_for_registration', 'ask_for_wait_confirmation']
readonly_fields= ('invited_for_group',)
def has_add_permission(self, request, obj=None):
return False
def ask_for_wait_confirmation(self, request, queryset):
"""Asks the waiting person to confirm their waiting status."""
for waiter in queryset:
waiter.ask_for_wait_confirmation()
messages.success(request,
_("Successfully asked %(name)s to confirm their waiting status.") % {'name': waiter.name})
ask_for_wait_confirmation.short_description = _('Ask the waiter to confirm their waiting status')
def ask_for_registration(self, request, queryset):
"""Asks the waiting person to register with all required data."""
if "apply" in request.POST:

@ -11,8 +11,9 @@ from django.contrib.contenttypes.models import ContentType
from utils import RestrictedFileField
import os
from mailer.mailutils import send as send_mail, mail_root, get_mail_confirmation_link,\
prepend_base_url, get_registration_link
prepend_base_url, get_registration_link, get_wait_confirmation_link
from django.contrib.auth.models import User
from django.conf import settings
from dateutil.relativedelta import relativedelta
@ -51,7 +52,6 @@ class Group(models.Model):
year_to = models.IntegerField(verbose_name=_('highest year'), default=2011)
leiters = models.ManyToManyField('members.Member', verbose_name=_('youth leaders'),
related_name='leited_groups', blank=True)
secret = models.CharField(max_length=32, default=generate_random_key)
def __str__(self):
"""String representation"""
@ -138,6 +138,13 @@ class Person(models.Model):
self.save()
return (email, parents)
def send_mail(self, subject, content):
send_mail(subject,
content,
mail_root,
[self.email, self.email_parents] if self.email_parents and self.cc_email_parents
else self.email)
class Member(Person):
"""
@ -318,9 +325,14 @@ class MemberUnconfirmedProxy(Member):
class MemberWaitingList(Person):
"""A participant on the waiting list"""
WAITING_CONFIRMATION_SUCCESS = 0
WAITING_CONFIRMATION_INVALID = 1
WAITING_CONFIRMATION_EXPIRED = 1
WAITING_CONFIRMED = 2
last_wait_confirmation = models.DateField(auto_now=True, verbose_name=_('Last wait confirmation'))
wait_confirmation_key = models.CharField(max_length=32, default="")
wait_confirmation_key_expiry = models.DateTimeField(default=timezone.now)
wait_confirmation_key_expire = models.DateTimeField(default=timezone.now)
registration_key = models.CharField(max_length=32, default="")
registration_expire = models.DateTimeField(default=timezone.now)
@ -341,12 +353,50 @@ class MemberWaitingList(Person):
def waiting_confirmation_needed(self):
"""Returns if person should be asked to confirm waiting status."""
return wait_confirmation_key is None \
and last_wait_confirmation < timezone.now - timezone.timedelta(days=90)
and last_wait_confirmation < timezone.now() - timezone.timedelta(days=90)
@property
def waiting_confirmed(self):
"""Returns if person is still confirmed to be waiting."""
return last_wait_confirmation < timezone.now() - timezone.timedelta(days=100)
"""Returns if the persons waiting status is considered to be confirmed."""
cutoff = timezone.now() \
- timezone.timedelta(days= settings.GRACE_PERIOD_WAITING_CONFIRMATION \
+ settings.WAITING_CONFIRMATION_FREQUENCY)
return self.last_wait_confirmation > cutoff.date()
waiting_confirmed.admin_order_field = 'last_wait_confirmation'
waiting_confirmed.boolean = True
waiting_confirmed.short_description = _('Waiting status confirmed')
def ask_for_wait_confirmation(self):
"""Sends an email to the person asking them to confirm their intention to wait."""
self.send_mail(_('Waiting confirmation needed'),
WAIT_CONFIRMATION_TEXT.format(name=self.prename,
link=get_wait_confirmation_link(self)))
def confirm_waiting(self, key):
# if a wrong key is supplied, we return invalid
if not self.wait_confirmation_key == key:
return self.WAITING_CONFIRMATION_INVALID
# if the current wait confirmation key is not expired, return sucess
if timezone.now() < self.wait_confirmation_key_expire:
self.last_wait_confirmation = timezone.now()
self.wait_confirmation_key_expire = timezone.now()
self.save()
return self.WAITING_CONFIRMATION_SUCCESS
# if the waiting is already confirmed, return success
# this might happen if both parents and member mail are used for communication
if self.waiting_confirmed():
return self.WAITING_CONFIRMED
# otherwise the link is too old and the person was not confirmed in time
return self.WAITING_CONFIRMATION_EXPIRED
def generate_wait_confirmation_key(self):
self.wait_confirmation_key = uuid.uuid4().hex
self.wait_confirmation_key_expire = timezone.now() \
+ timezone.timedelta(days=settings.GRACE_PERIOD_WAITING_CONFIRMATION)
self.save()
return self.wait_confirmation_key
def generate_registration_key(self):
self.registration_key = uuid.uuid4().hex
@ -710,3 +760,20 @@ Bei Fragen, wende dich gerne an jugendreferent@jdav-ludwigsburg.de.
Viele Grüße
Deine JDAV Ludwigsburg"""
WAIT_CONFIRMATION_TEXT = """Hallo {name},
leider können wir dir zur Zeit noch keinen Platz in einer Jugendgruppe anbieten. Da wir
sehr viele Interessenten haben und wir möglichst vielen die Möglichkeit bieten möchten, an
einer Jugendgruppe teilhaben zu können, fragen wir regelmäßig alle Personen auf der
Warteliste ab, ob sie noch Interesse haben.
Wenn du weiterhin auf der Warteliste bleiben möchtest, klicke auf den folgenden Link:
{link}
Falls du nicht mehr auf der Warteliste bleiben möchtest, musst du nichts machen. Du wirst automatisch entfernt.
Viele Grüße
Deine JDAV Ludwigsburg"""

@ -0,0 +1,10 @@
from celery import shared_task
from django.utils import timezone
from django.conf import settings
from .models import MemberWaitingList
@shared_task
def ask_for_waiting_confirmation():
cutoff = timezone.now() - timezone.timedelta(days=settings.WAITING_CONFIRMATION_FREQUENCY)
for waiter in MemberWaitingList.objects.filter(last_wait_confirmation__lte=cutoff):
waiter.ask_for_wait_confirmation()

@ -0,0 +1,21 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Waiting confirmation failed" %}
{% endblock %}
{% block content %}
<h1>{% trans "Waiting confirmation failed" %}</h1>
{% if expired %}
{% url 'members:register_waiting_list' as reg_wait_link %}
<p>{% blocktrans %}Unfortunately, you did not confirm your intention to stay on the waiting list in time. You lost your spot on the list. You can <a href="{{reg_wait_link}}">rejoin the waiting list</a>.{% endblocktrans %}
</p>
{% else %}
<p>{% trans "The supplied link is invalid." %}</p>
{% endif %}
{% endblock %}

@ -0,0 +1,23 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Waiting confirmed" %}
{% endblock %}
{% block content %}
<h1>{% trans "Waiting confirmed" %}</h1>
{% if already_confirmed %}
<p>{% blocktrans %}Thank you {{prename}} for your interest in staying on the waiting list.
Your spot was already confirmed.{% endblocktrans %}
</p>
{% else %}
<p>{% blocktrans %}Thank you {{prename}} for your interest in staying on the waiting list.
Your spot has been confirmed.{% endblocktrans %}
</p>
{% endif %}
{% endblock %}

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

@ -278,3 +278,38 @@ def render_invited_registration_failed(request, reason=""):
if reason:
context['reason'] = reason
return render(request, 'members/invited_registration_failed.html', context)
def confirm_waiting(request):
if request.method == 'GET' and 'key' in request.GET:
key = request.GET['key']
try:
waiter = MemberWaitingList.objects.get(wait_confirmation_key=key)
except MemberWaitingList.DoesNotExist:
return render_waiting_confirmation_invalid(request)
status = waiter.confirm_waiting(key)
if status == MemberWaitingList.WAITING_CONFIRMATION_SUCCESS:
return render_waiting_confirmation_success(request,
waiter.prename,
already_confirmed=False)
elif status == MemberWaitingList.WAITING_CONFIRMED:
return render_waiting_confirmation_success(request,
waiter.prename,
already_confirmed=True)
elif status == MemberWaitingList.WAITING_CONFIRMATION_EXPIRED:
return render_waiting_confirmation_invalid(request, prename=waiter.prename, expired=True)
else:
# invalid
return render_waiting_confirmation_invalid(request)
return HttpResponseRedirect(reverse('startpage:index'))
def render_waiting_confirmation_invalid(request, prename=None, expired=False):
return render(request,
'members/waiting_confirmation_invalid.html',
{'expired': expired, 'prename': prename})
def render_waiting_confirmation_success(request, prename, already_confirmed):
return render(request, 'members/waiting_confirmation_success.html',
{'prename': prename, 'already_confirmed': already_confirmed})

@ -5,6 +5,6 @@ source venv/bin/activate
cd jdav_web
celery -A jdav_web worker -l debug &
celery -A jdav_web worker -B --scheduler django_celery_beat.schedulers:DatabaseScheduler -l info &
python3 manage.py runserver 8008

Loading…
Cancel
Save