Add a kind of trashbin for registrations and participants
All checks were successful
buildbot/tox Build done.
All checks were successful
buildbot/tox Build done.
This commit is contained in:
@@ -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>
|
||||
|
||||
<button disabled="disabled"
|
||||
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
|
||||
<span class="text-muted">{% bootstrap_icon 'minus-sign' %}</span>
|
||||
</button>
|
||||
|
||||
{% 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>
|
||||
|
||||
{% 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>
|
||||
|
||||
{% if registration.answered %}
|
||||
</s>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% empty %}
|
||||
{% trans 'Keine Anmeldungen vorhanden' %}
|
||||
@@ -207,6 +191,12 @@
|
||||
{% else %}
|
||||
{% trans 'Nicht Mitglied' %}
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
|
||||
{% endif %}
|
||||
{% if event.charge and participant.paid and has_permission_payment %}
|
||||
|
||||
<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 %}
|
||||
|
||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
||||
{% bootstrap_icon 'piggy-bank' %}
|
||||
</span>
|
||||
{% elif event.charge and has_permission_payment %}
|
||||
|
||||
<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 %}
|
||||
|
||||
<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> {% 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>
|
||||
|
||||
<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>
|
||||
|
||||
<span class="text-muted">
|
||||
{{ registration.get_full_name }}
|
||||
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
|
||||
{{ registration.phone_number }})
|
||||
|
||||
<span title="Anmeldezeitpunkt">
|
||||
{% bootstrap_icon 'time' %}
|
||||
{{ registration.created_at|date:'d. F Y, G:i' }}
|
||||
</span>
|
||||
|
||||
<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> {% 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 }})
|
||||
|
||||
{% if participant.dav_member %}
|
||||
{{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }}
|
||||
{% else %}
|
||||
{% trans 'Nicht Mitglied' %}
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
||||
|
||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
||||
{% bootstrap_icon 'piggy-bank' %}
|
||||
</span>
|
||||
{% elif event.charge %}
|
||||
|
||||
<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 %}
|
||||
@@ -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
|
||||
|
||||
46
dav_events/migrations/0036_trashedparticipant.py
Normal file
46
dav_events/migrations/0036_trashedparticipant.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -5,3 +5,4 @@ from .eventflag import EventFlag
|
||||
from .eventstatus import EventStatus
|
||||
from .oneclickaction import OneClickAction
|
||||
from .participant import Participant
|
||||
from .trash import TrashedParticipant
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
1
dav_events/models/trash/__init__.py
Normal file
1
dav_events/models/trash/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .trashed_participant import TrashedParticipant
|
||||
26
dav_events/models/trash/trashed_participant.py
Normal file
26
dav_events/models/trash/trashed_participant.py
Normal 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(),
|
||||
)
|
||||
@@ -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>
|
||||
|
||||
<button disabled="disabled"
|
||||
class="btn btn-link no-padding" title="Anmeldung wurde bereits bearbeitet">
|
||||
<span class="text-muted">{% bootstrap_icon 'minus-sign' %}</span>
|
||||
</button>
|
||||
|
||||
{% 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>
|
||||
|
||||
{% 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>
|
||||
|
||||
{% if registration.answered %}
|
||||
</span>
|
||||
</s>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{% endif %}
|
||||
{% if event.charge and participant.paid and has_permission_payment %}
|
||||
|
||||
<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 %}
|
||||
|
||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
||||
{% bootstrap_icon 'piggy-bank' %}
|
||||
</span>
|
||||
{% elif event.charge and has_permission_payment %}
|
||||
|
||||
<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 %}
|
||||
|
||||
<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> {% 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>
|
||||
|
||||
<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>
|
||||
|
||||
<span class="text-muted">
|
||||
{{ registration.get_full_name }}
|
||||
(<a href="mailto:{{ registration.email_address }}">{{ registration.email_address }}</a>,
|
||||
{{ registration.phone_number }})
|
||||
|
||||
<span title="Anmeldezeitpunkt">
|
||||
{% bootstrap_icon 'time' %}
|
||||
{{ registration.created_at|date:'d. F Y, G:i' }}
|
||||
</span>
|
||||
|
||||
<span title="{{ registration.get_info }}">
|
||||
{% bootstrap_icon 'info-sign' %}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<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> {% 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 }})
|
||||
|
||||
<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 %}
|
||||
|
||||
<span class="text-success" title="{% trans 'Geldeingang bestätigt' %}">
|
||||
{% bootstrap_icon 'piggy-bank' %}
|
||||
</span>
|
||||
{% elif event.charge %}
|
||||
|
||||
<span class="text-danger" title="{% trans 'Geldeingang unbestätigt' %}">
|
||||
{% bootstrap_icon 'piggy-bank' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<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 %}
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
39
dav_registration/migrations/0006_auto_20201203_1144.py
Normal file
39
dav_registration/migrations/0006_auto_20201203_1144.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user