From be1f4710440c8a6efe0dbf074af985b1bea16a7f Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Tue, 15 Oct 2024 23:08:14 +0200 Subject: [PATCH] members: enter and edit emergency contacts on registration and echo --- .../jdav_web/settings/components/texts.py | 6 +- jdav_web/jdav_web/settings/local.py | 4 + jdav_web/members/admin.py | 10 +- ...mergencycontact_confirmed_mail_and_more.py | 28 ++++ jdav_web/members/models.py | 28 +++- jdav_web/members/templates/members/echo.html | 9 +- .../templates/members/echo_password.html | 27 +++ .../members/echo_wrong_password.html | 15 ++ .../templates/members/member_form.html | 97 +++++++++++ .../members/templates/members/register.html | 21 +-- jdav_web/members/views.py | 157 +++++++++++++----- 11 files changed, 322 insertions(+), 80 deletions(-) create mode 100644 jdav_web/members/migrations/0016_alter_emergencycontact_confirmed_mail_and_more.py create mode 100644 jdav_web/members/templates/members/echo_password.html create mode 100644 jdav_web/members/templates/members/echo_wrong_password.html create mode 100644 jdav_web/members/templates/members/member_form.html diff --git a/jdav_web/jdav_web/settings/components/texts.py b/jdav_web/jdav_web/settings/components/texts.py index dce07cc..a89356c 100644 --- a/jdav_web/jdav_web/settings/components/texts.py +++ b/jdav_web/jdav_web/settings/components/texts.py @@ -81,7 +81,11 @@ kurze Bestätigung von dir. Dafür besuche einfach diesen Link: {link} -Dort kannst du deine Daten überprüfen und ändern. Falls du nicht innerhalb von +Dort kannst du deine Daten nach Eingabe eines Passworts überprüfen und ändern. Dein +Passwort ist dein Geburtsdatum. Ist dein Geburtsdatum zum Beispiel der 4. Januar 1942, +so ist dein Passwort: 04.01.1942 + +Falls du nicht innerhalb von 30 Tagen deine Daten bestätigst, wirst du aus unserer Datenbank gelöscht und erhälst in Zukunft keine Mails mehr von uns. diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py index abf3117..8cc0c79 100644 --- a/jdav_web/jdav_web/settings/local.py +++ b/jdav_web/jdav_web/settings/local.py @@ -10,6 +10,10 @@ SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de" RESPONSIBLE_MAIL = "jugendreferat@jdav-hd.de" +# echo + +ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y' + # misc CONGRATULATE_MEMBERS_MAX = 10 diff --git a/jdav_web/members/admin.py b/jdav_web/members/admin.py index fb2f534..d6fef57 100644 --- a/jdav_web/members/admin.py +++ b/jdav_web/members/admin.py @@ -107,7 +107,8 @@ class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline): formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } - fields = ['prename', 'lastname', 'email', 'phone_number'] + fields = ['prename', 'lastname', 'email', 'phone_number', 'confirmed_mail'] + readonly_fields = ['confirmed_mail'] extra = 0 @@ -255,13 +256,18 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): class MemberUnconfirmedAdmin(admin.ModelAdmin): - fields = ['prename', 'lastname', 'email', 'alternative_email', 'street', 'plz', + fields = ['prename', 'lastname', + ('email', 'confirmed_mail'), + ('alternative_email', 'confirmed_alternative_mail'), + 'street', 'plz', 'town', 'phone_number', 'birth_date', 'gender', 'group', 'registration_form', 'comments'] list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_alternative_mail') search_fields = ('prename', 'lastname', 'email') list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail') + readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail'] actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter'] + inlines = [EmergencyContactInline] change_form_template = "members/change_member_unconfirmed.html" def has_add_permission(self, request, obj=None): diff --git a/jdav_web/members/migrations/0016_alter_emergencycontact_confirmed_mail_and_more.py b/jdav_web/members/migrations/0016_alter_emergencycontact_confirmed_mail_and_more.py new file mode 100644 index 0000000..8727b0f --- /dev/null +++ b/jdav_web/members/migrations/0016_alter_emergencycontact_confirmed_mail_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.1 on 2024-10-15 21:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0015_alter_emergencycontact_lastname_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='emergencycontact', + name='confirmed_mail', + field=models.BooleanField(default=False, verbose_name='Email confirmed'), + ), + migrations.AlterField( + model_name='member', + name='confirmed_mail', + field=models.BooleanField(default=False, verbose_name='Email confirmed'), + ), + migrations.AlterField( + model_name='memberwaitinglist', + name='confirmed_mail', + field=models.BooleanField(default=False, verbose_name='Email confirmed'), + ), + ] diff --git a/jdav_web/members/models.py b/jdav_web/members/models.py index c81af89..0ff9c1c 100644 --- a/jdav_web/members/models.py +++ b/jdav_web/members/models.py @@ -85,7 +85,7 @@ class Contact(CommonModel): lastname = models.CharField(max_length=20, verbose_name=_('last name')) email = models.EmailField(max_length=100, default="") - confirmed_mail = models.BooleanField(default=True, verbose_name=_('Email confirmed')) + confirmed_mail = models.BooleanField(default=False, verbose_name=_('Email confirmed')) confirm_mail_key = models.CharField(max_length=32, default="") class Meta(CommonModel.Meta): @@ -147,9 +147,12 @@ def confirm_mail_by_key(key): matching_unconfirmed = MemberUnconfirmedProxy.objects.filter(confirm_mail_key=key) \ | MemberUnconfirmedProxy.objects.filter(confirm_alternative_mail_key=key) matching_waiter = MemberWaitingList.objects.filter(confirm_mail_key=key) - if len(matching_unconfirmed) + len(matching_waiter) != 1: + matching_emergency_contact = EmergencyContact.objects.filter(confirm_mail_key=key) + matches = list(matching_unconfirmed) + list(matching_waiter) + list(matching_emergency_contact) + # if not exactly one match, return None. The case > 1 match should not occur! + if len(matches) != 1: return None - person = matching_unconfirmed[0] if len(matching_unconfirmed) == 1 else matching_waiter[0] + person = matches[0] return person, person.confirm_mail(key) @@ -310,6 +313,10 @@ class Member(Person): def may_echo(self, key): return self.echo_key == key and timezone.now() < self.echo_expire + @property + def echo_password(self): + return self.birth_date.strftime(settings.ECHO_PASSWORD_BIRTHDATE_FORMAT) + @property def contact_phone_number(self): """Synonym for phone number field.""" @@ -387,15 +394,26 @@ class Member(Person): self.confirmed_alternative_mail = False self.save() - if self.confirmed_alternative_mail and self.confirmed_mail and not self.confirmed: + if self.registration_ready(): self.notify_jugendleiters_about_confirmed_mail() if waiter: waiter.delete() return self.request_mail_confirmation(rerequest=False) + def registration_ready(self): + """Returns if the member is currently unconfirmed and all email addresses + are confirmed.""" + return not self.confirmed and self.confirmed_alternative_mail and self.confirmed_mail and\ + all([emc.confirmed_mail for emc in self.emergencycontact_set.all()]) + + def request_mail_confirmation(self, rerequest=False): + ret = super().request_mail_confirmation(rerequest) + rets = [emc.request_mail_confirmation(rerequest) for emc in self.emergencycontact_set.all()] + return ret or any(rets) + def confirm_mail(self, key): ret = super().confirm_mail(key) - if self.confirmed_alternative_mail and self.confirmed_mail and not self.confirmed: + if self.registration_ready(): self.notify_jugendleiters_about_confirmed_mail() return ret diff --git a/jdav_web/members/templates/members/echo.html b/jdav_web/members/templates/members/echo.html index e968d51..b12b38f 100644 --- a/jdav_web/members/templates/members/echo.html +++ b/jdav_web/members/templates/members/echo.html @@ -18,13 +18,6 @@

{{ error_message }}

{% endif %} -
- - {% csrf_token %} - {{form}} -
- -

-
+{% include "members/member_form.html" %} {% endblock %} diff --git a/jdav_web/members/templates/members/echo_password.html b/jdav_web/members/templates/members/echo_password.html new file mode 100644 index 0000000..d220c99 --- /dev/null +++ b/jdav_web/members/templates/members/echo_password.html @@ -0,0 +1,27 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Echo" %} +{% endblock %} + +{% block content %} + +

{% trans "Echo" %}

+ +

{% blocktrans %}Thanks for echoing back. Please enter the password, which you can find in the email we sent you. +{% endblocktrans %}

+ +{% if error_message %} +

{{ error_message }}

+{% endif %} + +
+ {% csrf_token %} + + +

+
+ +{% endblock %} diff --git a/jdav_web/members/templates/members/echo_wrong_password.html b/jdav_web/members/templates/members/echo_wrong_password.html new file mode 100644 index 0000000..1f60b3c --- /dev/null +++ b/jdav_web/members/templates/members/echo_wrong_password.html @@ -0,0 +1,15 @@ +{% extends "members/base.html" %} +{% load i18n %} +{% load static %} + +{% block title %} +{% trans "Echo" %} +{% endblock %} + +{% block content %} + +

{% trans "Echo" %}

+ +

{% trans "You entered a wrong password to often." %}

+ +{% endblock %} diff --git a/jdav_web/members/templates/members/member_form.html b/jdav_web/members/templates/members/member_form.html new file mode 100644 index 0000000..4280557 --- /dev/null +++ b/jdav_web/members/templates/members/member_form.html @@ -0,0 +1,97 @@ +{% load i18n %} +{% load static %} + +{% if error_message %} +

{{ error_message }}

+{% endif %} + +
+ + {% csrf_token %} + {{form}} +
+

{% trans "Emergency contacts:" %}

+ {{emergency_contacts_formset.non_form_errors}} + {{emergency_contacts_formset.management_form}} +
+ {% for form in emergency_contacts_formset.forms %} +
+ {{form}} + +
+ {% endfor %} +
+ + {% if registration %} +

+ + {% blocktrans %}I am already or will become a member of the DAV {{ sektion }} soon.{% endblocktrans %}
+ + {% blocktrans %}I agree that my data is stored and processed on the server of the JDAV {{ sektion }}.{% endblocktrans %} +

+ {% endif %} + + + + +

+
+ + + + diff --git a/jdav_web/members/templates/members/register.html b/jdav_web/members/templates/members/register.html index 92b5608..7eb806d 100644 --- a/jdav_web/members/templates/members/register.html +++ b/jdav_web/members/templates/members/register.html @@ -14,25 +14,6 @@

{% trans "Here you can register for group" %} {{ pwd.group.name }}.

-{% if error_message %} -

{{ error_message }}

-{% endif %} - -
- - {% csrf_token %} - {{form}} -
-

- - {% blocktrans %}I am member of the DAV {{ sektion }}.{% endblocktrans %}
- - {% blocktrans %}I agree that my data is stored and processed on the server of the JDAV {{ sektion }}.{% endblocktrans %} -

- - - -

-
+{% include "members/member_form.html" %} {% endblock %} diff --git a/jdav_web/members/views.py b/jdav_web/members/views.py index 722415d..c21f448 100644 --- a/jdav_web/members/views.py +++ b/jdav_web/members/views.py @@ -1,9 +1,10 @@ from startpage.views import render from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect -from django.forms import ModelForm, TextInput, DateInput +from django.forms import ModelForm, TextInput, DateInput, BaseInlineFormSet,\ + inlineformset_factory, HiddenInput from members.models import Member, RegistrationPassword, MemberUnconfirmedProxy, MemberWaitingList, Group,\ - confirm_mail_by_key + confirm_mail_by_key, EmergencyContact from django.urls import reverse from django.utils import timezone from django.conf import settings @@ -13,10 +14,7 @@ class MemberForm(ModelForm): class Meta: model = Member fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', 'country', - 'phone_number', 'birth_date'] - widgets = { - 'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'}) - } + 'phone_number'] class MemberRegistrationForm(ModelForm): def __init__(self, *args, **kwargs): @@ -52,6 +50,42 @@ class MemberRegistrationWaitingListForm(ModelForm): required = [] +class EmergencyContactForm(ModelForm): + def __init__(self, *args, **kwargs): + super(EmergencyContactForm, self).__init__(*args, **kwargs) + + for field in self.Meta.required: + self.fields[field].widget.attrs['required'] = 'required' + + class Meta: + model = EmergencyContact + fields = ['prename', 'lastname', 'email', 'phone_number'] + required = ['prename', 'lastname', 'email', 'phone_number'] + + +class BaseEmergencyContactsFormSet(BaseInlineFormSet): + deletion_widget = HiddenInput + + +EmergencyContactsFormSet = inlineformset_factory(Member, EmergencyContact, + form=EmergencyContactForm, fields=['prename', 'lastname', 'email', 'phone_number'], + extra=0, min_num=1, + can_delete=True, can_delete_extra=True, validate_min=True, + formset=BaseEmergencyContactsFormSet) + + +def render_echo_password(request, key): + return render(request, 'members/echo_password.html', + context={'key': key}) + + +def render_echo_wrong_password(request, key): + return render(request, + 'members/echo_password.html', + {'error_message': _("The entered password is wrong."), + 'key': key}) + + def render_echo_failed(request, reason=""): context = {} if reason: @@ -59,9 +93,13 @@ def render_echo_failed(request, reason=""): return render(request, 'members/echo_failed.html', context) -def render_echo(request, key, form): - return render(request, 'members/echo.html', {'form': form.as_table(), - 'key' : key}) +def render_echo(request, key, password, form, emergency_contacts_formset): + return render(request, 'members/echo.html', + {'form': form.as_table(), + 'emergency_contacts_formset': emergency_contacts_formset, + 'key' : key, + 'registration': False, + 'password': password}) def render_echo_success(request, name): @@ -69,37 +107,51 @@ def render_echo_success(request, name): def echo(request): - if request.method == 'GET' and 'key' in request.GET: - try: - key = request.GET['key'] - member = Member.objects.get(echo_key=key) - if not member.may_echo(key): - raise KeyError - form = MemberForm(instance=member) - return render_echo(request, key, form) - except Member.DoesNotExist: - return render_echo_failed(request, _("invalid")) - except KeyError: - return render_echo_failed(request, _("expired")) - elif request.method == 'POST': + if request.method == 'GET' and 'key' not in request.GET: + # invalid + return HttpResponseRedirect(reverse('startpage:index')) + + if request.method == 'GET': + # show password + return render_echo_password(request, request.GET['key']) + + if 'password' not in request.POST or 'key' not in request.POST: + return render_echo_failed(request, _("invalid")) + + key = request.POST['key'] + password = request.POST['password'] + # try to get a member from the supplied echo key + try: + member = Member.objects.get(echo_key=key) + except Member.DoesNotExist: + return render_echo_failed(request, _("invalid")) + # check if echo key is not expired + if not member.may_echo(key): + return render_echo_failed(request, _("expired")) + # check password + if password != member.echo_password: + return render_echo_wrong_password(request, key) + if "save" in request.POST: + form = MemberForm(request.POST, instance=member) + emergency_contacts_formset = EmergencyContactsFormSet(request.POST, instance=member) try: - key = request.POST['key'] - member = Member.objects.get(echo_key=key) - if not member.may_echo(key): - raise KeyError - form = MemberForm(request.POST, instance=member) - try: - form.save() - member.echo_key, member.echo_expire = "", timezone.now() - member.echoed = True - member.save() - return render_echo_success(request, member.prename) - except ValueError: - # when input is invalid - form = MemberForm(request.POST) - return render_echo(request, key, form) - except (Member.DoesNotExist, KeyError): - return render_echo_failed(request, _("invalid")) + if not emergency_contacts_formset.is_valid(): + raise ValueError(_("Invalid emergency contacts")) + form.save() + emergency_contacts_formset.save() + member.echo_key, member.echo_expire = "", timezone.now() + member.echoed = True + member.save() + return render_echo_success(request, member.prename) + except ValueError: + # when input is invalid + form = MemberForm(request.POST) + emergency_contacts_formset = EmergencyContactsFormSet(request.POST) + return render_echo(request, key, password, form, emergency_contacts_formset) + else: + form = MemberForm(instance=member) + emergency_contacts_formset = EmergencyContactsFormSet(instance=member) + return render_echo(request, key, password, form, emergency_contacts_formset) def render_register_password(request): @@ -121,16 +173,21 @@ def render_register_success(request, groupname, membername, needs_mail_confirmat 'needs_mail_confirmation': needs_mail_confirmation}) -def render_register(request, group, form=None, pwd=None, waiter_key=''): +def render_register(request, group, form=None, emergency_contacts_formset=None, + pwd=None, waiter_key=''): if form is None: form = MemberRegistrationForm() + if emergency_contacts_formset is None: + emergency_contacts_formset = EmergencyContactsFormSet() return render(request, 'members/register.html', {'form': form, + 'emergency_contacts_formset': emergency_contacts_formset, 'group': group, 'waiter_key': waiter_key, 'pwd': pwd, 'sektion': settings.SEKTION, + 'registration': True }) @@ -172,15 +229,27 @@ def register(request): if "save" in request.POST: # process registration form = MemberRegistrationForm(request.POST, request.FILES) + emergency_contacts_formset = EmergencyContactsFormSet(request.POST) try: - new_member = form.save() + # first try to save member + new_member = form.save(commit=False) + # then instantiate emergency contacts with this member + emergency_contacts_formset.instance = new_member + if emergency_contacts_formset.is_valid(): + # if emergency contacts are valid, save new_member and save emergency contacts + new_member.save() + emergency_contacts_formset.save() + else: + raise ValueError needs_mail_confirmation = new_member.create_from_registration(waiter, group) return render_register_success(request, group.name, new_member.prename, needs_mail_confirmation) - except ValueError: + except ValueError as e: + print("value error", e) # when input is invalid - return render_register(request, group, form, pwd=pwd, waiter_key=waiter_key) + return render_register(request, group, form, emergency_contacts_formset, pwd=pwd.password, + waiter_key=waiter_key) # we are not saving yet - return render_register(request, group, form=None, pwd=pwd, waiter_key=waiter_key) + return render_register(request, group, form=None, pwd=pwd.password, waiter_key=waiter_key) def confirm_mail(request):