Add a kind of trashbin for registrations and participants
All checks were successful
buildbot/tox Build done.

This commit is contained in:
2020-12-03 11:47:41 +01:00
parent f2f0df3ab3
commit 94595f4785
13 changed files with 484 additions and 93 deletions

View File

@@ -114,7 +114,9 @@
</div>
</div>
</div>
<hr />
<h4>{% trans 'Teilnehmer' %}</h4>
<div class="panel-group" id="form-accordion-participants" role="tablist" aria-multiselectable="true">
{% if registrations_support %}
@@ -131,22 +133,11 @@
<div id="collapseRegistrations" class="panel-collapse collapse"
role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body">
{% for registration in registrations_all %}
{% for registration in registrations_pending %}
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}">
{% if registration.answered %}
<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 %}
{% if has_permission_update_participants %}
<button type="submit" name="action" value="accept_registration"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
@@ -158,9 +149,6 @@
</button>
&nbsp;
{% endif %}
{% if registration.answered %}
<s class="text-muted">
{% endif %}
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
@@ -173,10 +161,6 @@
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</span>
&nbsp;
{% if registration.answered %}
</s>
{% endif %}
</form>
{% empty %}
{% trans 'Keine Anmeldungen vorhanden' %}
@@ -207,6 +191,12 @@
{% else %}
{% trans 'Nicht Mitglied' %}
{% 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">
<form action="" method="post" class="form-inline">
{% csrf_token %}
@@ -222,31 +212,33 @@
class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button>
<button name="action" value="remove_participant"
title="{% trans 'Teilnehmer jetzt löschen' %}
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y' }})"
<button name="action" value="trash_participant"
title="{% trans 'Eintrag in Papierkorb verschieben' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span>
<span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button>
&nbsp;
{% endif %}
{% if event.charge and participant.paid and has_permission_payment %}
&nbsp;
<button name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding">
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% elif event.charge and has_permission_payment %}
&nbsp;
<button name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
@@ -297,6 +289,110 @@
</div>
{% endif %}
</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>
{% endblock page-container-fluid %}

View File

@@ -1,6 +1,6 @@
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)
@@ -31,3 +31,8 @@ class OneClickActionAdmin(admin.ModelAdmin):
@admin.register(Participant)
class ParticipantAdmin(admin.ModelAdmin):
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 .oneclickaction import OneClickAction
from .participant import Participant
from .trash import TrashedParticipant

View File

@@ -13,12 +13,7 @@ midnight = datetime.time(00, 00, 00)
@python_2_unicode_compatible
class Participant(models.Model):
event = models.ForeignKey('Event', related_name='participants')
created_at = models.DateTimeField(auto_now_add=True)
position = models.IntegerField(verbose_name='Listennummer')
class AbstractParticipant(models.Model):
personal_names = models.CharField(max_length=1024,
verbose_name=_('Vorname(n)'))
family_names = models.CharField(max_length=1024,
@@ -54,17 +49,10 @@ class Participant(models.Model):
purge_at = models.DateTimeField()
class Meta:
unique_together = (('event', 'position'), )
verbose_name = _('Teilnehmer')
verbose_name_plural = _('Teilnehmer')
ordering = ['event', 'position']
abstract = True
def __str__(self):
return '{eventnumber} - {position}. {name}'.format(
eventnumber=self.event.get_number(),
position=self.position,
name=self.get_full_name(),
)
return self.get_full_name()
def get_full_name(self):
return '{} {}'.format(self.personal_names, self.family_names)
@@ -96,6 +84,12 @@ class Participant(models.Model):
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):
if self.dav_member and not self.dav_number:
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.full_clean()
super(Participant, self).save(**kwargs)
super(AbstractParticipant, self).save(**kwargs)
@staticmethod
def calc_purge_at(event):
@@ -132,3 +126,23 @@ class Participant(models.Model):
purge_date = july_nextyear
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>
<hr />
<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).
@@ -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.
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!
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 %}"
role="tabpanel" aria-labelledby="headingRegistrations">
<div class="panel-body">
{% for registration in registrations_all %}
{% for registration in registrations_pending %}
<form action="" method="post" class="form-inline">
{% csrf_token %}
<input type="hidden" name="registration" value="{{ registration.id }}">
{% if registration.answered %}
<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 %}
{% if has_permission_update_participants %}
<button type="submit" name="action" value="accept_registration"
class="btn btn-link no-padding" title="zur Teilnehmerliste hinzufügen">
<span class="text-success">{% bootstrap_icon 'plus-sign' %}</span>
@@ -293,10 +283,6 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
</button>
&nbsp;
{% endif %}
{% if registration.answered %}
<s>
<span class="text-muted">
{% endif %}
{{ registration.get_full_name }}
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
{{ registration.phone_number }})
@@ -309,19 +295,9 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
<span title="{{ registration.get_info }}">
{% bootstrap_icon 'info-sign' %}
</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>
{% empty %}
{% trans 'Keine Anmeldungen vorhanden' %}
{% trans 'Keine unbearbeiteten Anmeldungen vorhanden' %}
{% endfor %}
</div>
</div>
@@ -378,6 +354,12 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
<small>
(<a href="mailto:{{ participant.email_address }}">{{ participant.email_address }}</a>, {{ participant.phone_number }})
</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">
<form action="" method="post" class="form-inline">
{% csrf_token %}
@@ -393,31 +375,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 %}">
<span class="text-info">{% bootstrap_icon 'triangle-bottom' %}</span>
</button>
<button name="action" value="remove_participant"
title="{% trans 'Teilnehmer jetzt löschen' %}
({% trans 'erfolgt automatisch am' %} {{ participant.purge_at|date:'d. F Y'}})"
<button name="action" value="trash_participant"
title="{% trans 'Eintrag in Papierkorb verschieben' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'remove-circle' %}</span>
<span class="text-danger">{% bootstrap_icon 'trash' %}</span>
</button>
&nbsp;
{% endif %}
{% if event.charge and participant.paid and has_permission_payment %}
&nbsp;
<button name="action" value="revoke_payment"
title="{% trans 'Geldeingang wurde bestätigt' %} - {% trans 'Bestätigung des Geldeingangs zurückziehen' %}"
class="btn btn-link no-padding">
<span class="text-success">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge and participant.paid %}
&nbsp;
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
{% elif event.charge and has_permission_payment %}
&nbsp;
<button name="action" value="confirm_payment"
title="{% trans 'Geldeingang bestätigen' %}"
class="btn btn-link no-padding">
<span class="text-danger">{% bootstrap_icon 'piggy-bank' %}</span>
</button>
{% elif event.charge %}
&nbsp;
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
{% bootstrap_icon 'piggy-bank' %}
</span>
@@ -472,6 +456,129 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
</div>
{% endif %}
</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 kann man diese Einträge noch im Papierkorb sehen (herausholen kann sie im Moment aber nur heinzel).
">
{% 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="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>
&nbsp;
<span class="text-info" 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.
">
{% bootstrap_icon 'question-sign' %}
</span>
</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>
<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>
&nbsp;
<span class="text-info" title="Diesen Teilnehmer hast du
am {{ participant.trashed_at|date:'d. F Y, G:i' }}
von Position {{ participant.position }} der Teilnehmerliste entfernt.
">
{% bootstrap_icon 'question-sign' %}
</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>
{% endblock page-container-fluid %}

View File

@@ -1,7 +1,7 @@
import logging
from django.utils import timezone
from .models import Participant
from .models import Participant, TrashedParticipant
logger = logging.getLogger(__name__)
@@ -11,3 +11,6 @@ def purge_participants():
for p in Participant.objects.filter(purge_at__lte=now):
logger.info('Purge participant \'%s\'', p)
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()
context['participants'] = participants
participants_trash = event.trashed_participants.all()
context['participants_trash'] = participants_trash
if participants.count() > 1:
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
if registrations_support:
registrations_all = event.registrations.all()
registrations_pending = registrations_all.filter(answered=False)
context['registrations_pending'] = registrations_pending
registrations_pending = registrations_all.filter(~Q(status__answered=True))
registrations_answered = registrations_all.filter(status__answered=True)
context['registrations_all'] = registrations_all
context['registrations_pending'] = registrations_pending
context['registrations_answered'] = registrations_answered
context['registrations'] = registrations_all
return context
@@ -289,13 +293,11 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
'purge_at': registration.purge_at,
}
participant = models.Participant.objects.create(**data)
registration.answered = True
registration.save()
registration.accepted()
messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name())))
def _reject_registration(self, registration):
registration.answered = True
registration.save()
registration.rejected()
def _swap_participants_position(self, participant1, participant2):
event = participant1.event
@@ -357,18 +359,21 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView):
participant = event.participants.get(id=participant_id)
participant.paid = False
participant.save()
elif action == 'remove_participant':
elif action == 'trash_participant':
self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id')
participant = event.participants.get(id=participant_id)
full_name = participant.get_full_name()
position = participant.position
participants_below = event.participants.filter(position__gt=participant.position)
data = participant.get_data_dict()
trashed = models.TrashedParticipant.objects.create(**data)
participant.delete()
qs = event.participants.filter(position__gt=position)
for participant in qs:
for participant in participants_below:
participant.position -= 1
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 == 'moveup_participant':
self.enforce_permission(event, permission='update-participants')
participant_id = request.POST.get('id')

View File

@@ -1,8 +1,12 @@
from django.contrib import admin
from .models import Registration
from .models import Registration, RegistrationStatus
class RegistrationStatusInline(admin.StackedInline):
model = RegistrationStatus
@admin.register(Registration)
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'))
purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung'))
answered = models.BooleanField(_('Durch Tourleitung beantwortet'), default=False)
@staticmethod
def pk2hexstr(pk):
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)
if creating:
status = RegistrationStatus(registration=self)
status.save()
logger.info('Registration stored: %s', 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
def calc_purge_at(cls, event):
if event.alt_last_day:
@@ -179,3 +197,29 @@ Anmerkung:
purge_date = july_nextyear
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)