members: enter and edit emergency contacts on registration and echo
gitea/kompass/pipeline/head There was a failure building this commit Details

individual-sender-address
Christian Merten 1 year ago
parent cdab970bfc
commit be1f471044
Signed by: christian.merten
GPG Key ID: D953D69721B948B3

@ -81,7 +81,11 @@ kurze Bestätigung von dir. Dafür besuche einfach diesen Link:
{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 30 Tagen deine Daten bestätigst, wirst du aus unserer Datenbank gelöscht und
erhälst in Zukunft keine Mails mehr von uns. erhälst in Zukunft keine Mails mehr von uns.

@ -10,6 +10,10 @@ SEKTION_BOARD_MAIL = "vorstand@alpenverein-heidelberg.de"
RESPONSIBLE_MAIL = "jugendreferat@jdav-hd.de" RESPONSIBLE_MAIL = "jugendreferat@jdav-hd.de"
# echo
ECHO_PASSWORD_BIRTHDATE_FORMAT = '%d.%m.%Y'
# misc # misc
CONGRATULATE_MEMBERS_MAX = 10 CONGRATULATE_MEMBERS_MAX = 10

@ -107,7 +107,8 @@ class EmergencyContactInline(CommonAdminInlineMixin, admin.TabularInline):
formfield_overrides = { formfield_overrides = {
TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} 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 extra = 0
@ -255,13 +256,18 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
class MemberUnconfirmedAdmin(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', 'town', 'phone_number', 'birth_date', 'gender', 'group',
'registration_form', 'comments'] 'registration_form', 'comments']
list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_alternative_mail') list_display = ('name', 'birth_date', 'age', 'get_group', 'confirmed_mail', 'confirmed_alternative_mail')
search_fields = ('prename', 'lastname', 'email') search_fields = ('prename', 'lastname', 'email')
list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail') list_filter = ('group', 'confirmed_mail', 'confirmed_alternative_mail')
readonly_fields = ['confirmed_mail', 'confirmed_alternative_mail']
actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter'] actions = ['request_mail_confirmation', 'confirm', 'demote_to_waiter']
inlines = [EmergencyContactInline]
change_form_template = "members/change_member_unconfirmed.html" change_form_template = "members/change_member_unconfirmed.html"
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):

@ -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'),
),
]

@ -85,7 +85,7 @@ class Contact(CommonModel):
lastname = models.CharField(max_length=20, verbose_name=_('last name')) lastname = models.CharField(max_length=20, verbose_name=_('last name'))
email = models.EmailField(max_length=100, default="") 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="") confirm_mail_key = models.CharField(max_length=32, default="")
class Meta(CommonModel.Meta): class Meta(CommonModel.Meta):
@ -147,9 +147,12 @@ def confirm_mail_by_key(key):
matching_unconfirmed = MemberUnconfirmedProxy.objects.filter(confirm_mail_key=key) \ matching_unconfirmed = MemberUnconfirmedProxy.objects.filter(confirm_mail_key=key) \
| MemberUnconfirmedProxy.objects.filter(confirm_alternative_mail_key=key) | MemberUnconfirmedProxy.objects.filter(confirm_alternative_mail_key=key)
matching_waiter = MemberWaitingList.objects.filter(confirm_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 return None
person = matching_unconfirmed[0] if len(matching_unconfirmed) == 1 else matching_waiter[0] person = matches[0]
return person, person.confirm_mail(key) return person, person.confirm_mail(key)
@ -310,6 +313,10 @@ class Member(Person):
def may_echo(self, key): def may_echo(self, key):
return self.echo_key == key and timezone.now() < self.echo_expire 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 @property
def contact_phone_number(self): def contact_phone_number(self):
"""Synonym for phone number field.""" """Synonym for phone number field."""
@ -387,15 +394,26 @@ class Member(Person):
self.confirmed_alternative_mail = False self.confirmed_alternative_mail = False
self.save() 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() self.notify_jugendleiters_about_confirmed_mail()
if waiter: if waiter:
waiter.delete() waiter.delete()
return self.request_mail_confirmation(rerequest=False) 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): def confirm_mail(self, key):
ret = super().confirm_mail(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() self.notify_jugendleiters_about_confirmed_mail()
return ret return ret

@ -18,13 +18,6 @@
<p><b>{{ error_message }}</b></p> <p><b>{{ error_message }}</b></p>
{% endif %} {% endif %}
<form action="" method="post"> {% include "members/member_form.html" %}
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<input type="hidden" name="key" value="{{ key }}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %} {% endblock %}

@ -0,0 +1,27 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p>{% blocktrans %}Thanks for echoing back. Please enter the password, which you can find in the email we sent you.
{% endblocktrans %}</p>
{% if error_message %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post">
{% csrf_token %}
<input type="password" name="password" required>
<input type="hidden" name="key" value="{{key}}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %}

@ -0,0 +1,15 @@
{% extends "members/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}
{% trans "Echo" %}
{% endblock %}
{% block content %}
<h1>{% trans "Echo" %}</h1>
<p>{% trans "You entered a wrong password to often." %}</p>
{% endblock %}

@ -0,0 +1,97 @@
{% load i18n %}
{% load static %}
{% if error_message %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post" enctype="multipart/form-data">
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<p><b>{% trans "Emergency contacts:" %}</b></p>
{{emergency_contacts_formset.non_form_errors}}
{{emergency_contacts_formset.management_form}}
<div id="formset-container">
{% for form in emergency_contacts_formset.forms %}
<div class="form-row" {% if form.DELETE.value %}style="display:none"{% endif %}>
{{form}}
<button type="button" class="remove-form-button" onclick="setHidden(this)">Remove</button>
</div>
{% endfor %}
</div>
<button type="button" id="add-form-button">Add More</button>
{% if registration %}
<p>
<input type="checkbox" required>
{% blocktrans %}I am already or will become a member of the DAV {{ sektion }} soon.{% endblocktrans %}<br>
<input type="checkbox" required>
{% blocktrans %}I agree that my data is stored and processed on the server of the JDAV {{ sektion }}.{% endblocktrans %}
</p>
{% endif %}
<input type="hidden" name="password" value="{{ password }}">
<input type="hidden" name="waiter_key" value="{{ waiter_key }}">
<input type="hidden" name="save">
<input type="hidden" name="key" value="{{ key }}">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
<div id="empty_form" class="form-row" style="display:none">
{{ emergency_contacts_formset.empty_form }}
<button type="button" id="empty_remove_form_button" class="remove-form-button"
onclick="setHidden(this)">Remove</button>
</div>
<script>
function addRequired(element) {
var inputs = element.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
inputs[i].setAttribute('required', 'required');
}
}
function removeRequired(element) {
var inputs = element.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
inputs[i].removeAttribute('required');
}
}
var divs = document.getElementsByClassName('form-row');
for (var i = 0; i < divs.length; i++) {
if (divs[i].getAttribute('style') == 'display:none') {
removeRequired(divs[i]);
}
}
function setHidden(element) {
element.parentElement.setAttribute('style', 'display:none');
var deleteInput = element.parentElement.querySelector(':scope > [id*=DELETE]');
deleteInput.value = "on";
removeRequired(element.parentElement);
}
var form_count = {{emergency_contacts_formset.total_form_count}};
const addFormButton = document.getElementById('add-form-button');
const formsetContainer = document.getElementById('formset-container');
const emptyForm = document.getElementById('empty_form');
// add required flags to emptyForm
addRequired(emptyForm);
addFormButton.addEventListener('click', () => {
form_count++;
// Clone the template node
const newForm = emptyForm.cloneNode(true);
newForm.removeAttribute('style');
newForm.removeAttribute('id');
// replace the __prefix__ placeholder by the correct number
var res = newForm.innerHTML.replace(/__prefix__/g, form_count - 1);
newForm.innerHTML = res;
// Append the new form
formsetContainer.appendChild(newForm);
// Update total_forms
const totalFormsInput = document.getElementById('id_emergencycontact_set-TOTAL_FORMS');
totalFormsInput.value = parseInt(form_count);
});
</script>

@ -14,25 +14,6 @@
<p>{% trans "Here you can register for group" %} {{ pwd.group.name }}.</p> <p>{% trans "Here you can register for group" %} {{ pwd.group.name }}.</p>
{% if error_message %} {% include "members/member_form.html" %}
<p><b>{{ error_message }}</b></p>
{% endif %}
<form action="" method="post" enctype="multipart/form-data">
<table class="termine">
{% csrf_token %}
{{form}}
</table>
<p>
<input type="checkbox" required>
{% blocktrans %}I am member of the DAV {{ sektion }}.{% endblocktrans %}<br>
<input type="checkbox" required>
{% blocktrans %}I agree that my data is stored and processed on the server of the JDAV {{ sektion }}.{% endblocktrans %}
</p>
<input type="hidden" name="password" value="{{ pwd.password }}">
<input type="hidden" name="waiter_key" value="{{ waiter_key }}">
<input type="hidden" name="save">
<p><input type="submit" value="{% trans "submit" %}"/></p>
</form>
{% endblock %} {% endblock %}

@ -1,9 +1,10 @@
from startpage.views import render from startpage.views import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.http import HttpResponseRedirect 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,\ 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.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
@ -13,10 +14,7 @@ class MemberForm(ModelForm):
class Meta: class Meta:
model = Member model = Member
fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', 'country', fields = ['prename', 'lastname', 'street', 'plz', 'town', 'address_extra', 'country',
'phone_number', 'birth_date'] 'phone_number']
widgets = {
'birth_date': DateInput(format='%d.%m.%Y', attrs={'class': 'datepicker'})
}
class MemberRegistrationForm(ModelForm): class MemberRegistrationForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -52,6 +50,42 @@ class MemberRegistrationWaitingListForm(ModelForm):
required = [] 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=""): def render_echo_failed(request, reason=""):
context = {} context = {}
if reason: if reason:
@ -59,9 +93,13 @@ def render_echo_failed(request, reason=""):
return render(request, 'members/echo_failed.html', context) return render(request, 'members/echo_failed.html', context)
def render_echo(request, key, form): def render_echo(request, key, password, form, emergency_contacts_formset):
return render(request, 'members/echo.html', {'form': form.as_table(), return render(request, 'members/echo.html',
'key' : key}) {'form': form.as_table(),
'emergency_contacts_formset': emergency_contacts_formset,
'key' : key,
'registration': False,
'password': password})
def render_echo_success(request, name): def render_echo_success(request, name):
@ -69,27 +107,38 @@ def render_echo_success(request, name):
def echo(request): def echo(request):
if request.method == 'GET' and 'key' in request.GET: 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: try:
key = request.GET['key']
member = Member.objects.get(echo_key=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: except Member.DoesNotExist:
return render_echo_failed(request, _("invalid")) return render_echo_failed(request, _("invalid"))
except KeyError: # check if echo key is not expired
return render_echo_failed(request, _("expired"))
elif request.method == 'POST':
try:
key = request.POST['key']
member = Member.objects.get(echo_key=key)
if not member.may_echo(key): if not member.may_echo(key):
raise KeyError 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) form = MemberForm(request.POST, instance=member)
emergency_contacts_formset = EmergencyContactsFormSet(request.POST, instance=member)
try: try:
if not emergency_contacts_formset.is_valid():
raise ValueError(_("Invalid emergency contacts"))
form.save() form.save()
emergency_contacts_formset.save()
member.echo_key, member.echo_expire = "", timezone.now() member.echo_key, member.echo_expire = "", timezone.now()
member.echoed = True member.echoed = True
member.save() member.save()
@ -97,9 +146,12 @@ def echo(request):
except ValueError: except ValueError:
# when input is invalid # when input is invalid
form = MemberForm(request.POST) form = MemberForm(request.POST)
return render_echo(request, key, form) emergency_contacts_formset = EmergencyContactsFormSet(request.POST)
except (Member.DoesNotExist, KeyError): return render_echo(request, key, password, form, emergency_contacts_formset)
return render_echo_failed(request, _("invalid")) 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): 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}) '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: if form is None:
form = MemberRegistrationForm() form = MemberRegistrationForm()
if emergency_contacts_formset is None:
emergency_contacts_formset = EmergencyContactsFormSet()
return render(request, return render(request,
'members/register.html', 'members/register.html',
{'form': form, {'form': form,
'emergency_contacts_formset': emergency_contacts_formset,
'group': group, 'group': group,
'waiter_key': waiter_key, 'waiter_key': waiter_key,
'pwd': pwd, 'pwd': pwd,
'sektion': settings.SEKTION, 'sektion': settings.SEKTION,
'registration': True
}) })
@ -172,15 +229,27 @@ def register(request):
if "save" in request.POST: if "save" in request.POST:
# process registration # process registration
form = MemberRegistrationForm(request.POST, request.FILES) form = MemberRegistrationForm(request.POST, request.FILES)
emergency_contacts_formset = EmergencyContactsFormSet(request.POST)
try: 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) needs_mail_confirmation = new_member.create_from_registration(waiter, group)
return render_register_success(request, group.name, new_member.prename, needs_mail_confirmation) 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 # 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 # 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): def confirm_mail(request):

Loading…
Cancel
Save