diff --git a/dav_event_office/templates/dav_event_office/event_detail.html b/dav_event_office/templates/dav_event_office/event_detail.html index d9fc9c4..4235d70 100644 --- a/dav_event_office/templates/dav_event_office/event_detail.html +++ b/dav_event_office/templates/dav_event_office/event_detail.html @@ -114,7 +114,9 @@ +
+

{% trans 'Teilnehmer' %}

{% if registrations_support %} @@ -131,22 +133,11 @@
- {% for registration in registrations_all %} + {% for registration in registrations_pending %}
{% csrf_token %} - {% if registration.answered %} - -   - -   - {% elif has_permission_update_participants %} + {% if has_permission_update_participants %}   {% endif %} - {% if registration.answered %} - - {% endif %} {{ registration.get_full_name }} ({{ registration.email_address }}, {{ registration.phone_number }}) @@ -173,10 +161,6 @@ {% bootstrap_icon 'info-sign' %} -   - {% if registration.answered %} - - {% endif %}
{% empty %} {% trans 'Keine Anmeldungen vorhanden' %} @@ -207,6 +191,12 @@ {% else %} {% trans 'Nicht Mitglied' %} {% endif %} +   + + {% bootstrap_icon 'info-sign' %} +
{% csrf_token %} @@ -222,31 +212,33 @@ class="btn btn-link no-padding {% if forloop.last %}invisible{% endif %}"> {% bootstrap_icon 'triangle-bottom' %} - -   {% endif %} {% if event.charge and participant.paid and has_permission_payment %} +   {% elif event.charge and participant.paid %} +   {% bootstrap_icon 'piggy-bank' %} {% elif event.charge and has_permission_payment %} +   {% elif event.charge %} +   {% bootstrap_icon 'piggy-bank' %} @@ -297,6 +289,110 @@
{% endif %}
+ +
+ +

{% trans 'Papierkorb' %}

+
+ {% if registrations_support and registrations_answered %} +
+ +
+
+ {% for registration in registrations_answered %} +
+ +   + +   + + {{ registration.get_full_name }} + ({{ registration.email_address }}, + {{ registration.phone_number }}) +   + + {% bootstrap_icon 'time' %} + {{ registration.created_at|date:'d. F Y, G:i' }} + +   + + {% bootstrap_icon 'info-sign' %} + + +
+ {% endfor %} +
+
+
+ {% endif %} + {% if participants_trash %} +
+ +
+
+ {% for participant in participants_trash %} +
+ + {{ participant.get_full_name }} + ({{ participant.email_address }}, + {{ participant.phone_number }}) +   + {% if participant.dav_member %} + {{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }} + {% else %} + {% trans 'Nicht Mitglied' %} + {% endif %} +   + + {% bootstrap_icon 'info-sign' %} + + {% if event.charge and participant.paid %} +   + + {% bootstrap_icon 'piggy-bank' %} + + {% elif event.charge %} +   + + {% bootstrap_icon 'piggy-bank' %} + + {% endif %} + +
+ {% endfor %} +
+
+
+ {% endif %} + {% if not registrations_answered and not participants_trash %} + {% trans 'Der Papierkorb ist leer.' %} + {% endif %} +
{% endblock page-container-fluid %} \ No newline at end of file diff --git a/dav_events/admin.py b/dav_events/admin.py index 5d76ac6..8b3896d 100644 --- a/dav_events/admin.py +++ b/dav_events/admin.py @@ -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 diff --git a/dav_events/migrations/0036_trashedparticipant.py b/dav_events/migrations/0036_trashedparticipant.py new file mode 100644 index 0000000..8753703 --- /dev/null +++ b/dav_events/migrations/0036_trashedparticipant.py @@ -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'], + }, + ), + ] diff --git a/dav_events/models/__init__.py b/dav_events/models/__init__.py index 30c2480..b50a909 100644 --- a/dav_events/models/__init__.py +++ b/dav_events/models/__init__.py @@ -5,3 +5,4 @@ from .eventflag import EventFlag from .eventstatus import EventStatus from .oneclickaction import OneClickAction from .participant import Participant +from .trash import TrashedParticipant diff --git a/dav_events/models/participant.py b/dav_events/models/participant.py index f42ddd4..85472bb 100644 --- a/dav_events/models/participant.py +++ b/dav_events/models/participant.py @@ -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(), + ) diff --git a/dav_events/models/trash/__init__.py b/dav_events/models/trash/__init__.py new file mode 100644 index 0000000..6723f08 --- /dev/null +++ b/dav_events/models/trash/__init__.py @@ -0,0 +1 @@ +from .trashed_participant import TrashedParticipant \ No newline at end of file diff --git a/dav_events/models/trash/trashed_participant.py b/dav_events/models/trash/trashed_participant.py new file mode 100644 index 0000000..90609ac --- /dev/null +++ b/dav_events/models/trash/trashed_participant.py @@ -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(), + ) diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index c280dc6..e29a100 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -222,6 +222,7 @@ +
@@ -266,22 +267,11 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
- {% for registration in registrations_all %} + {% for registration in registrations_pending %} {% csrf_token %} - {% if registration.answered %} - -   - -   - {% elif has_permission_update_participants %} + {% if has_permission_update_participants %}   {% endif %} - {% if registration.answered %} - - - {% endif %} {{ registration.get_full_name }} ({{ registration.email_address }}, {{ registration.phone_number }}) @@ -309,19 +295,9 @@ Das musst du selbst (per E-Mail oder telefonisch) machen. {% bootstrap_icon 'info-sign' %} -   - {% if registration.answered %} - - -   - - {% bootstrap_icon 'question-sign' %} - - {% endif %} {% empty %} - {% trans 'Keine Anmeldungen vorhanden' %} + {% trans 'Keine unbearbeiteten Anmeldungen vorhanden' %} {% endfor %}
@@ -378,6 +354,12 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh ({{ participant.email_address }}, {{ participant.phone_number }}) +   + + {% bootstrap_icon 'info-sign' %} +
{% 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 %}"> {% bootstrap_icon 'triangle-bottom' %} - -   {% endif %} {% if event.charge and participant.paid and has_permission_payment %} +   {% elif event.charge and participant.paid %} +   {% bootstrap_icon 'piggy-bank' %} {% elif event.charge and has_permission_payment %} +   {% elif event.charge %} +   {% bootstrap_icon 'piggy-bank' %} @@ -472,6 +456,129 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh
{% endif %}
+ +
+
+ {% bootstrap_icon 'question-sign' %} +
+

{% trans 'Papierkorb' %}

+
+ {% if registrations_support and registrations_answered %} +
+ +
+
+ {% for registration in registrations_answered %} +
+ +   + +   + + {{ registration.get_full_name }} + ({{ registration.email_address }}, + {{ registration.phone_number }}) +   + + {% bootstrap_icon 'time' %} + {{ registration.created_at|date:'d. F Y, G:i' }} + +   + + {% bootstrap_icon 'info-sign' %} + + +   + + {% bootstrap_icon 'question-sign' %} + +
+ {% endfor %} +
+
+
+ {% endif %} + {% if participants_trash %} +
+ +
+
+ {% for participant in participants_trash %} +
+ + {{ participant.get_full_name }} + ({{ participant.email_address }}, + {{ participant.phone_number }}) +   + + {% bootstrap_icon 'info-sign' %} + + {% if event.charge and participant.paid %} +   + + {% bootstrap_icon 'piggy-bank' %} + + {% elif event.charge %} +   + + {% bootstrap_icon 'piggy-bank' %} + + {% endif %} + +   + + {% bootstrap_icon 'question-sign' %} + +
+ {% endfor %} +
+
+
+ {% endif %} + {% if not registrations_answered and not participants_trash %} + {% trans 'Der Papierkorb ist leer.' %} + {% endif %} +
{% endblock page-container-fluid %} \ No newline at end of file diff --git a/dav_events/utils.py b/dav_events/utils.py index 85377a6..a1cebe4 100644 --- a/dav_events/utils.py +++ b/dav_events/utils.py @@ -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() diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 24afa0d..08f41fb 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -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') diff --git a/dav_registration/admin.py b/dav_registration/admin.py index b7617c8..401f651 100644 --- a/dav_registration/admin.py +++ b/dav_registration/admin.py @@ -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] diff --git a/dav_registration/migrations/0006_auto_20201203_1144.py b/dav_registration/migrations/0006_auto_20201203_1144.py new file mode 100644 index 0000000..13d155a --- /dev/null +++ b/dav_registration/migrations/0006_auto_20201203_1144.py @@ -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'), + ), + ] diff --git a/dav_registration/models.py b/dav_registration/models.py index 3ff8494..38b614c 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -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)