Merge pull request 'Add trashbin for registrations and participants' (#28) from heinzel into master
All checks were successful
buildbot/tox Build done.

Reviewed-on: #28
This commit was merged in pull request #28.
This commit is contained in:
2020-12-08 16:05:43 +01:00
13 changed files with 559 additions and 109 deletions

View File

@@ -114,7 +114,9 @@
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<h4>{% trans 'Teilnehmer' %}</h4> <h4>{% trans 'Teilnehmer' %}</h4>
<div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true">
{% if registrations_support %} {% if registrations_support %}
@@ -131,22 +133,11 @@
<div id="collapseRegistrations" class="panel-collapse collapse" <div id="collapseRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingRegistrations"> role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body"> <div class="panel-body">
{% for registration in registrations_all %} {% for registration in registrations_pending %}
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}"> <input type="hidden" name="registration" value="{{ registration.id }}">
{% if registration.answered %} {% if has_permission_update_participants %}
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="text-muted">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="text-muted">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
{% elif has_permission_update_participants %}
<button type="submit" name="action" value="accept_registration" <button type="submit" name="action" value="accept_registration"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen"> class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span> <span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
@@ -158,9 +149,6 @@
</button> </button>
&nbsp; &nbsp;
{% endif %} {% endif %}
{% if registration.answered %}
<s class="text-muted">
{% endif %}
{{ registration.get_full_name }} {{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>, (<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }}) {{ registration.phone_number }})
@@ -173,10 +161,6 @@
<span title="{{ registration.get_info }}"> <span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %} {% bootstrap_icon 'info-sign' %}
</span> </span>
&nbsp;
{% if registration.answered %}
</s>
{% endif %}
</form> </form>
{% empty %} {% empty %}
{% trans 'Keine Anmeldungen vorhanden' %} {% trans 'Keine Anmeldungen vorhanden' %}
@@ -191,13 +175,16 @@
{% with position=participant.position %} {% with position=participant.position %}
<div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}"> <div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}">
<div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab"> <div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab">
<h5 class="panel-title"> <div>
<a role="button" href="#collapseParticipant_{{ participant.id }}" <strong><span class="panel-title">
data-toggle="collapse" <a role="button" href="#collapseParticipant_{{ participant.id }}"
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}"> data-toggle="collapse"
<span class="caret"></span>&nbsp;&nbsp; aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
{{ position }}. {{ participant.get_full_name }} <span class="caret"></span>&nbsp;&nbsp;
</a> {{ position }}. {{ participant.get_full_name }}
</a>
</span></strong>
&nbsp;
<small> <small>
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }}) (<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }})
</small> </small>
@@ -207,6 +194,12 @@
{% else %} {% else %}
{% trans 'Nicht Mitglied' %} {% trans 'Nicht Mitglied' %}
{% endif %} {% endif %}
&nbsp;
<span class="text-info"
title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
<div class="pull-right"> <div class="pull-right">
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
@@ -222,31 +215,33 @@
class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}"> class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span> <span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button> </button>
<button name="action" value="remove_participant" <button name="action" value="trash_participant"
title="{% trans 'Teilnehmer jetzt löschen' %} title="{% trans 'Eintrag in Papierkorb verschieben' %}"
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y' }})"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span> <span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button> </button>
&nbsp;
{% endif %} {% endif %}
{% if event.charge and participant.paid and has_permission_payment %} {% if event.charge and participant.paid and has_permission_payment %}
&nbsp;
<button name="action" value="revoke_payment" <button name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}" title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span> <span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button> </button>
{% elif event.charge and participant.paid %} {% elif event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}"> <span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %} {% bootstrap_icon 'piggy-bank' %}
</span> </span>
{% elif event.charge and has_permission_payment %} {% elif event.charge and has_permission_payment %}
&nbsp;
<button name="action" value="confirm_payment" <button name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}" title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span> <span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button> </button>
{% elif event.charge %} {% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}"> <span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %} {% bootstrap_icon 'piggy-bank' %}
</span> </span>
@@ -257,7 +252,7 @@
{% endif %} {% endif %}
</form> </form>
</div> </div>
</h5> </div>
</div> </div>
<div id="collapseParticipant_{{ participant.id }}" <div id="collapseParticipant_{{ participant.id }}"
class="panel-collapse collapse {% if form.errors %}in{% endif %}" class="panel-collapse collapse {% if form.errors %}in{% endif %}"
@@ -297,6 +292,110 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<hr />
<h4>{% trans 'Papierkorb' %}</h4>
<div class="panel-group" id="form-accordion-trash" role="tablist" aria-multiselectable="true">
{% if registrations_support and registrations_answered %}
<div class="panel panel-info">
<div id="headingAnsweredRegistrations" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseAnsweredRegistrations"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseAnsweredRegistrations">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Bearbeitete Anmeldungen' %}
</a>
</h5>
</div>
<div id="collapseAnsweredRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingAnsweredRegistrations">
<div class="panel-body">
{% for registration in registrations_answered %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="{% if registration.status.accepted %}text-success{% else %}text-muted{% endif %}">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="{% if not registration.status.accepted %}text-danger{% else %}text-muted{% endif %}">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
&nbsp;
<span title="Anmeldezeitpunkt">
{% bootstrap_icon 'time' %}
{{ registration.created_at|date:'d. F Y, G:i' }}
</span>
&nbsp;
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</span>
</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if participants_trash %}
<div class="panel panel-info">
<div id="headingTrashedParticipants" class="panel-heading" role="tab">
<h5 class="panel-title">
<a role="button" href="#collapseTrashedParticipants"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseTrashedParticipants">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Gelöschte Teilnehmer' %}
</a>
</h5>
</div>
<div id="collapseTrashedParticipants" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingTrashedParticipants">
<div class="panel-body">
{% for participant in participants_trash %}
<div>
<span class="text-muted">
{{ participant.get_full_name }}
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>,
{{ participant.phone_number }})
&nbsp;
{% if participant.dav_member %}
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
{% else %}
{% trans 'Nicht Mitglied' %}
{% endif %}
&nbsp;
<span title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
{% if event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% endif %}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if not registrations_answered and not participants_trash %}
<span class="text-muted small">{% trans 'Der Papierkorb ist leer.' %}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endblock page-container-fluid %} {% endblock page-container-fluid %}

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import EventStatus, EventFlag, Event, OneClickAction, Participant from .models import EventStatus, EventFlag, Event, OneClickAction, Participant, TrashedParticipant
@admin.register(EventStatus) @admin.register(EventStatus)
@@ -31,3 +31,8 @@ class OneClickActionAdmin(admin.ModelAdmin):
@admin.register(Participant) @admin.register(Participant)
class ParticipantAdmin(admin.ModelAdmin): class ParticipantAdmin(admin.ModelAdmin):
pass pass
@admin.register(TrashedParticipant)
class TrashedParticipantAdmin(admin.ModelAdmin):
pass

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-03 10:44
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dav_events', '0035_merge_20201103_1112'),
]
operations = [
migrations.CreateModel(
name='TrashedParticipant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('personal_names', models.CharField(max_length=1024, verbose_name='Vorname(n)')),
('family_names', models.CharField(max_length=1024, verbose_name='Familienname')),
('address', models.CharField(help_text='Straße, Hausnummer', max_length=1024, verbose_name='Anschrift')),
('postal_code', models.CharField(max_length=254, verbose_name='Postleitzahl')),
('city', models.CharField(max_length=1024, verbose_name='Ort')),
('email_address', models.EmailField(max_length=254, verbose_name='E-Mail-Adresse')),
('phone_number', models.CharField(max_length=254, verbose_name='Telefonnummer')),
('dav_member', models.BooleanField(default=True, help_text='In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.', verbose_name='DAV Mitglied')),
('dav_number', models.CharField(blank=True, max_length=62, validators=[django.core.validators.RegexValidator('^([0-9]{1,10}/[0-9]{2,10}/)?[0-9]{1,10}(\\*[0-9]{1,10})?(\\*[0-9]{4}\\*[0-9]{4})?([* ][0-9]{8})?$', 'Ungültiges Format.')], verbose_name='DAV Mitgliedsnummer')),
('emergency_contact', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Notfall-Kontakt')),
('experience', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Erfahrung')),
('note', models.TextField(blank=True, help_text='Kann frei gelassen werden.', verbose_name='Anmerkung')),
('paid', models.BooleanField(default=False, verbose_name='Teilnehmerbeitrag bezahlt')),
('purge_at', models.DateTimeField()),
('created_at', models.DateTimeField()),
('trashed_at', models.DateTimeField(auto_now_add=True)),
('position', models.IntegerField(verbose_name='Listennummer')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trashed_participants', to='dav_events.Event')),
],
options={
'verbose_name': 'Gelöschter Teilnehmer (Papierkorb)',
'verbose_name_plural': 'Gelöschte Teilnehmer (Papierkorb)',
'ordering': ['event', 'trashed_at'],
},
),
]

View File

@@ -5,3 +5,4 @@ from .eventflag import EventFlag
from .eventstatus import EventStatus from .eventstatus import EventStatus
from .oneclickaction import OneClickAction from .oneclickaction import OneClickAction
from .participant import Participant from .participant import Participant
from .trash import TrashedParticipant

View File

@@ -13,12 +13,7 @@ midnight = datetime.time(00, 00, 00)
@python_2_unicode_compatible @python_2_unicode_compatible
class Participant(models.Model): class AbstractParticipant(models.Model):
event = models.ForeignKey('Event', related_name='participants')
created_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
personal_names = models.CharField(max_length=1024, personal_names = models.CharField(max_length=1024,
verbose_name=_('Vorname(n)')) verbose_name=_('Vorname(n)'))
family_names = models.CharField(max_length=1024, family_names = models.CharField(max_length=1024,
@@ -54,17 +49,10 @@ class Participant(models.Model):
purge_at = models.DateTimeField() purge_at = models.DateTimeField()
class Meta: class Meta:
unique_together = (('event', 'position'), ) abstract = True
verbose_name = _('Teilnehmer')
verbose_name_plural = _('Teilnehmer')
ordering = ['event', 'position']
def __str__(self): def __str__(self):
return '{eventnumber} - {position}. {name}'.format( return self.get_full_name()
eventnumber=self.event.get_number(),
position=self.position,
name=self.get_full_name(),
)
def get_full_name(self): def get_full_name(self):
return '{} {}'.format(self.personal_names, self.family_names) return '{} {}'.format(self.personal_names, self.family_names)
@@ -96,6 +84,12 @@ class Participant(models.Model):
note=self.note, note=self.note,
) )
def get_data_dict(self):
data = {}
for field in self._meta.fields:
data[field.name] = getattr(self, field.name)
return data
def clean(self): def clean(self):
if self.dav_member and not self.dav_number: if self.dav_member and not self.dav_number:
raise ValidationError({'dav_number': _('Bei DAV Mitgliedern brauchen wir die Mitgliedsnummer.')}) raise ValidationError({'dav_number': _('Bei DAV Mitgliedern brauchen wir die Mitgliedsnummer.')})
@@ -105,7 +99,7 @@ class Participant(models.Model):
self.purge_at = self.__class__.calc_purge_at(self.event) self.purge_at = self.__class__.calc_purge_at(self.event)
self.full_clean() self.full_clean()
super(Participant, self).save(**kwargs) super(AbstractParticipant, self).save(**kwargs)
@staticmethod @staticmethod
def calc_purge_at(event): def calc_purge_at(event):
@@ -132,3 +126,23 @@ class Participant(models.Model):
purge_date = july_nextyear purge_date = july_nextyear
return timezone.make_aware(datetime.datetime.combine(purge_date, midnight)) return timezone.make_aware(datetime.datetime.combine(purge_date, midnight))
@python_2_unicode_compatible
class Participant(AbstractParticipant):
event = models.ForeignKey('Event', related_name='participants')
created_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
class Meta:
verbose_name = _('Teilnehmer')
verbose_name_plural = _('Teilnehmer')
unique_together = (('event', 'position'), )
ordering = ['event', 'position']
def __str__(self):
return '{eventnumber} - {position}. {name}'.format(
eventnumber=self.event.get_number(),
position=self.position,
name=self.get_full_name(),
)

View File

@@ -0,0 +1 @@
from .trashed_participant import TrashedParticipant

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from ..participant import AbstractParticipant
@python_2_unicode_compatible
class TrashedParticipant(AbstractParticipant):
event = models.ForeignKey('Event', related_name='trashed_participants')
created_at = models.DateTimeField()
trashed_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
class Meta:
verbose_name = _('Gelöschter Teilnehmer (Papierkorb)')
verbose_name_plural = _('Gelöschte Teilnehmer (Papierkorb)')
ordering = ['event', 'trashed_at']
def __str__(self):
return '{eventnumber} - {name}'.format(
eventnumber=self.event.get_number(),
name=self.get_full_name(),
)

View File

@@ -222,6 +222,7 @@
</div> </div>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="pull-right text-info" style="margin-right: 1em;" <div class="pull-right text-info" style="margin-right: 1em;"
title="Sobald du im Kasten 'Anmeldungen' bei einzelnen Personen auf das Plus-Symbol geklickt hast, oder jemanden mit dem Formular unter 'weiteren Teilnehmer hinzufügen' eingetragen hast, erscheinen diese Personen weiter unten als Teilnehmer (graue Kästen). title="Sobald du im Kasten 'Anmeldungen' bei einzelnen Personen auf das Plus-Symbol geklickt hast, oder jemanden mit dem Formular unter 'weiteren Teilnehmer hinzufügen' eingetragen hast, erscheinen diese Personen weiter unten als Teilnehmer (graue Kästen).
@@ -249,7 +250,7 @@ Die Person wird dann automatisch auf die Teilnehmerliste übernommen.
- Wenn du jemanden nicht mitnehmen möchtest, schicke ihm per E-Mail eine Absage und klicke dann auf das Minus-Symbol. - Wenn du jemanden nicht mitnehmen möchtest, schicke ihm per E-Mail eine Absage und klicke dann auf das Minus-Symbol.
Nach einem Klick auf Plus oder Minus werden die entsprechenden Zeilen ausgegraut. Nach einem Klick auf Plus oder Minus werden die entsprechenden Zeilen in den Papierkorb verschoben.
Wichtig: das System verschickt keine Zu- oder Absagen an die Teilnehmer! Wichtig: das System verschickt keine Zu- oder Absagen an die Teilnehmer!
Das musst du selbst (per E-Mail oder telefonisch) machen. Das musst du selbst (per E-Mail oder telefonisch) machen.
"> ">
@@ -266,22 +267,11 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
<div id="collapseRegistrations" class="panel-collapse collapse {% if registrations_pending %}in{% endif %}" <div id="collapseRegistrations" class="panel-collapse collapse {% if registrations_pending %}in{% endif %}"
role="tabpanel" aria-labelledby="headingRegistrations"> role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body"> <div class="panel-body">
{% for registration in registrations_all %} {% for registration in registrations_pending %}
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}"> <input type="hidden" name="registration" value="{{ registration.id }}">
{% if registration.answered %} {% if has_permission_update_participants %}
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="text-muted">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
<span class="text-muted">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
{% elif has_permission_update_participants %}
<button type="submit" name="action" value="accept_registration" <button type="submit" name="action" value="accept_registration"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen"> class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span> <span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
@@ -293,10 +283,6 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
</button> </button>
&nbsp; &nbsp;
{% endif %} {% endif %}
{% if registration.answered %}
<s>
<span class="text-muted">
{% endif %}
{{ registration.get_full_name }} {{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>, (<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }}) {{ registration.phone_number }})
@@ -309,19 +295,9 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
<span title="{{ registration.get_info }}"> <span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %} {% bootstrap_icon 'info-sign' %}
</span> </span>
&nbsp;
{% if registration.answered %}
</span>
</s>
&nbsp;
<span class="text-info" title="Bei dieser Anmeldung hast du bereits auf Plus oder Minus geklickt.
Leider speichert das System hier nicht, welchen der beiden Knöpfe du gewählt hast, aber bei Plus sollte die Person ja weiter unten als Teilnehmer gelistet sein.">
{% bootstrap_icon 'question-sign' %}
</span>
{% endif %}
</form> </form>
{% empty %} {% empty %}
{% trans 'Keine Anmeldungen vorhanden' %} {% trans 'Keine unbearbeiteten Anmeldungen vorhanden' %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -368,16 +344,25 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
{% with position=participant.position %} {% with position=participant.position %}
<div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}"> <div class="panel {% if event.max_participants and position > event.max_participants %}panel-warning{% else %}panel-default{% endif %}">
<div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab"> <div id="headingParticipant_{{ participant.id }}" class="panel-heading" role="tab">
<h5 class="panel-title"> <div>
<a role="button" href="#collapseParticipant_{{ participant.id }}" <strong><span class="panel-title">
data-toggle="collapse" <a role="button" href="#collapseParticipant_{{ participant.id }}"
aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}"> data-toggle="collapse"
<span class="caret"></span>&nbsp;&nbsp; aria-expanded="true" aria-controls="collapseParticipant_{{ participant.id }}">
{{ position }}. {{ participant.get_full_name }} <span class="caret"></span>&nbsp;&nbsp;
</a> {{ position }}. {{ participant.get_full_name }}
</a>
</span></strong>
&nbsp;
<small> <small>
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }}) (<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }})
</small> </small>
&nbsp;
<span class="text-info"
title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
<div class="pull-right"> <div class="pull-right">
<form action="" method="post" class="form-inline"> <form action="" method="post" class="form-inline">
{% csrf_token %} {% csrf_token %}
@@ -393,31 +378,33 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}"> class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span> <span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button> </button>
<button name="action" value="remove_participant" <button name="action" value="trash_participant"
title="{% trans 'Teilnehmer jetzt löschen' %} title="{% trans 'Eintrag in Papierkorb verschieben' %}"
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y'}})"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span> <span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button> </button>
&nbsp;
{% endif %} {% endif %}
{% if event.charge and participant.paid and has_permission_payment %} {% if event.charge and participant.paid and has_permission_payment %}
&nbsp;
<button name="action" value="revoke_payment" <button name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}" title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span> <span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button> </button>
{% elif event.charge and participant.paid %} {% elif event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}"> <span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %} {% bootstrap_icon 'piggy-bank' %}
</span> </span>
{% elif event.charge and has_permission_payment %} {% elif event.charge and has_permission_payment %}
&nbsp;
<button name="action" value="confirm_payment" <button name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}" title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding"> class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span> <span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button> </button>
{% elif event.charge %} {% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}"> <span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %} {% bootstrap_icon 'piggy-bank' %}
</span> </span>
@@ -428,7 +415,7 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
{% endif %} {% endif %}
</form> </form>
</div> </div>
</h5> </div>
</div> </div>
<div id="collapseParticipant_{{ participant.id }}" <div id="collapseParticipant_{{ participant.id }}"
class="panel-collapse collapse {% if form.errors %}in{% endif %}" class="panel-collapse collapse {% if form.errors %}in{% endif %}"
@@ -472,6 +459,157 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
</div> </div>
{% endif %} {% endif %}
</div> </div>
<hr />
<div class="pull-right text-info" style="margin-right: 1em;"
title="Wenn du Anmeldungen mit einem Klick auf das Plus- oder Minus-Symbol 'bearbeitest', oder Teilnehmer aus der Teilnehmerliste entfernst, dann sind diese Einträge danach noch im Papierkorb zu finden.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h4>{% trans 'Papierkorb' %}</h4>
<div class="panel-group" id="form-accordion-trash" role="tablist" aria-multiselectable="true">
{% if registrations_support and registrations_answered %}
<div class="panel panel-info">
<div id="headingAnsweredRegistrations" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Wenn du unter 'Anmeldungen' auf das Plus- oder Minus-Symbol geklickt hast, dann sind diese Einträge hier zu sehen.
">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title">
<a role="button" href="#collapseAnsweredRegistrations"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseAnsweredRegistrations">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Bearbeitete Anmeldungen' %}
</a>
</h5>
</div>
<div id="collapseAnsweredRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingAnsweredRegistrations">
<div class="panel-body">
{% for registration in registrations_answered %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Bei dieser Anmeldung hast du bereits
am {{ registration.status.updated_at|date:'d. F Y, G:i' }}
auf {% if registration.status.accepted %}Plus{% else %}Minus{% endif %} geklickt.
">
<span class="{% if registration.status.accepted %}text-success{% else %}text-muted{% endif %}">{% bootstrap_icon 'plus-sign' %}</span>
</button>
&nbsp;
<button disabled="disabled"
class="btn btn-link no-padding" title="Bei dieser Anmeldung hast du bereits
am {{ registration.status.updated_at|date:'d. F Y, G:i' }}
auf {% if registration.status.accepted %}Plus{% else %}Minus{% endif %} geklickt.
">
<span class="{% if not registration.status.accepted %}text-danger{% else %}text-muted{% endif %}">{% bootstrap_icon 'minus-sign' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
&nbsp;
<span title="Anmeldezeitpunkt">
{% bootstrap_icon 'time' %}
{{ registration.created_at|date:'d. F Y, G:i' }}
</span>
&nbsp;
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</span>
</span>
{% if has_permission_update_registration %}
<div class="pull-right">
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}">
<button type="submit" name="action" value="untrash_registration"
class="btn btn-link no-padding"
title="{% trans 'Eintrag in Anmeldungen zurückholen' %}">
<span class="text-danger">{% bootstrap_icon 'repeat' %}</span>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if participants_trash %}
<div class="panel panel-info">
<div id="headingTrashedParticipants" class="panel-heading" role="tab">
<div class="pull-right text-info" title="Wenn du Teilnehmer deiner Teilnehmerliste hinzugefügt hast und sie später wieder entfernt hast, dann tauchen diese Einträge hier auf.">
{% bootstrap_icon 'question-sign' %}
</div>
<h5 class="panel-title">
<a role="button" href="#collapseTrashedParticipants"
data-toggle="collapse"
aria-expanded="true" aria-controls="collapseTrashedParticipants">
<span class="caret"></span>&nbsp;&nbsp;{% trans 'Gelöschte Teilnehmer' %}
</a>
</h5>
</div>
<div id="collapseTrashedParticipants" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingTrashedParticipants">
<div class="panel-body">
{% for participant in participants_trash %}
<div>
<button disabled="disabled"
class="btn btn-link no-padding" title="Diesen Teilnehmer hast du
am {{ participant.trashed_at|date:'d. F Y, G:i' }}
von Position {{ participant.position }} der Teilnehmerliste entfernt.
">
<span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button>
&nbsp;
<span class="text-muted">
{{ participant.get_full_name }}
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>,
{{ participant.phone_number }})
&nbsp;
<span title="{{ participant.get_info }}
{% trans 'Zeitpunkt der automatischen Löschung' %}: {{ participant.purge_at|date:'d. F Y' }}">
{% bootstrap_icon 'info-sign' %}
</span>
{% if event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% endif %}
</span>
{% if has_permission_update_participants %}
<div class="pull-right">
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="id" value="{{ participant.id }}">
<button name="action" value="untrash_participant"
title="{% trans 'Eintrag in Teilnehmerliste zurückholen' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'repeat' %}</span>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if not registrations_answered and not participants_trash %}
<span class="text-muted small">{% trans 'Der Papierkorb ist leer.' %}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endblock page-container-fluid %} {% endblock page-container-fluid %}

View File

@@ -1,7 +1,7 @@
import logging import logging
from django.utils import timezone from django.utils import timezone
from .models import Participant from .models import Participant, TrashedParticipant
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -11,3 +11,6 @@ def purge_participants():
for p in Participant.objects.filter(purge_at__lte=now): for p in Participant.objects.filter(purge_at__lte=now):
logger.info('Purge participant \'%s\'', p) logger.info('Purge participant \'%s\'', p)
p.delete() p.delete()
for p in TrashedParticipant.objects.filter(purge_at__lte=now):
logger.info('Purge participant from trash \'%s\'', p)
p.delete()

View File

@@ -215,6 +215,8 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
participants = event.participants.all() participants = event.participants.all()
context['participants'] = participants context['participants'] = participants
participants_trash = event.trashed_participants.all()
context['participants_trash'] = participants_trash
if participants.count() > 1: if participants.count() > 1:
email_list = [u'"{}" <{}>'.format(p.get_full_name(), p.email_address) for p in participants] email_list = [u'"{}" <{}>'.format(p.get_full_name(), p.email_address) for p in participants]
@@ -231,9 +233,11 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
context['registrations_support'] = registrations_support context['registrations_support'] = registrations_support
if registrations_support: if registrations_support:
registrations_all = event.registrations.all() registrations_all = event.registrations.all()
registrations_pending = registrations_all.filter(answered=False) registrations_pending = registrations_all.filter(~Q(status__answered=True))
context['registrations_pending'] = registrations_pending registrations_answered = registrations_all.filter(status__answered=True)
context['registrations_all'] = registrations_all context['registrations_all'] = registrations_all
context['registrations_pending'] = registrations_pending
context['registrations_answered'] = registrations_answered
context['registrations'] = registrations_all context['registrations'] = registrations_all
return context return context
@@ -289,13 +293,16 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
'purge_at': registration.purge_at, 'purge_at': registration.purge_at,
} }
participant = models.Participant.objects.create(**data) participant = models.Participant.objects.create(**data)
registration.answered = True registration.accepted()
registration.save()
messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name()))) messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name())))
def _reject_registration(self, registration): def _reject_registration(self, registration):
registration.answered = True registration.rejected()
registration.save()
def _reset_registration(self, registration):
registration.status.accepted = None
registration.status.answered = False
registration.status.save()
def _swap_participants_position(self, participant1, participant2): def _swap_participants_position(self, participant1, participant2):
event = participant1.event event = participant1.event
@@ -345,6 +352,14 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
self._reject_registration(registration) self._reject_registration(registration)
else: else:
raise FieldDoesNotExist('Event has no registrations') raise FieldDoesNotExist('Event has no registrations')
elif action == 'untrash_registration':
self.enforce_permission(event, permission='update-registration')
if hasattr(event, 'registrations'):
registration_id = request.POST.get('registration')
registration = event.registrations.get(id=registration_id)
self._reset_registration(registration)
else:
raise FieldDoesNotExist('Event has no registrations')
elif action == 'confirm_payment': elif action == 'confirm_payment':
self.enforce_permission(event, permission='payment') self.enforce_permission(event, permission='payment')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')
@@ -357,18 +372,33 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
participant = event.participants.get(id=participant_id) participant = event.participants.get(id=participant_id)
participant.paid = False participant.paid = False
participant.save() participant.save()
elif action == 'remove_participant': elif action == 'trash_participant':
self.enforce_permission(event, permission='update-participants') self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')
participant = event.participants.get(id=participant_id) participant = event.participants.get(id=participant_id)
full_name = participant.get_full_name() participants_below = event.participants.filter(position__gt=participant.position)
position = participant.position
data = participant.get_data_dict()
trashed = models.TrashedParticipant.objects.create(**data)
participant.delete() participant.delete()
qs = event.participants.filter(position__gt=position)
for participant in qs: for participant in participants_below:
participant.position -= 1 participant.position -= 1
participant.save() participant.save()
messages.success(request, _(u'Teilnehmer gelöscht: {}'.format(full_name)))
messages.success(request, _(u'Teilnehmer in den Papierkorb verschoben: {}'.format(trashed.get_full_name())))
elif action == 'untrash_participant':
self.enforce_permission(event, permission='update-participants')
trashed_id = request.POST.get('id')
trashed = event.trashed_participants.get(id=trashed_id)
trashed.position = event.participants.count() + 1
data = trashed.get_data_dict()
del data['trashed_at']
participant = models.Participant.objects.create(**data)
trashed.delete()
messages.success(request, _(u'Teilnehmer zurückgeholt: {}'.format(participant.get_full_name())))
elif action == 'moveup_participant': elif action == 'moveup_participant':
self.enforce_permission(event, permission='update-participants') self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id') participant_id = request.POST.get('id')

View File

@@ -1,8 +1,12 @@
from django.contrib import admin from django.contrib import admin
from .models import Registration from .models import Registration, RegistrationStatus
class RegistrationStatusInline(admin.StackedInline):
model = RegistrationStatus
@admin.register(Registration) @admin.register(Registration)
class RegistrationAdmin(admin.ModelAdmin): class RegistrationAdmin(admin.ModelAdmin):
pass inlines = [RegistrationStatusInline]

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-12-03 10:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dav_registration', '0005_auto_20201015_1738'),
]
operations = [
migrations.CreateModel(
name='RegistrationStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True)),
('answered', models.BooleanField(default=False, verbose_name='Durch Tourleitung beantwortet')),
('accepted', models.NullBooleanField(verbose_name='Zusage erteilt')),
],
options={
'verbose_name': 'Anmeldungsstatus',
'verbose_name_plural': 'Anmeldungsstati',
'ordering': ['updated_at'],
},
),
migrations.RemoveField(
model_name='registration',
name='answered',
),
migrations.AddField(
model_name='registrationstatus',
name='registration',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='status', to='dav_registration.Registration'),
),
]

View File

@@ -72,8 +72,6 @@ class Registration(models.Model):
verbose_name=_('Einwilligung zur Datenspeicherung')) verbose_name=_('Einwilligung zur Datenspeicherung'))
purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung')) purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung'))
answered = models.BooleanField(_('Durch Tourleitung beantwortet'), default=False)
@staticmethod @staticmethod
def pk2hexstr(pk): def pk2hexstr(pk):
return hex(pk * 113)[2:] # 113 has no meaning, but it produce nice looking hex codes. return hex(pk * 113)[2:] # 113 has no meaning, but it produce nice looking hex codes.
@@ -151,9 +149,29 @@ Anmerkung:
super(Registration, self).save(**kwargs) super(Registration, self).save(**kwargs)
if creating: if creating:
status = RegistrationStatus(registration=self)
status.save()
logger.info('Registration stored: %s', self) logger.info('Registration stored: %s', self)
signals.registration_created.send(sender=self.__class__, registration=self) signals.registration_created.send(sender=self.__class__, registration=self)
def answered(self, accepted):
if accepted is not True and accepted is not False:
raise ValueError('boolean parameter expected')
if hasattr(self, 'status'):
status = self.status
else:
status = RegistrationStatus(registration=self)
status.accepted = accepted
status.answered = True
status.save()
def accepted(self):
return self.answered(accepted=True)
def rejected(self):
return self.answered(accepted=False)
@classmethod @classmethod
def calc_purge_at(cls, event): def calc_purge_at(cls, event):
if event.alt_last_day: if event.alt_last_day:
@@ -179,3 +197,29 @@ Anmerkung:
purge_date = july_nextyear purge_date = july_nextyear
return timezone.make_aware(datetime.datetime.combine(purge_date, midnight)) return timezone.make_aware(datetime.datetime.combine(purge_date, midnight))
@python_2_unicode_compatible
class RegistrationStatus(models.Model):
registration = models.OneToOneField(Registration, on_delete=models.CASCADE, related_name='status')
updated_at = models.DateTimeField(auto_now=True)
answered = models.BooleanField(_('Durch Tourleitung beantwortet'), default=False)
accepted = models.NullBooleanField(_('Zusage erteilt'))
class Meta:
verbose_name = _('Anmeldungsstatus')
verbose_name_plural = _('Anmeldungsstati')
ordering = ['updated_at']
def __str__(self):
return '{} (Updated: {})'.format(self.registration, self.updated_at.strftime('%d.%m.%Y %H:%M:%S'))
def clean(self):
if self.answered and self.accepted is None:
raise ValidationError({'accepted': 'if answered is true, accepted must not be none'})
elif not self.answered and self.accepted is not None:
raise ValidationError({'answered': 'if answered is false, accepted must be none'})
def save(self, **kwargs):
self.full_clean()
super(RegistrationStatus, self).save(**kwargs)