From 6780bc563564123424a6677e3189f87ec198f17d Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 08:23:05 +0200 Subject: [PATCH 01/41] Added migration, created after update --- .../migrations/0033_auto_20200925_1543.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 dav_events/migrations/0033_auto_20200925_1543.py diff --git a/dav_events/migrations/0033_auto_20200925_1543.py b/dav_events/migrations/0033_auto_20200925_1543.py new file mode 100644 index 0000000..91e1ccb --- /dev/null +++ b/dav_events/migrations/0033_auto_20200925_1543.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-09-25 13:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0032_auto_20190605_1400'), + ] + + operations = [ + migrations.AlterField( + model_name='eventstatus', + name='bootstrap_context', + field=models.CharField(blank=True, choices=[('default', 'default'), ('primary', 'primary'), ('success', 'success'), ('info', 'info'), ('warning', 'warning'), ('danger', 'danger'), ('black', 'black')], max_length=20), + ), + ] From 9addc237bd2c455f3dc7c60f81ab4f1e82a2e12b Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 15:28:48 +0200 Subject: [PATCH 02/41] First things to implement a event change log --- dav_events/migrations/0034_eventchange.py | 35 +++++++++++++++ dav_events/models/__init__.py | 1 + dav_events/models/event.py | 49 +++++++++++++++----- dav_events/models/eventchange.py | 36 +++++++++++++++ dav_events/tests/test_models.py | 54 +++++++++++++++++++++++ dav_events/workflow.py | 5 +-- 6 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 dav_events/migrations/0034_eventchange.py create mode 100644 dav_events/models/eventchange.py create mode 100644 dav_events/tests/test_models.py diff --git a/dav_events/migrations/0034_eventchange.py b/dav_events/migrations/0034_eventchange.py new file mode 100644 index 0000000..e83f74b --- /dev/null +++ b/dav_events/migrations/0034_eventchange.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-09-29 10:11 +from __future__ import unicode_literals + +import dav_events.models.eventchange +import dav_events.roles +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dav_events', '0033_auto_20200925_1543'), + ] + + operations = [ + migrations.CreateModel( + name='EventChange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('operation', models.CharField(choices=[('update', 'update')], max_length=20)), + ('content', models.TextField()), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='dav_events.Event')), + ('user', models.ForeignKey(default=dav_events.models.eventchange.get_system_user_id, on_delete=models.SET(dav_events.roles.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['event', 'timestamp'], + }, + ), + ] diff --git a/dav_events/models/__init__.py b/dav_events/models/__init__.py index dbddae6..30c2480 100644 --- a/dav_events/models/__init__.py +++ b/dav_events/models/__init__.py @@ -1,5 +1,6 @@ from ..roles import get_system_user, get_ghost_user from .event import Event +from .eventchange import EventChange from .eventflag import EventFlag from .eventstatus import EventStatus from .oneclickaction import OneClickAction diff --git a/dav_events/models/event.py b/dav_events/models/event.py index 22e6c15..9f9a1ef 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import difflib +import json import logging import os import re @@ -16,12 +17,12 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import get_language, ugettext_lazy as _ from django_countries.fields import CountryField -from . import get_ghost_user from .. import choices from .. import config from .. import signals from ..workflow import DefaultWorkflow - +from . import get_ghost_user +from .eventchange import EventChange logger = logging.getLogger(__name__) @@ -292,9 +293,8 @@ class Event(models.Model): logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner) self.owner = owner creating = True - elif not implicit_update: + else: original = Event.objects.get(id=self.id) - original_text = original.render_as_text(show_internal_fields=True) if not self.editor or not self.editor.is_authenticated: self.editor = self.owner @@ -305,13 +305,40 @@ class Event(models.Model): logger.info('Event created: %s', self) signals.event_created.send(sender=self.__class__, event=self) self.workflow.update_status('draft', self.editor) - elif not implicit_update: - modified_text = self.render_as_text(show_internal_fields=True) - o_lines = original_text.split('\n') - m_lines = modified_text.split('\n') - diff_lines = list(difflib.unified_diff(o_lines, m_lines, n=len(m_lines), lineterm='')) - logger.info('Event updated: %s', self) - signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor) + else: + change = EventChange(event=self, user=self.editor, operation='update', content=self.diff(original)) + change.save() + if not implicit_update: + logger.info('Event updated: %s', self) + signals.event_updated.send(sender=self.__class__, event=self, diff=self.diff(original, fmt='human_readable'), user=self.editor) + + def diff(self, event, fmt='json'): + if fmt == 'human_readable': + from_text = event.render_as_text(show_internal_fields=True) + to_text = self.render_as_text(show_internal_fields=True) + from_lines = from_text.split('\n') + to_lines = to_text.split('\n') + diff_lines = list(difflib.unified_diff(from_lines, to_lines, n=len(from_lines), lineterm='')) + diff_text = '\n'.join(diff_lines[3:]) + elif fmt == 'json': + fields = self._meta.get_fields() + changes = [] + for field in fields: + field_name = field.name + from_value = getattr(event, field_name) + to_value = getattr(self, field_name) + if from_value != to_value: + change = { + 'field': field_name, + 'refer': str(from_value), + 'current': str(to_value), + } + changes.append(change) + diff_text = json.dumps(changes) + else: + raise ValueError("Event.diff(): Unsupported format: {}".format(fmt)) + + return diff_text def is_deadline_expired(self): today = datetime.date.today() diff --git a/dav_events/models/eventchange.py b/dav_events/models/eventchange.py new file mode 100644 index 0000000..8aa5745 --- /dev/null +++ b/dav_events/models/eventchange.py @@ -0,0 +1,36 @@ +from __future__ import unicode_literals +from django.conf import settings +from django.db import models +from django.utils import timezone +from django.utils.encoding import python_2_unicode_compatible + +from . import get_ghost_user, get_system_user + +CHANGE_OPERATIONS = ( + ('update', 'update'), +) + + +def get_system_user_id(): + return get_system_user().id + + +@python_2_unicode_compatible +class EventChange(models.Model): + event = models.ForeignKey('dav_events.Event', related_name='changes') + timestamp = models.DateTimeField(default=timezone.now) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + default=get_system_user_id, + on_delete=models.SET(get_ghost_user), + related_name='+') + + operation = models.CharField(max_length=20, choices=CHANGE_OPERATIONS) + content = models.TextField() + + class Meta: + ordering = ['event', 'timestamp'] + + def __str__(self): + s = '{timestamp} - {user} - {operation}' + return s.format(operation=self.operation, timestamp=self.timestamp.strftime('%d.%m.%Y %H:%M:%S %Z'), + user=self.user) diff --git a/dav_events/tests/test_models.py b/dav_events/tests/test_models.py new file mode 100644 index 0000000..0ca1c1c --- /dev/null +++ b/dav_events/tests/test_models.py @@ -0,0 +1,54 @@ +from __future__ import unicode_literals +import datetime +import json +from django.test import TestCase + +from .generic import EventMixin + +TEST_EVENT_DATA = { + 'title': 'Täst', + 'description': 'Teßt', + 'mode': 'joint', + 'sport': 'W', + 'level': 'beginner', + 'first_day': datetime.date(2019, 3, 1), + 'country': 'DE', + 'trainer_firstname': 'Übungsleiter', + 'trainer_familyname': 'Weißalles', + 'trainer_email': 'trainer@localhost', +} + + +class EventsTestCase(EventMixin, TestCase): + def test_empty_changelog(self): + data = TEST_EVENT_DATA + event = self.create_event_by_model(data) + event.sport = 'M' + self.assertFalse(event.changes.exists()) + + def test_changelog(self): + data = TEST_EVENT_DATA + event = self.create_event_by_model(data) + + event.alt_first_day = event.first_day + datetime.timedelta(1) + event.sport = 'M' + event.ski_lift = True + event.save() + + event.country = 'FR' + event.max_participants = 8 + event.save() + + changes = event.changes + self.assertEqual(changes.count(), 2) + + subchanges = json.loads(changes.first().content) + self.assertEqual(len(subchanges), 3) + self.assertIn({'field': 'alt_first_day', 'refer': 'None', 'current': '2019-03-02'}, subchanges) + self.assertIn({'field': 'sport', 'refer': 'W', 'current': 'M'}, subchanges) + self.assertIn({'field': 'ski_lift', 'refer': 'False', 'current': 'True'}, subchanges) + + subchanges = json.loads(changes.last().content) + self.assertEqual(len(subchanges), 2) + self.assertIn({'field': 'country', 'refer': 'DE', 'current': 'FR'}, subchanges) + self.assertIn({'field': 'max_participants', 'refer': '0', 'current': '8'}, subchanges) diff --git a/dav_events/workflow.py b/dav_events/workflow.py index 69195d5..8538882 100644 --- a/dav_events/workflow.py +++ b/dav_events/workflow.py @@ -312,10 +312,9 @@ class BasicWorkflow(object): if not app_config.settings.enable_email_on_update: return - if len(diff) < 1: + if not diff: logger.debug('send_emails_on_update(): No diff data -> Skip sending mails.') return - diff_text = '\n'.join(diff[3:]) # Who should be informed about the update? recipients = [event.owner] @@ -329,7 +328,7 @@ class BasicWorkflow(object): for recipient in recipients: if recipient.email and recipient.email != updater.email: - email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff_text) + email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff) email.send() def send_emails_on_status_update(self, flag): From 10ea6affafbe61c6adde4278115ada3cf15553ce Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 18:24:38 +0200 Subject: [PATCH 03/41] Show event change log on event detail page --- .../templates/dav_events/event_detail.html | 85 ++++++++++++++----- dav_events/templatetags/dav_events.py | 51 +++++++++++ 2 files changed, 115 insertions(+), 21 deletions(-) diff --git a/dav_events/templates/dav_events/event_detail.html b/dav_events/templates/dav_events/event_detail.html index f4b95c9..fc086d9 100644 --- a/dav_events/templates/dav_events/event_detail.html +++ b/dav_events/templates/dav_events/event_detail.html @@ -1,6 +1,7 @@ {% extends 'dav_events/base.html' %} {% load bootstrap3 %} {% load i18n %} +{% load dav_events %} {% block head-title %}{{ event }} - {{ block.super }}{% endblock head-title %} @@ -188,35 +189,77 @@
{{ event.render_as_html }} -
-
-
-
-
Status-Log
- {% for flag in event.flags.all %} -
-
- {% bootstrap_icon 'check' %} - {{ flag.status.label }}: -
-
- {{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}
- {% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }} +
+
+
+
+ +
+
+ {% for flag in event.flags.all %} +
+
+ {% bootstrap_icon 'check' %} + {{ flag.status.label }}: +
+
+ {{ flag.timestamp|date:'l, d. F Y, H:i' }} {% trans 'Uhr' %}
+ {% trans 'von' %} {{ flag.user.get_full_name|default:flag.user }} +
+
+ {% endfor %}
- {% endfor %}
-
-
{% trans 'Veröffentlichung' %}
+
+ +
+
+ {% render_event_changelog event %} +
+
+
+
+
+
+ {% if event.internal_note %} +
+
+
{% trans 'Bearbeitungshinweis' %}
+
+
+
{{ event.internal_note|linebreaksbr }}
+
+
+ {% endif %} +
+
+
{% trans 'Veröffentlichung' %}
+
+
{% if event.planned_publication_date %} {{ event.planned_publication_date|date:'l, d. F Y' }} {% else %} {% trans 'Unverzüglich' %} {% endif %} - {% if event.internal_note %} -
{% trans 'Bearbeitungshinweis' %}
-
{{ event.internal_note|linebreaksbr }}
- {% endif %}
diff --git a/dav_events/templatetags/dav_events.py b/dav_events/templatetags/dav_events.py index f5731f2..0e5bae7 100644 --- a/dav_events/templatetags/dav_events.py +++ b/dav_events/templatetags/dav_events.py @@ -1,6 +1,9 @@ +import json from django import template from django.utils.html import format_html from django.utils.safestring import mark_safe +from django.utils import timezone +from django.utils.translation import ugettext as _ from ..models.eventstatus import EventStatus, get_or_create_event_status @@ -30,3 +33,51 @@ def render_event_status(event, show_void=True): context=context) return mark_safe(html) + + +@register.simple_tag +def render_event_changelog(event): + change_templ = u'
  • \n' \ + u'\t

    {timestamp}' \ + u' - ' \ + u' {user}

    \n' \ + u'\t{content}\n' \ + u'
  • \n' + subchange_templ = u'
  • \n' \ + u'\t{field}:{separator1}\n' \ + u'\t{refer}\n' \ + u'\t{separator2}\n' \ + u'\t{current}\n' \ + u'
  • \n' + + if event.changes.exists(): + html = u'
      \n' + for change in event.changes.all(): + username = change.user.get_full_name() + if not username: + username = change.user + content_html = u'
        ' + subchanges = json.loads(change.content) + for subchange in subchanges: + field_label = event._meta.get_field(subchange['field']).verbose_name + if len(subchange['refer']) + len(subchange['current']) > 20: + separator1 = u'
        ' + separator2 = u'
        ' + else: + separator1 = u' ' + separator2 = u' -> ' + content_html += format_html(subchange_templ, + field=field_label, + separator1=mark_safe(separator1), + refer=subchange['refer'], + separator2=mark_safe(separator2), + current=subchange['current']) + content_html += u'
      ' + html += format_html(change_templ, + timestamp=timezone.localtime(change.timestamp).strftime('%Y-%m-%d %H:%M:%S %Z'), + user=username, + content=mark_safe(content_html)) + html += u'
    \n' + else: + html = _(u'No entries') + return mark_safe(html) From 09bfbeedc4ac65ba09be0326e27b3c45f33e8bab Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 19:02:55 +0200 Subject: [PATCH 04/41] Satisfy python2 tests --- dav_events/models/event.py | 16 +++++++++++++--- dav_events/models/eventchange.py | 1 + dav_events/tests/test_models.py | 22 +++++++++++++++------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/dav_events/models/event.py b/dav_events/models/event.py index 9f9a1ef..b3ae952 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -15,7 +15,7 @@ from django.db import models from django.template.loader import get_template from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import get_language, ugettext_lazy as _ -from django_countries.fields import CountryField +from django_countries.fields import Country, CountryField from .. import choices from .. import config @@ -326,12 +326,22 @@ class Event(models.Model): for field in fields: field_name = field.name from_value = getattr(event, field_name) + if (isinstance(from_value, datetime.datetime) or + isinstance(from_value, datetime.date) or + isinstance(from_value, datetime.time) or + isinstance(from_value, Country)): + from_value = str(from_value) to_value = getattr(self, field_name) + if (isinstance(to_value, datetime.datetime) or + isinstance(to_value, datetime.date) or + isinstance(to_value, datetime.time) or + isinstance(to_value, Country)): + to_value = str(to_value) if from_value != to_value: change = { 'field': field_name, - 'refer': str(from_value), - 'current': str(to_value), + 'refer': from_value, + 'current': to_value, } changes.append(change) diff_text = json.dumps(changes) diff --git a/dav_events/models/eventchange.py b/dav_events/models/eventchange.py index 8aa5745..0d0f14d 100644 --- a/dav_events/models/eventchange.py +++ b/dav_events/models/eventchange.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.conf import settings from django.db import models diff --git a/dav_events/tests/test_models.py b/dav_events/tests/test_models.py index 0ca1c1c..9899fdf 100644 --- a/dav_events/tests/test_models.py +++ b/dav_events/tests/test_models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime import json @@ -36,19 +37,26 @@ class EventsTestCase(EventMixin, TestCase): event.save() event.country = 'FR' + event.save() + + event.trainer_familyname += '-Ömlaut' event.max_participants = 8 event.save() changes = event.changes - self.assertEqual(changes.count(), 2) + self.assertEqual(changes.count(), 3) - subchanges = json.loads(changes.first().content) + subchanges = json.loads(changes.get(pk=1).content) self.assertEqual(len(subchanges), 3) - self.assertIn({'field': 'alt_first_day', 'refer': 'None', 'current': '2019-03-02'}, subchanges) + self.assertIn({'field': 'alt_first_day', 'refer': None, 'current': '2019-03-02'}, subchanges) self.assertIn({'field': 'sport', 'refer': 'W', 'current': 'M'}, subchanges) - self.assertIn({'field': 'ski_lift', 'refer': 'False', 'current': 'True'}, subchanges) + self.assertIn({'field': 'ski_lift', 'refer': False, 'current': True}, subchanges) - subchanges = json.loads(changes.last().content) - self.assertEqual(len(subchanges), 2) + subchanges = json.loads(changes.get(pk=2).content) + self.assertEqual(len(subchanges), 1) self.assertIn({'field': 'country', 'refer': 'DE', 'current': 'FR'}, subchanges) - self.assertIn({'field': 'max_participants', 'refer': '0', 'current': '8'}, subchanges) + + subchanges = json.loads(changes.get(pk=3).content) + self.assertEqual(len(subchanges), 2) + self.assertIn({'field': 'trainer_familyname', 'refer': 'Weißalles', 'current': 'Weißalles-Ömlaut'}, subchanges) + self.assertIn({'field': 'max_participants', 'refer': 0, 'current': 8}, subchanges) From 96d6dc72fbebeac3b7a78b5cace67400a5f6bddf Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 19:36:42 +0200 Subject: [PATCH 05/41] Satisfied screenshot tests --- dav_events/templates/dav_events/event_detail.html | 2 +- dav_events/templatetags/dav_events.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dav_events/templates/dav_events/event_detail.html b/dav_events/templates/dav_events/event_detail.html index fc086d9..21ef44e 100644 --- a/dav_events/templates/dav_events/event_detail.html +++ b/dav_events/templates/dav_events/event_detail.html @@ -198,7 +198,7 @@ - Status-Log + Status-Flags
    diff --git a/dav_events/templatetags/dav_events.py b/dav_events/templatetags/dav_events.py index 0e5bae7..b63b81e 100644 --- a/dav_events/templatetags/dav_events.py +++ b/dav_events/templatetags/dav_events.py @@ -60,7 +60,11 @@ def render_event_changelog(event): subchanges = json.loads(change.content) for subchange in subchanges: field_label = event._meta.get_field(subchange['field']).verbose_name - if len(subchange['refer']) + len(subchange['current']) > 20: + try: + is_long_strings = (len(subchange['refer']) + len(subchange['current'])) > 20 + except TypeError: + is_long_strings = False + if is_long_strings: separator1 = u'
    ' separator2 = u'
    ' else: From 5237d815517f48d072cbb6bc43f40c918269d310 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 22:19:43 +0200 Subject: [PATCH 06/41] Create change log entry on status updates --- dav_events/migrations/0034_eventchange.py | 4 +- dav_events/models/event.py | 20 ++--- dav_events/models/eventchange.py | 15 ++-- dav_events/templatetags/dav_events.py | 91 +++++++++++++++-------- dav_events/tests/test_models.py | 25 ++++--- dav_events/workflow.py | 3 + 6 files changed, 102 insertions(+), 56 deletions(-) diff --git a/dav_events/migrations/0034_eventchange.py b/dav_events/migrations/0034_eventchange.py index e83f74b..a185f07 100644 --- a/dav_events/migrations/0034_eventchange.py +++ b/dav_events/migrations/0034_eventchange.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.29 on 2020-09-29 10:11 +# Generated by Django 1.11.29 on 2020-09-29 20:15 from __future__ import unicode_literals import dav_events.models.eventchange @@ -23,7 +23,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('operation', models.CharField(choices=[('update', 'update')], max_length=20)), + ('operation', models.CharField(choices=[('update', 'Update'), ('set_flag', 'Raise Flag'), ('unset_flag', 'Lower Flag')], max_length=20)), ('content', models.TextField()), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='dav_events.Event')), ('user', models.ForeignKey(default=dav_events.models.eventchange.get_system_user_id, on_delete=models.SET(dav_events.roles.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL)), diff --git a/dav_events/models/event.py b/dav_events/models/event.py index b3ae952..c4c3b0c 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -306,11 +306,13 @@ class Event(models.Model): signals.event_created.send(sender=self.__class__, event=self) self.workflow.update_status('draft', self.editor) else: - change = EventChange(event=self, user=self.editor, operation='update', content=self.diff(original)) + change = EventChange(event=self, user=self.editor, operation=EventChange.UPDATE, + content=self.diff(original)) change.save() if not implicit_update: logger.info('Event updated: %s', self) - signals.event_updated.send(sender=self.__class__, event=self, diff=self.diff(original, fmt='human_readable'), user=self.editor) + signals.event_updated.send(sender=self.__class__, event=self, user=self.editor, + diff=self.diff(original, fmt='human_readable')) def diff(self, event, fmt='json'): if fmt == 'human_readable': @@ -326,16 +328,14 @@ class Event(models.Model): for field in fields: field_name = field.name from_value = getattr(event, field_name) - if (isinstance(from_value, datetime.datetime) or - isinstance(from_value, datetime.date) or - isinstance(from_value, datetime.time) or - isinstance(from_value, Country)): + try: + json.dumps(from_value) + except TypeError: from_value = str(from_value) to_value = getattr(self, field_name) - if (isinstance(to_value, datetime.datetime) or - isinstance(to_value, datetime.date) or - isinstance(to_value, datetime.time) or - isinstance(to_value, Country)): + try: + json.dumps(to_value) + except TypeError: to_value = str(to_value) if from_value != to_value: change = { diff --git a/dav_events/models/eventchange.py b/dav_events/models/eventchange.py index 0d0f14d..a1f3084 100644 --- a/dav_events/models/eventchange.py +++ b/dav_events/models/eventchange.py @@ -7,10 +7,6 @@ from django.utils.encoding import python_2_unicode_compatible from . import get_ghost_user, get_system_user -CHANGE_OPERATIONS = ( - ('update', 'update'), -) - def get_system_user_id(): return get_system_user().id @@ -18,6 +14,15 @@ def get_system_user_id(): @python_2_unicode_compatible class EventChange(models.Model): + UPDATE = 'update' + RAISE_FLAG = 'set_flag' + LOWER_FLAG = 'unset_flag' + OPERATION_CHOICES = ( + (UPDATE, 'Update'), + (RAISE_FLAG, 'Raise Flag'), + (LOWER_FLAG, 'Lower Flag'), + ) + event = models.ForeignKey('dav_events.Event', related_name='changes') timestamp = models.DateTimeField(default=timezone.now) user = models.ForeignKey(settings.AUTH_USER_MODEL, @@ -25,7 +30,7 @@ class EventChange(models.Model): on_delete=models.SET(get_ghost_user), related_name='+') - operation = models.CharField(max_length=20, choices=CHANGE_OPERATIONS) + operation = models.CharField(max_length=20, choices=OPERATION_CHOICES) content = models.TextField() class Meta: diff --git a/dav_events/templatetags/dav_events.py b/dav_events/templatetags/dav_events.py index b63b81e..4449da5 100644 --- a/dav_events/templatetags/dav_events.py +++ b/dav_events/templatetags/dav_events.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import json from django import template from django.utils.html import format_html @@ -5,6 +6,7 @@ from django.utils.safestring import mark_safe from django.utils import timezone from django.utils.translation import ugettext as _ +from ..models.eventchange import EventChange from ..models.eventstatus import EventStatus, get_or_create_event_status register = template.Library() @@ -38,50 +40,81 @@ def render_event_status(event, show_void=True): @register.simple_tag def render_event_changelog(event): change_templ = u'
  • \n' \ - u'\t

    {timestamp}' \ + u'\t

    ' \ + u'' \ + u' {timestamp}' \ u' - ' \ u' {user}

    \n' \ u'\t{content}\n' \ u'
  • \n' - subchange_templ = u'
  • \n' \ - u'\t{field}:{separator1}\n' \ - u'\t{refer}\n' \ - u'\t{separator2}\n' \ - u'\t{current}\n' \ - u'
  • \n' + update_sub_templ = u'
  • \n' \ + u'\t{field}:{separator1}\n' \ + u'\t{refer}\n' \ + u'\t{separator2}\n' \ + u'\t{current}\n' \ + u'
  • \n' + raise_flag_templ = u'' \ + u' {label}' \ + u' ' + lower_flag_templ = u'' \ + u' {label}' \ + u' ' if event.changes.exists(): html = u'
      \n' + for change in event.changes.all(): + username = change.user.get_full_name() if not username: username = change.user - content_html = u'
        ' - subchanges = json.loads(change.content) - for subchange in subchanges: - field_label = event._meta.get_field(subchange['field']).verbose_name - try: - is_long_strings = (len(subchange['refer']) + len(subchange['current'])) > 20 - except TypeError: - is_long_strings = False - if is_long_strings: - separator1 = u'
        ' - separator2 = u'
        ' - else: - separator1 = u' ' - separator2 = u' -> ' - content_html += format_html(subchange_templ, - field=field_label, - separator1=mark_safe(separator1), - refer=subchange['refer'], - separator2=mark_safe(separator2), - current=subchange['current']) - content_html += u'
      ' + + if change.operation == EventChange.UPDATE: + icon = u'pencil' + content_html = u'
        ' + subchanges = json.loads(change.content) + for subchange in subchanges: + field_label = event._meta.get_field(subchange['field']).verbose_name + try: + is_long_strings = (len(subchange['refer']) + len(subchange['current'])) > 20 + except TypeError: + is_long_strings = False + if is_long_strings: + separator1 = u'
        ' + separator2 = u'
        ' + else: + separator1 = u' ' + separator2 = u' -> ' + content_html += format_html(update_sub_templ, + field=field_label, + separator1=mark_safe(separator1), + refer=subchange['refer'], + separator2=mark_safe(separator2), + current=subchange['current']) + content_html += u'
      ' + elif change.operation == EventChange.RAISE_FLAG: + icon = u'flag' + status = get_or_create_event_status(change.content) + content_html = format_html(raise_flag_templ, + bcontext=status.bootstrap_context, + label=status.label) + elif change.operation == EventChange.LOWER_FLAG: + icon = u'flag' + status = get_or_create_event_status(change.content) + content_html = format_html(lower_flag_templ, + bcontext=status.bootstrap_context, + label=status.label) + else: + icon = u'question-sign' + content_html = format_html(u'{content}', content=change.content) + html += format_html(change_templ, + icon=icon, timestamp=timezone.localtime(change.timestamp).strftime('%Y-%m-%d %H:%M:%S %Z'), user=username, content=mark_safe(content_html)) + html += u'
    \n' else: - html = _(u'No entries') + html = _(u'Keine Einträge') return mark_safe(html) diff --git a/dav_events/tests/test_models.py b/dav_events/tests/test_models.py index 9899fdf..48c0c65 100644 --- a/dav_events/tests/test_models.py +++ b/dav_events/tests/test_models.py @@ -4,6 +4,7 @@ import datetime import json from django.test import TestCase +from ..models.eventchange import EventChange from .generic import EventMixin TEST_EVENT_DATA = { @@ -21,12 +22,6 @@ TEST_EVENT_DATA = { class EventsTestCase(EventMixin, TestCase): - def test_empty_changelog(self): - data = TEST_EVENT_DATA - event = self.create_event_by_model(data) - event.sport = 'M' - self.assertFalse(event.changes.exists()) - def test_changelog(self): data = TEST_EVENT_DATA event = self.create_event_by_model(data) @@ -44,19 +39,29 @@ class EventsTestCase(EventMixin, TestCase): event.save() changes = event.changes - self.assertEqual(changes.count(), 3) + self.assertEqual(changes.count(), 4) - subchanges = json.loads(changes.get(pk=1).content) + change = changes.get(pk=1) + self.assertEqual(change.operation, EventChange.RAISE_FLAG) + self.assertEqual(change.content, 'draft') + + change = changes.get(pk=2) + self.assertEqual(change.operation, EventChange.UPDATE) + subchanges = json.loads(change.content) self.assertEqual(len(subchanges), 3) self.assertIn({'field': 'alt_first_day', 'refer': None, 'current': '2019-03-02'}, subchanges) self.assertIn({'field': 'sport', 'refer': 'W', 'current': 'M'}, subchanges) self.assertIn({'field': 'ski_lift', 'refer': False, 'current': True}, subchanges) - subchanges = json.loads(changes.get(pk=2).content) + change = changes.get(pk=3) + self.assertEqual(change.operation, EventChange.UPDATE) + subchanges = json.loads(change.content) self.assertEqual(len(subchanges), 1) self.assertIn({'field': 'country', 'refer': 'DE', 'current': 'FR'}, subchanges) - subchanges = json.loads(changes.get(pk=3).content) + change = changes.get(pk=4) + self.assertEqual(change.operation, EventChange.UPDATE) + subchanges = json.loads(change.content) self.assertEqual(len(subchanges), 2) self.assertIn({'field': 'trainer_familyname', 'refer': 'Weißalles', 'current': 'Weißalles-Ömlaut'}, subchanges) self.assertIn({'field': 'max_participants', 'refer': 0, 'current': 8}, subchanges) diff --git a/dav_events/workflow.py b/dav_events/workflow.py index 8538882..1856480 100644 --- a/dav_events/workflow.py +++ b/dav_events/workflow.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from . import emails from . import signals +from .models.eventchange import EventChange from .models.eventflag import EventFlag from .models.eventstatus import get_or_create_event_status from .roles import get_users_by_role, has_role @@ -50,6 +51,8 @@ class BasicWorkflow(object): kwargs['status'] = status flag = EventFlag(**kwargs) flag.save() + change = EventChange(event=event, user=flag.user, operation=EventChange.RAISE_FLAG, content=status.code) + change.save() logger.info('Flagging status \'%s\' for %s', status.code, event) return flag From 9945bb588055b32f3d625dd451b8ed28572322b6 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 13 Oct 2020 12:27:42 +0200 Subject: [PATCH 07/41] Close #13 dav_events/Anmeldungen: Mitgliedsmummer wird nun im Info-Tooltip angezeigt. --- dav_registration/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dav_registration/models.py b/dav_registration/models.py index a1c7e64..ae22221 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -109,6 +109,8 @@ class Registration(models.Model): text = """{fullname} {address}, {postal_code} {city} +DAV Mitgliedsnummer: {dav_number} + Erfahrung: {experience} @@ -120,6 +122,7 @@ Anmerkung: address=self.address, postal_code=self.postal_code, city=self.city, + dav_number=self.dav_number, experience=self.experience, note=self.note, ) From 0819b9a38a3ff2e3aa6d379f21b086e84e930043 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 13 Oct 2020 12:40:40 +0200 Subject: [PATCH 08/41] Close #12 and #17 - Added Help-Texts (Mouse-Over-Questionmarks) to the Event-Registraions-View. - Answered registrations will be shown but muted. --- .../dav_events/event_registrations.html | 66 +++++++++++++++++-- dav_events/views/events.py | 8 ++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index fc63f20..a0814bd 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -223,11 +223,38 @@

    +
    + {% bootstrap_icon 'question-sign' %} +

    {% trans 'Teilnehmer' %}

    {% if registrations_support %}
    -
    - {% for registration in registrations %} + {% for registration in registrations_all %}
    {% csrf_token %} - {% if has_permission_update_participants %} + {% if registration.answered %} + +   + +   + {% elif has_permission_update_participants %}
    @@ -284,6 +330,14 @@ {% if has_permission_update_participants %}
    diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 2fd0a44..2e225ac 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -230,9 +230,11 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): registrations_support = hasattr(event, 'registrations') context['registrations_support'] = registrations_support if registrations_support: - registrations = event.registrations.filter(answered=False) - # registrations = event.registrations.all() - context['registrations'] = registrations + registrations_unanswered = event.registrations.filter(answered=False) + registrations_all = event.registrations.all() + context['registrations_unanswered'] = registrations_unanswered + context['registrations_all'] = registrations_all + context['registrations'] = registrations_all return context From e44cff343f309d058e406c9a27c75767ccfd5213 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 13 Oct 2020 13:14:33 +0200 Subject: [PATCH 09/41] More Information on a failing test. dav_events.tests: sometimes an OneclickActionTest failes without providing enough information. --- dav_events/tests/test_oneclickactions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dav_events/tests/test_oneclickactions.py b/dav_events/tests/test_oneclickactions.py index c0b1da7..ee0f45e 100644 --- a/dav_events/tests/test_oneclickactions.py +++ b/dav_events/tests/test_oneclickactions.py @@ -121,7 +121,11 @@ class ActionTestCase(EmailTestMixin, RoleMixin, EventMixin, TestCase): 'user': user.get_full_name(), }) html = message.replace('\'', ''') - self.assertInHTML(html, content) + # Sometimes this test fail, and we cannot see the tested content, so we create our own Exception + try: + self.assertInHTML(html, content) + except AssertionError as e: + raise AssertionError('Not in HTML:\n{}\n-----\n{}\n'.format(html, content)) from e self.assertRegex(content, r'alert-success') def setUp(self): From 28a35d98d291b00f6f29a68b6c2275f3bcb1e4f9 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 13 Oct 2020 14:05:44 +0200 Subject: [PATCH 10/41] Fix the previous change --- dav_events/tests/test_oneclickactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dav_events/tests/test_oneclickactions.py b/dav_events/tests/test_oneclickactions.py index ee0f45e..beee8c2 100644 --- a/dav_events/tests/test_oneclickactions.py +++ b/dav_events/tests/test_oneclickactions.py @@ -124,8 +124,8 @@ class ActionTestCase(EmailTestMixin, RoleMixin, EventMixin, TestCase): # Sometimes this test fail, and we cannot see the tested content, so we create our own Exception try: self.assertInHTML(html, content) - except AssertionError as e: - raise AssertionError('Not in HTML:\n{}\n-----\n{}\n'.format(html, content)) from e + except AssertionError: + raise AssertionError('Not in HTML:\n{}\n-----\n{}\n'.format(html, content)) self.assertRegex(content, r'alert-success') def setUp(self): From 63026e429b5627643c5335b42f5326b059fdec30 Mon Sep 17 00:00:00 2001 From: heinzel Date: Thu, 15 Oct 2020 17:44:12 +0200 Subject: [PATCH 11/41] Fix #9 Registrations: Add support for non members --- .../dav_event_office/event_detail.html | 29 ++++-- .../includes/participant_form.html | 5 +- .../dav_event_office/participant_list.html | 6 +- .../migrations/0034_auto_20201015_1738.py | 26 +++++ dav_events/models/participant.py | 19 +++- .../dav_events/includes/participant_form.html | 5 +- dav_events/views/events.py | 4 +- dav_registration/forms.py | 19 +++- .../migrations/0005_auto_20201015_1738.py | 26 +++++ dav_registration/models.py | 19 +++- .../dav_registration/emails/inform_self.txt | 2 +- .../emails/inform_trainer.txt | 2 +- .../dav_registration/registration_form.html | 39 +++++++- dav_registration/tests/test_emails.py | 30 ++++-- dav_registration/tests/test_models.py | 99 +++++++++++++++++++ dav_registration/tests/test_utils.py | 14 ++- 16 files changed, 315 insertions(+), 29 deletions(-) create mode 100644 dav_events/migrations/0034_auto_20201015_1738.py create mode 100644 dav_registration/migrations/0005_auto_20201015_1738.py create mode 100644 dav_registration/tests/test_models.py 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 5e3e307..d9fc9c4 100644 --- a/dav_event_office/templates/dav_event_office/event_detail.html +++ b/dav_event_office/templates/dav_event_office/event_detail.html @@ -131,11 +131,22 @@ @@ -189,7 +201,12 @@ ({{ participant.email_address }}, {{ participant.phone_number }}) -   {{ participant.dav_number }} +   + {% if participant.dav_member %} + {{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }} + {% else %} + {% trans 'Nicht Mitglied' %} + {% endif %}
    {% csrf_token %} diff --git a/dav_event_office/templates/dav_event_office/includes/participant_form.html b/dav_event_office/templates/dav_event_office/includes/participant_form.html index 4eb0e04..9279bd4 100644 --- a/dav_event_office/templates/dav_event_office/includes/participant_form.html +++ b/dav_event_office/templates/dav_event_office/includes/participant_form.html @@ -29,9 +29,12 @@
    -
    +
    {% bootstrap_field form.dav_number %}
    +
    + {% bootstrap_field form.dav_member %} +
    {% bootstrap_field form.emergency_contact %}
    diff --git a/dav_event_office/templates/dav_event_office/participant_list.html b/dav_event_office/templates/dav_event_office/participant_list.html index 02fc370..f747551 100644 --- a/dav_event_office/templates/dav_event_office/participant_list.html +++ b/dav_event_office/templates/dav_event_office/participant_list.html @@ -55,7 +55,11 @@ {{ participant.personal_names }} - {{ participant.dav_number }} + {% if participant.dav_member %} + {{ participant.dav_number|default:'Fehler! heinzel Bescheid geben!' }} + {% else %} + {% trans 'Nicht Mitglied' %} + {% endif %} diff --git a/dav_events/migrations/0034_auto_20201015_1738.py b/dav_events/migrations/0034_auto_20201015_1738.py new file mode 100644 index 0000000..b530157 --- /dev/null +++ b/dav_events/migrations/0034_auto_20201015_1738.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-10-15 15:38 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0033_auto_20200925_1543'), + ] + + operations = [ + migrations.AddField( + model_name='participant', + name='dav_member', + field=models.BooleanField(default=True, help_text='In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.', verbose_name='DAV Mitglied'), + ), + migrations.AlterField( + model_name='participant', + name='dav_number', + field=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'), + ), + ] diff --git a/dav_events/models/participant.py b/dav_events/models/participant.py index 4171c2d..f42ddd4 100644 --- a/dav_events/models/participant.py +++ b/dav_events/models/participant.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals import datetime +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible @@ -32,8 +33,11 @@ class Participant(models.Model): email_address = models.EmailField(verbose_name=_('E-Mail-Adresse')) phone_number = models.CharField(max_length=254, verbose_name=_('Telefonnummer')) + dav_member = models.BooleanField(default=True, + verbose_name=_('DAV Mitglied'), + help_text=_('In Ausnahmefällen nehmen wir auch Nichtmitglieder mit.')) dav_number = models.CharField(max_length=62, - validators=[DAVNumberValidator], + blank=True, validators=[DAVNumberValidator], verbose_name=_('DAV Mitgliedsnummer')) emergency_contact = models.TextField(blank=True, verbose_name=_('Notfall-Kontakt'), @@ -69,25 +73,38 @@ class Participant(models.Model): text = """{fullname} {address}, {postal_code} {city} + DAV Mitglied: {dav_info} + Notfallkontakt: {emergency_contact} Anmerkung: {note} """ + if not self.dav_member: + dav_info = _('Nein') + else: + dav_info = self.dav_number + return text.format( fullname=self.get_full_name(), address=self.address, postal_code=self.postal_code, city=self.city, + dav_info=dav_info, emergency_contact=self.emergency_contact, note=self.note, ) + def clean(self): + if self.dav_member and not self.dav_number: + raise ValidationError({'dav_number': _('Bei DAV Mitgliedern brauchen wir die Mitgliedsnummer.')}) + def save(self, **kwargs): if not self.purge_at and self.event: self.purge_at = self.__class__.calc_purge_at(self.event) + self.full_clean() super(Participant, self).save(**kwargs) @staticmethod diff --git a/dav_events/templates/dav_events/includes/participant_form.html b/dav_events/templates/dav_events/includes/participant_form.html index 4eb0e04..9279bd4 100644 --- a/dav_events/templates/dav_events/includes/participant_form.html +++ b/dav_events/templates/dav_events/includes/participant_form.html @@ -29,9 +29,12 @@
    -
    +
    {% bootstrap_field form.dav_number %}
    +
    + {% bootstrap_field form.dav_member %} +
    {% bootstrap_field form.emergency_contact %}
    diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 2e225ac..9eeb586 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -230,9 +230,9 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): registrations_support = hasattr(event, 'registrations') context['registrations_support'] = registrations_support if registrations_support: - registrations_unanswered = event.registrations.filter(answered=False) registrations_all = event.registrations.all() - context['registrations_unanswered'] = registrations_unanswered + registrations_pending = registrations_all.filter(answered=False) + context['registrations_pending'] = registrations_pending context['registrations_all'] = registrations_all context['registrations'] = registrations_all diff --git a/dav_registration/forms.py b/dav_registration/forms.py index 504874b..e80d9e3 100644 --- a/dav_registration/forms.py +++ b/dav_registration/forms.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import logging from django import forms -from django.utils.translation import ugettext +from django.utils.translation import ugettext, ugettext_lazy as _ from .models import Registration @@ -9,10 +9,17 @@ logger = logging.getLogger(__name__) class RegistrationForm(forms.ModelForm): + not_dav_member = forms.BooleanField(required=False, + label=_('Ich bin noch kein DAV Mitglied.'), + help_text=_('Wenn du noch kein DAV Mitglied bist,' + ' oder deine Aufnahme noch in Arbeit ist,' + ' kreuze dieses Feld hier an.')) + class Meta: model = Registration exclude = ['event', 'created_at', 'privacy_policy', 'purge_at', 'answered'] widgets = { + 'dav_member': forms.HiddenInput(), 'emergency_contact': forms.Textarea(attrs={'rows': 4}), 'experience': forms.Textarea(attrs={'rows': 5}), 'note': forms.Textarea(attrs={'rows': 5}), @@ -41,3 +48,13 @@ class RegistrationForm(forms.ModelForm): code='privacy_policy_not_accepted', ) return val + + def clean(self): + super(RegistrationForm, self).clean() + dav_member = self.cleaned_data.get('dav_member') + dav_number = self.cleaned_data.get('dav_number') + if dav_member and not dav_number: + error_msg = ugettext(u'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.') + self.add_error('not_dav_member', error_msg) + raise forms.ValidationError(error_msg, code='dav_number_missing') + return self.cleaned_data diff --git a/dav_registration/migrations/0005_auto_20201015_1738.py b/dav_registration/migrations/0005_auto_20201015_1738.py new file mode 100644 index 0000000..3d69b2b --- /dev/null +++ b/dav_registration/migrations/0005_auto_20201015_1738.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-10-15 15:38 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_registration', '0004_auto_20190605_1400'), + ] + + operations = [ + migrations.AddField( + model_name='registration', + name='dav_member', + field=models.BooleanField(default=True, verbose_name='DAV Mitglied'), + ), + migrations.AlterField( + model_name='registration', + name='dav_number', + field=models.CharField(blank=True, help_text='Deine Mitgliedsnummer findest du unter dem Strichcode auf deinem DAV Ausweis.
    Beispiel: 131/00/012345 (der Teil bis zum ersten * genügt)', 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'), + ), + ] diff --git a/dav_registration/models.py b/dav_registration/models.py index ae22221..3ff8494 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import logging +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils import timezone @@ -39,8 +40,10 @@ class Registration(models.Model): phone_number = models.CharField(max_length=254, verbose_name=_('Telefonnummer'), help_text=_('Idealerweise eine Mobilfunk-Nummer')) + dav_member = models.BooleanField(default=True, + verbose_name=_('DAV Mitglied')) dav_number = models.CharField(max_length=62, - validators=[DAVNumberValidator], + blank=True, validators=[DAVNumberValidator], verbose_name=_('DAV Mitgliedsnummer'), help_text='%s
    %s %s' % ( _('Deine Mitgliedsnummer findest du unter dem Strichcode' @@ -109,7 +112,7 @@ class Registration(models.Model): text = """{fullname} {address}, {postal_code} {city} -DAV Mitgliedsnummer: {dav_number} +DAV Mitglied: {dav_info} Erfahrung: {experience} @@ -117,16 +120,25 @@ Erfahrung: Anmerkung: {note} """ + if not self.dav_member: + dav_info = _('Nein') + else: + dav_info = self.dav_number + return text.format( fullname=self.get_full_name(), address=self.address, postal_code=self.postal_code, city=self.city, - dav_number=self.dav_number, + dav_info=dav_info, experience=self.experience, note=self.note, ) + def clean(self): + if self.dav_member and not self.dav_number: + raise ValidationError({'dav_number': _('Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.')}) + def save(self, **kwargs): creating = False if not self.id: @@ -135,6 +147,7 @@ Anmerkung: if not self.purge_at and self.event: self.purge_at = self.__class__.calc_purge_at(self.event) + self.full_clean() super(Registration, self).save(**kwargs) if creating: diff --git a/dav_registration/templates/dav_registration/emails/inform_self.txt b/dav_registration/templates/dav_registration/emails/inform_self.txt index 5db5623..dfdbfeb 100644 --- a/dav_registration/templates/dav_registration/emails/inform_self.txt +++ b/dav_registration/templates/dav_registration/emails/inform_self.txt @@ -22,7 +22,7 @@ Personendaten {{ registration.postal_code }} {{ registration.city }} Telefon: {{ registration.phone_number }} E-Mail: {{ registration.email_address }} -DAV Mitgliedsnummer: {{ registration.dav_number }} +{% if registration.dav_member %}DAV Mitgliedsnummer: {{ registration.dav_number }}{% else %}DAV Mitglied: Nein{% endif %} Notfall-Kontakt --------------- diff --git a/dav_registration/templates/dav_registration/emails/inform_trainer.txt b/dav_registration/templates/dav_registration/emails/inform_trainer.txt index d3e00ba..5da7483 100644 --- a/dav_registration/templates/dav_registration/emails/inform_trainer.txt +++ b/dav_registration/templates/dav_registration/emails/inform_trainer.txt @@ -11,7 +11,7 @@ Teilnehmer*in: {{ registration.address }}, {{ registration.postal_code }} {{ registration.city }} {{ registration.phone_number }} {{ registration.email_address }} -{{ registration.dav_number }} +{% if registration.dav_member %}{{ registration.dav_number }}{% else %}Nicht DAV Mitglied{% endif %} Notfall-Kontakt: {% if registration.emergency_contact %}{{ registration.emergency_contact }}{% else %}-{% endif %} diff --git a/dav_registration/templates/dav_registration/registration_form.html b/dav_registration/templates/dav_registration/registration_form.html index addb143..473e213 100644 --- a/dav_registration/templates/dav_registration/registration_form.html +++ b/dav_registration/templates/dav_registration/registration_form.html @@ -4,6 +4,40 @@ {% block head-title %}{% block form-title %}{% trans 'Anmeldung' %} - {{ event.number }}{% endblock form-title %} - {{ block.super }}{% endblock head-title %} +{% block head-additional %} + +{% endblock head-additional %} + {% block page-container-fluid %}

    {% trans 'Anmeldung' %}

    @@ -56,9 +90,12 @@
    -
    +
    {% bootstrap_field form.dav_number %}
    +
    + {% bootstrap_field form.not_dav_member %} +
    {% bootstrap_field form.emergency_contact %}
    diff --git a/dav_registration/tests/test_emails.py b/dav_registration/tests/test_emails.py index 75012f3..a8502cc 100644 --- a/dav_registration/tests/test_emails.py +++ b/dav_registration/tests/test_emails.py @@ -32,11 +32,11 @@ Vorgang: {registration_hexstr} (wird nur gebraucht, wenn irgendwas schief geht) Personendaten ------------- {participant_full_name} - - -Telefon: +Here +1 Karlsruhe +Telefon: 12 E-Mail: {participant_email} -DAV Mitgliedsnummer: +DAV Mitgliedsnummer: 0 Notfall-Kontakt --------------- @@ -66,10 +66,10 @@ Vorgang: {registration_hexstr} Teilnehmer*in: {participant_full_name} -, - +Here, 1 Karlsruhe +12 {participant_email} - +0 Notfall-Kontakt: - @@ -107,7 +107,12 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'event': event, 'personal_names': 'Participant', 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', 'email_address': 'participant@localhost', + 'dav_number': '0', } registration = self.create_registration(registration_data) @@ -150,6 +155,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'postal_code': '76131', 'city': 'Karlsruhe', 'phone_number': '+49 721 1234567890 AB (Büro)', + 'dav_member': False, 'dav_number': '131/00/007*12345', 'emergency_contact': 'Call 911!', 'experience': 'Yes, we can!', @@ -167,7 +173,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): search += '{} {}\n'.format(registration_data['postal_code'], registration_data['city']) search += 'Telefon: {}\n'.format(registration_data['phone_number']) search += 'E-Mail: {}\n'.format(registration_data['email_address']) - search += 'DAV Mitgliedsnummer: {}\n'.format(registration_data['dav_number']) + search += 'DAV Mitglied: Nein\n' self.assertIn(search, mail.body) search = '\n' @@ -204,7 +210,12 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'event': event, 'personal_names': 'Participant', 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', 'email_address': 'participant@localhost', + 'dav_number': '0', } registration = self.create_registration(registration_data) @@ -247,6 +258,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'postal_code': '76131', 'city': 'Karlsruhe', 'phone_number': '+49 721 1234567890 AB (Büro)', + 'dav_member': False, 'dav_number': '131/00/007*12345', 'emergency_contact': 'Call 911!', 'experience': 'Yes, we can!', @@ -265,7 +277,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): search += '\n' search += registration_data['email_address'] search += '\n' - search += registration_data['dav_number'] + search += 'Nicht DAV Mitglied' search += '\n' self.assertIn(search, mail.body) diff --git a/dav_registration/tests/test_models.py b/dav_registration/tests/test_models.py new file mode 100644 index 0000000..932835f --- /dev/null +++ b/dav_registration/tests/test_models.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.apps import apps +from django.core.exceptions import ValidationError +from django.test import TestCase + +from dav_events.tests.generic import EventMixin + +from .generic import RegistrationMixin + + +class RegistrationTestCase(EventMixin, RegistrationMixin, TestCase): + def setUp(self): + super(RegistrationTestCase, self).setUp() + + app_config = apps.get_app_config('dav_events') + app_config.settings.enable_email_on_status_update = False + + self.event = self.create_event_by_model() + self.submit_event(self.event) + self.accept_event(self.event) + self.confirm_publication_event(self.event) + + def test_create_members(self): + event = self.event + registration_data = { + 'event': event, + 'personal_names': 'Participant', + 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', + 'email_address': 'participant@localhost', + } + dav_numbers = ['0', '12345', '131/00/12345'] + for n in dav_numbers: + d = registration_data + d['dav_number'] = n + self.create_registration(d) + + def test_create_members_without_number(self): + event = self.event + registration_data = { + 'event': event, + 'personal_names': 'Participant', + 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', + 'email_address': 'participant@localhost', + } + with self.assertRaisesMessage(ValidationError, + 'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.'): + self.create_registration(registration_data) + + registration_data['dav_number'] = '' + with self.assertRaisesMessage(ValidationError, + 'Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.'): + self.create_registration(registration_data) + + def test_create_members_with_invalid_numbers(self): + event = self.event + registration_data = { + 'event': event, + 'personal_names': 'Participant', + 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', + 'email_address': 'participant@localhost', + } + dav_numbers = ['Nein', '-', '13100123456789'] + for n in dav_numbers: + d = registration_data + d['dav_number'] = n + + with self.assertRaises(ValidationError) as context: + self.create_registration(d) + + self.assertEqual(context.exception.messages, ['Ungültiges Format.']) + + def test_create_non_member(self): + event = self.event + registration_data = { + 'event': event, + 'personal_names': 'Participant', + 'family_names': 'One', + 'address': 'Here', + 'postal_code': '1', + 'city': 'Karlsruhe', + 'phone_number': '12', + 'email_address': 'participant@localhost', + 'dav_member': False, + } + self.create_registration(registration_data) + diff --git a/dav_registration/tests/test_utils.py b/dav_registration/tests/test_utils.py index 81ff5d4..46ec24a 100644 --- a/dav_registration/tests/test_utils.py +++ b/dav_registration/tests/test_utils.py @@ -29,6 +29,16 @@ class UtilsTestCase(RegistrationMixin, EventMixin, TestCase): 'trainer_familyname': 'One', 'trainer_email': 'trainer@localhost', } + registration_data = { + 'personal_names': 'Participant', + 'family_names': 'P.', + 'address': 'Am Fächerbad 2', + 'postal_code': '76131', + 'city': 'Karlsruhe', + 'phone_number': '555 5555', + 'email_address': 'participant@localhost', + 'dav_number': '1', + } first_day = today - (one_day * 367) while first_day < today: @@ -41,7 +51,9 @@ class UtilsTestCase(RegistrationMixin, EventMixin, TestCase): self.accept_event(event) for i in range(0, registrations_per_event): - self.create_registration({'event': event}) + d = registration_data + d['event'] = event + self.create_registration(d) purge_registrations() From 3a3882080b1aa3ad4460f4b83620e0be1e34623f Mon Sep 17 00:00:00 2001 From: heinzel Date: Thu, 15 Oct 2020 18:05:49 +0200 Subject: [PATCH 12/41] Fix 63026e429b5627643c5335b42f5326b059fdec30 --- dav_events/templates/dav_events/event_registrations.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index a0814bd..c280dc6 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -263,7 +263,7 @@ Das musst du selbst (per E-Mail oder telefonisch) machen.
    -
    {% for registration in registrations_all %} From e3212280b87348ff38d1a3f492381e6f0d55b167 Mon Sep 17 00:00:00 2001 From: heinzel Date: Thu, 15 Oct 2020 18:19:32 +0200 Subject: [PATCH 13/41] Fix 63026e429b5627643c5335b42f5326b059fdec30 --- dav_events/views/events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 9eeb586..24afa0d 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -281,6 +281,7 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): 'city': registration.city, 'email_address': registration.email_address, 'phone_number': registration.phone_number, + 'dav_member': registration.dav_member, 'dav_number': registration.dav_number, 'emergency_contact': registration.emergency_contact, 'experience': registration.experience, From 37519d59e5a0428f000dd25ba7e30db2cb9fcda5 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 3 Nov 2020 11:18:48 +0100 Subject: [PATCH 14/41] Merged migrations and minor improvements --- .../migrations/0035_merge_20201103_1112.py | 16 ++++++++++++++++ dav_events/models/event.py | 1 + .../templates/dav_events/event/default.txt | 1 + dav_events/tests/test_emails.py | 1 + 4 files changed, 19 insertions(+) create mode 100644 dav_events/migrations/0035_merge_20201103_1112.py diff --git a/dav_events/migrations/0035_merge_20201103_1112.py b/dav_events/migrations/0035_merge_20201103_1112.py new file mode 100644 index 0000000..b45c133 --- /dev/null +++ b/dav_events/migrations/0035_merge_20201103_1112.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-11-03 10:12 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0034_auto_20201015_1738'), + ('dav_events', '0034_eventchange'), + ] + + operations = [ + ] diff --git a/dav_events/models/event.py b/dav_events/models/event.py index c4c3b0c..bd0a147 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -505,6 +505,7 @@ class Event(models.Model): 'course_goal_6': self.course_goal_6, 'planned_publication_date': self.planned_publication_date, 'internal_note': self.internal_note, + 'registration_closed': self.registration_closed, } if context is not None: r.update(context) diff --git a/dav_events/templates/dav_events/event/default.txt b/dav_events/templates/dav_events/event/default.txt index b563404..c526db6 100644 --- a/dav_events/templates/dav_events/event/default.txt +++ b/dav_events/templates/dav_events/event/default.txt @@ -54,6 +54,7 @@ {% trans 'Schwierigkeitsnivau' %}: {{ event.get_level_display }} {% if event.sport == 'S' %}{% trans 'Skiliftbenutzung' %}: {% if event.ski_list %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %} {% endif %}{% trans 'Gelände' %}: {{ event.get_terrain_display }} +{% trans 'Anmeldung' %}: {% if registration_required %}{% if registration_closed %}{% trans 'Geschlossen' %}{% else %}{% trans 'Erforderlich' %}{% endif %}{% else %}{% trans 'Nicht erforderlich' %}{% endif %} {% trans 'Anreise des Kurs-/Tourenleiters am Vortag' %}: {% if event.arrival_previous_day %}{% trans 'Ja' %}{% else %}{% trans 'Nein' %}{% endif %} {% trans 'Veröffentlichung' %}: {% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}{% trans 'sofort' %}{% endif %} {% if internal_note %} diff --git a/dav_events/tests/test_emails.py b/dav_events/tests/test_emails.py index 13b33f0..2e3c3e8 100644 --- a/dav_events/tests/test_emails.py +++ b/dav_events/tests/test_emails.py @@ -49,6 +49,7 @@ Link zur Veranstaltung: Veranstaltungsart: gemeinschaftliche Tour Schwierigkeitsnivau: Anfänger Gelände: Kletterhalle + Anmeldung: Nicht erforderlich Anreise des Kurs-/Tourenleiters am Vortag: Nein Veröffentlichung: sofort """ From fbcb8e5f4e7a8fb150204cbae6d48a09a83dd910 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 3 Nov 2020 14:46:21 +0100 Subject: [PATCH 15/41] Fix missing bootstrap context label on flags changelog entries --- dav_events/templatetags/dav_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dav_events/templatetags/dav_events.py b/dav_events/templatetags/dav_events.py index 4449da5..cbeac1d 100644 --- a/dav_events/templatetags/dav_events.py +++ b/dav_events/templatetags/dav_events.py @@ -96,13 +96,13 @@ def render_event_changelog(event): icon = u'flag' status = get_or_create_event_status(change.content) content_html = format_html(raise_flag_templ, - bcontext=status.bootstrap_context, + bcontext=status.bootstrap_context or u'default', label=status.label) elif change.operation == EventChange.LOWER_FLAG: icon = u'flag' status = get_or_create_event_status(change.content) content_html = format_html(lower_flag_templ, - bcontext=status.bootstrap_context, + bcontext=status.bootstrap_context or u'default', label=status.label) else: icon = u'question-sign' From 94595f4785b56d5b12ab8dea5276cbdebd5c0eaf Mon Sep 17 00:00:00 2001 From: heinzel Date: Thu, 3 Dec 2020 11:47:41 +0100 Subject: [PATCH 16/41] Add a kind of trashbin for registrations and participants --- .../dav_event_office/event_detail.html | 146 ++++++++++++--- dav_events/admin.py | 7 +- .../migrations/0036_trashedparticipant.py | 46 +++++ dav_events/models/__init__.py | 1 + dav_events/models/participant.py | 46 +++-- dav_events/models/trash/__init__.py | 1 + .../models/trash/trashed_participant.py | 26 +++ .../dav_events/event_registrations.html | 175 ++++++++++++++---- dav_events/utils.py | 5 +- dav_events/views/events.py | 29 +-- dav_registration/admin.py | 8 +- .../migrations/0006_auto_20201203_1144.py | 39 ++++ dav_registration/models.py | 48 ++++- 13 files changed, 484 insertions(+), 93 deletions(-) create mode 100644 dav_events/migrations/0036_trashedparticipant.py create mode 100644 dav_events/models/trash/__init__.py create mode 100644 dav_events/models/trash/trashed_participant.py create mode 100644 dav_registration/migrations/0006_auto_20201203_1144.py 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) From 7624c3d69b026e19e1fa6bfaf0ed7dd88efd1e9c Mon Sep 17 00:00:00 2001 From: heinzel Date: Thu, 3 Dec 2020 15:50:47 +0100 Subject: [PATCH 17/41] Added restore from trash feature --- .../dav_event_office/event_detail.html | 19 +++-- .../dav_events/event_registrations.html | 81 +++++++++++++------ dav_events/views/events.py | 25 ++++++ 3 files changed, 92 insertions(+), 33 deletions(-) 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 4235d70..ede551e 100644 --- a/dav_event_office/templates/dav_event_office/event_detail.html +++ b/dav_event_office/templates/dav_event_office/event_detail.html @@ -175,13 +175,16 @@ {% with position=participant.position %}
    {% bootstrap_icon 'question-sign' %}
    @@ -486,12 +489,18 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh {% for registration in registrations_answered %}
        @@ -509,13 +518,20 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh {% bootstrap_icon 'info-sign' %} -   - - {% bootstrap_icon 'question-sign' %} - + + {% if has_permission_update_registration %} +
    +
    + {% csrf_token %} + + +
    +
    + {% endif %}
    {% endfor %}
    @@ -541,6 +557,14 @@ auf {% if registration.status.accepted %}Plus{% else %}Minus{% endif %} geklickt
    {% for participant in participants_trash %}
    + +   {{ participant.get_full_name }} ({{ participant.email_address }}, @@ -562,13 +586,20 @@ auf {% if registration.status.accepted %}Plus{% else %}Minus{% endif %} geklickt {% endif %} -   - - {% bootstrap_icon 'question-sign' %} - + + {% if has_permission_update_participants %} +
    +
    + {% csrf_token %} + + +
    +
    + {% endif %}
    {% endfor %}
    diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 08f41fb..b9ccdb7 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -299,6 +299,11 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): def _reject_registration(self, registration): registration.rejected() + def _reset_registration(self, registration): + registration.status.accepted = None + registration.status.answered = False + registration.status.save() + def _swap_participants_position(self, participant1, participant2): event = participant1.event pos_tmp = event.participants.count() + 1 @@ -347,6 +352,14 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): self._reject_registration(registration) else: 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': self.enforce_permission(event, permission='payment') participant_id = request.POST.get('id') @@ -374,6 +387,18 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): participant.save() 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': self.enforce_permission(event, permission='update-participants') participant_id = request.POST.get('id') From e501ada83f499d847336cd5d8267f75e6f205d14 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 8 Dec 2020 18:00:23 +0100 Subject: [PATCH 18/41] Correct db migration for trashbin feature --- dav_registration/migrations/0006_auto_20201203_1144.py | 5 +++-- dav_registration/models.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dav_registration/migrations/0006_auto_20201203_1144.py b/dav_registration/migrations/0006_auto_20201203_1144.py index 13d155a..f106d0c 100644 --- a/dav_registration/migrations/0006_auto_20201203_1144.py +++ b/dav_registration/migrations/0006_auto_20201203_1144.py @@ -27,9 +27,10 @@ class Migration(migrations.Migration): 'ordering': ['updated_at'], }, ), - migrations.RemoveField( + migrations.RenameField( model_name='registration', - name='answered', + old_name='answered', + new_name='answered_2migrate', ), migrations.AddField( model_name='registrationstatus', diff --git a/dav_registration/models.py b/dav_registration/models.py index 38b614c..be1593c 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -72,6 +72,8 @@ class Registration(models.Model): verbose_name=_('Einwilligung zur Datenspeicherung')) purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung')) + answered_2migrate = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet')) + @staticmethod def pk2hexstr(pk): return hex(pk * 113)[2:] # 113 has no meaning, but it produce nice looking hex codes. From d60f1d9993f8614cade753b96c33c4638464edc2 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 8 Dec 2020 19:08:21 +0100 Subject: [PATCH 19/41] Fix migrations for trashbin feature --- .../dav_events/event_registrations.html | 8 ++-- dav_events/views/events.py | 8 ++-- .../migrations/0006_auto_20201203_1144.py | 5 --- .../migrations/0007_auto_20201208_1853.py | 26 ++++++++++++ .../migrations/0008_auto_20201208_1906.py | 20 +++++++++ dav_registration/models.py | 41 ++++++++----------- 6 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 dav_registration/migrations/0007_auto_20201208_1853.py create mode 100644 dav_registration/migrations/0008_auto_20201208_1906.py diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index 9944752..99c8330 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -491,17 +491,17 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh     diff --git a/dav_events/views/events.py b/dav_events/views/events.py index b9ccdb7..b4e21f8 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -293,16 +293,14 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): 'purge_at': registration.purge_at, } participant = models.Participant.objects.create(**data) - registration.accepted() + registration.status.set_accepted() messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name()))) def _reject_registration(self, registration): - registration.rejected() + registration.status.set_rejected() def _reset_registration(self, registration): - registration.status.accepted = None - registration.status.answered = False - registration.status.save() + registration.status.reset() def _swap_participants_position(self, participant1, participant2): event = participant1.event diff --git a/dav_registration/migrations/0006_auto_20201203_1144.py b/dav_registration/migrations/0006_auto_20201203_1144.py index f106d0c..a9f9bf4 100644 --- a/dav_registration/migrations/0006_auto_20201203_1144.py +++ b/dav_registration/migrations/0006_auto_20201203_1144.py @@ -27,11 +27,6 @@ class Migration(migrations.Migration): 'ordering': ['updated_at'], }, ), - migrations.RenameField( - model_name='registration', - old_name='answered', - new_name='answered_2migrate', - ), migrations.AddField( model_name='registrationstatus', name='registration', diff --git a/dav_registration/migrations/0007_auto_20201208_1853.py b/dav_registration/migrations/0007_auto_20201208_1853.py new file mode 100644 index 0000000..4485362 --- /dev/null +++ b/dav_registration/migrations/0007_auto_20201208_1853.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-12-08 17:53 +from __future__ import unicode_literals + +from django.db import migrations + + +def migrate_registration_status(apps, schema_editor): + Registration = apps.get_model('dav_registration', 'Registration') + RegistrationStatus = apps.get_model('dav_registration', 'RegistrationStatus') + db_alias = schema_editor.connection.alias + for r in Registration.objects.using(db_alias).all(): + s = RegistrationStatus(registration=r) + s.answered = r.answered + s.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_registration', '0006_auto_20201203_1144'), + ] + + operations = [ + migrations.RunPython(migrate_registration_status), + ] diff --git a/dav_registration/migrations/0008_auto_20201208_1906.py b/dav_registration/migrations/0008_auto_20201208_1906.py new file mode 100644 index 0000000..5495b04 --- /dev/null +++ b/dav_registration/migrations/0008_auto_20201208_1906.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-12-08 18:06 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_registration', '0007_auto_20201208_1853'), + ] + + operations = [ + migrations.RenameField( + model_name='registration', + old_name='answered', + new_name='answered_obsolete', + ), + ] diff --git a/dav_registration/models.py b/dav_registration/models.py index be1593c..62737ef 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -72,7 +72,7 @@ class Registration(models.Model): verbose_name=_('Einwilligung zur Datenspeicherung')) purge_at = models.DateTimeField(_('Zeitpunkt der Datenlöschung')) - answered_2migrate = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet')) + answered_obsolete = models.BooleanField(default=False, verbose_name=_('Durch Tourleitung beantwortet')) @staticmethod def pk2hexstr(pk): @@ -156,24 +156,6 @@ Anmerkung: 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: @@ -217,11 +199,24 @@ class RegistrationStatus(models.Model): 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'}) + if self.accepted is not None and self.answered is not True: + raise ValidationError({'answered': 'if accepted is not None, answered must be True'}) def save(self, **kwargs): self.full_clean() super(RegistrationStatus, self).save(**kwargs) + + def set_accepted(self): + self.accepted = True + self.answered = True + self.save() + + def set_rejected(self): + self.accepted = False + self.answered = True + self.save() + + def reset(self): + self.accepted = None + self.answered = False + self.save() From 3dc44187fa7fb584f5efce1e35fcc063a97c2026 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 8 Dec 2020 20:27:16 +0100 Subject: [PATCH 20/41] Improved display of trashed registrations --- .../templates/dav_event_office/event_detail.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ede551e..194d0c5 100644 --- a/dav_event_office/templates/dav_event_office/event_detail.html +++ b/dav_event_office/templates/dav_event_office/event_detail.html @@ -315,12 +315,12 @@
        @@ -398,4 +398,4 @@
    -{% endblock page-container-fluid %} \ No newline at end of file +{% endblock page-container-fluid %} From 45f50e8601f0158823a65c4c5a578fa3187a9930 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 8 Dec 2020 20:27:51 +0100 Subject: [PATCH 21/41] Better icon for untrashing --- dav_events/templates/dav_events/event_registrations.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index 99c8330..7d5b397 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -527,7 +527,7 @@ auf {% if registration.status.accepted == True %}Plus{% elif registration.status
    @@ -595,7 +595,7 @@ von Position {{ participant.position }} der Teilnehmerliste entfernt.
    @@ -612,4 +612,4 @@ von Position {{ participant.position }} der Teilnehmerliste entfernt. -{% endblock page-container-fluid %} \ No newline at end of file +{% endblock page-container-fluid %} From 998b4cd5cfa3fe5ecdfe68e9e01d1a94dcfa009b Mon Sep 17 00:00:00 2001 From: heinzel Date: Wed, 9 Dec 2020 08:31:41 +0100 Subject: [PATCH 22/41] Registrations: decollapse events by clicking on the title --- .../templates/dav_registration/event_list.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dav_registration/templates/dav_registration/event_list.html b/dav_registration/templates/dav_registration/event_list.html index 83c7f1c..9636609 100644 --- a/dav_registration/templates/dav_registration/event_list.html +++ b/dav_registration/templates/dav_registration/event_list.html @@ -17,7 +17,7 @@
    +

    {{ event.get_formated_date }} {% if event.get_alt_formated_date %} @@ -81,12 +85,12 @@

    {% endblock head-additional %} @@ -74,10 +85,10 @@
    -
    +
    {% bootstrap_field form.postal_code %}
    -
    +
    {% bootstrap_field form.city %}
    @@ -93,6 +104,12 @@
    {% bootstrap_field form.year_of_birth %}
    +
    + Antrag auf reduzierte Teilnahmegebühr + {% bootstrap_field form.apply_reduced_fee %} +
    +
    +
    {% bootstrap_field form.dav_number %}
    @@ -100,11 +117,6 @@ Nichtmitglieder {% bootstrap_field form.not_dav_member %}
    -
    -
    -
    - -
    {% bootstrap_field form.emergency_contact %}
    From 50d7ef953e5d5541f9c4154cfc7da4c54ef399bb Mon Sep 17 00:00:00 2001 From: heinzel Date: Wed, 9 Dec 2020 17:59:36 +0100 Subject: [PATCH 27/41] satisfy tests --- dav_registration/tests/test_emails.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/dav_registration/tests/test_emails.py b/dav_registration/tests/test_emails.py index 0a33235..1c6547b 100644 --- a/dav_registration/tests/test_emails.py +++ b/dav_registration/tests/test_emails.py @@ -37,6 +37,7 @@ Here Telefon: 12 E-Mail: {participant_email} Jahrgang: {year_of_birth} +Antrag auf reduzierte Teilnahmegebühr: {apply_reduced_fee_yesno} DAV Mitgliedsnummer: 0 Notfall-Kontakt @@ -71,6 +72,7 @@ Here, 1 Karlsruhe 12 {participant_email} 0 +Antrag auf reduzierte Teilnahmegebühr: {apply_reduced_fee_yesno} Notfall-Kontakt: - @@ -136,6 +138,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): participant_full_name=registration.get_full_name(), participant_email=registration.email_address, year_of_birth=registration.year_of_birth, + apply_reduced_fee_yesno='Nein', event_number=event.get_number(), event_title=event.title, event_formated_date=event.get_formated_date(), @@ -160,9 +163,10 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'postal_code': '76131', 'city': 'Karlsruhe', 'phone_number': '+49 721 1234567890 AB (Büro)', + 'year_of_birth': 1976, + 'apply_reduced_fee': True, 'dav_member': False, 'dav_number': '131/00/007*12345', - 'year_of_birth': 1976, 'emergency_contact': 'Call 911!', 'experience': 'Yes, we can!', 'note': 'Automatischer Software Test\nGruß\n heinzel =u}', @@ -180,6 +184,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): search += 'Telefon: {}\n'.format(registration_data['phone_number']) search += 'E-Mail: {}\n'.format(registration_data['email_address']) search += 'Jahrgang: {}\n'.format(registration_data['year_of_birth']) + search += 'Antrag auf reduzierte Teilnahmegebühr: Ja\n' search += 'DAV Mitglied: Nein\n' self.assertIn(search, mail.body) @@ -245,6 +250,7 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): participant_email=registration.email_address, year_of_birth=registration.year_of_birth, approx_age=registration.approx_age(), + apply_reduced_fee_yesno='Nein', event_number=event.get_number(), event_title=event.title, event_formated_date=event.get_formated_date(), @@ -268,9 +274,10 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): 'postal_code': '76131', 'city': 'Karlsruhe', 'phone_number': '+49 721 1234567890 AB (Büro)', + 'year_of_birth': THIS_YEAR, + 'apply_reduced_fee': True, 'dav_member': False, 'dav_number': '131/00/007*12345', - 'year_of_birth': THIS_YEAR, 'emergency_contact': 'Call 911!', 'experience': 'Yes, we can!', 'note': 'Automatischer Software Test\nGruß\n heinzel =u}', @@ -288,8 +295,8 @@ class EmailsTestCase(EmailTestMixin, EventMixin, RegistrationMixin, TestCase): search += '\n' search += registration_data['email_address'] search += '\n' - search += 'Nicht DAV Mitglied' - search += '\n' + search += 'Nicht DAV Mitglied\n' + search += 'Antrag auf reduzierte Teilnahmegebühr: Ja\n' self.assertIn(search, mail.body) search = '\n' From f016f4af41aa55b120385ace38d243b74e31593f Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 15 Dec 2020 11:56:36 +0100 Subject: [PATCH 28/41] UPD: Registration Form: more information about not DAV member --- dav_registration/forms.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dav_registration/forms.py b/dav_registration/forms.py index bbeb9ee..952813a 100644 --- a/dav_registration/forms.py +++ b/dav_registration/forms.py @@ -12,9 +12,13 @@ logger = logging.getLogger(__name__) class RegistrationForm(forms.ModelForm): not_dav_member = forms.BooleanField(required=False, label=_('Ich bin noch kein DAV Mitglied.'), - help_text=_('Wenn du noch kein DAV Mitglied bist,' - ' oder deine Aufnahme noch in Arbeit ist,' - ' kreuze dieses Feld hier an.')) + help_text='%s
    \n%s' % ( + _('Wenn du noch kein DAV Mitglied bist,' + ' oder deine Aufnahme noch in Arbeit ist,' + ' kreuze dieses Feld hier an.'), + _('Spätestens zu Veranstaltungsbeginn muss' + ' jedoch eine Mitgliedschaft bestehen.') + )) class Meta: model = Registration From a7fafbe73fc9b2f634b1020faa5cb907c2207118 Mon Sep 17 00:00:00 2001 From: heinzel Date: Wed, 16 Dec 2020 10:24:17 +0100 Subject: [PATCH 29/41] UPD: Participants: added help texts to the new fields --- .../migrations/0039_auto_20201215_1155.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 dav_events/migrations/0039_auto_20201215_1155.py diff --git a/dav_events/migrations/0039_auto_20201215_1155.py b/dav_events/migrations/0039_auto_20201215_1155.py new file mode 100644 index 0000000..42d664c --- /dev/null +++ b/dav_events/migrations/0039_auto_20201215_1155.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-12-15 10:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0038_auto_20201209_1542'), + ] + + operations = [ + migrations.AlterField( + model_name='participant', + name='apply_reduced_fee', + field=models.BooleanField(default=False, help_text='Für Jugendliche und Junioren (bis zum vollendeten 25. Lebensjahr), sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch "Karlsruher Pass"), wird die Teilnahmegebühr auf 50% ermäßigt.', verbose_name='Antrag auf reduzierte Teilnahmegebühr'), + ), + migrations.AlterField( + model_name='participant', + name='year_of_birth', + field=models.IntegerField(help_text='Vierstellige Jahreszahl', verbose_name='Geburtsjahr'), + ), + migrations.AlterField( + model_name='trashedparticipant', + name='apply_reduced_fee', + field=models.BooleanField(default=False, help_text='Für Jugendliche und Junioren (bis zum vollendeten 25. Lebensjahr), sowie Mitglieder mit geringen finanziellen Mitteln (Nachweis durch "Karlsruher Pass"), wird die Teilnahmegebühr auf 50% ermäßigt.', verbose_name='Antrag auf reduzierte Teilnahmegebühr'), + ), + migrations.AlterField( + model_name='trashedparticipant', + name='year_of_birth', + field=models.IntegerField(help_text='Vierstellige Jahreszahl', verbose_name='Geburtsjahr'), + ), + ] From d3d63097db979887179dc005df5cf1bb21cb5af8 Mon Sep 17 00:00:00 2001 From: heinzel Date: Wed, 16 Dec 2020 10:32:12 +0100 Subject: [PATCH 30/41] FIX f016f4af41aa55b120385ace38d243b74e31593f (python2 encoding error) --- dav_registration/forms.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dav_registration/forms.py b/dav_registration/forms.py index 952813a..2b10316 100644 --- a/dav_registration/forms.py +++ b/dav_registration/forms.py @@ -11,13 +11,13 @@ logger = logging.getLogger(__name__) class RegistrationForm(forms.ModelForm): not_dav_member = forms.BooleanField(required=False, - label=_('Ich bin noch kein DAV Mitglied.'), - help_text='%s
    \n%s' % ( - _('Wenn du noch kein DAV Mitglied bist,' - ' oder deine Aufnahme noch in Arbeit ist,' - ' kreuze dieses Feld hier an.'), - _('Spätestens zu Veranstaltungsbeginn muss' - ' jedoch eine Mitgliedschaft bestehen.') + label=_(u'Ich bin noch kein DAV Mitglied.'), + help_text=u'%s
    \n%s' % ( + _(u'Wenn du noch kein DAV Mitglied bist,' + u' oder deine Aufnahme noch in Arbeit ist,' + u' kreuze dieses Feld hier an.'), + _(u'Spätestens zu Veranstaltungsbeginn muss' + u' jedoch eine Mitgliedschaft bestehen.') )) class Meta: @@ -30,7 +30,7 @@ class RegistrationForm(forms.ModelForm): 'note': forms.Textarea(attrs={'rows': 5}), } labels = { - 'apply_reduced_fee': _('Ich bin noch keine 25 Jahre alt oder besitze einen "Karlsruher Pass".'), + 'apply_reduced_fee': _(u'Ich bin noch keine 25 Jahre alt oder besitze einen "Karlsruher Pass".'), } def clean_year_of_birth(self): From b44f6df32e2ba3f2a6b230cef085920360a9f437 Mon Sep 17 00:00:00 2001 From: heinzel Date: Wed, 16 Dec 2020 17:28:40 +0100 Subject: [PATCH 31/41] Registrations: view to uncheck apply_reduced_fee while accepting --- dav_events/forms/__init__.py | 1 + dav_events/forms/registration.py | 8 ++ .../migrations/0040_auto_20201216_1712.py | 35 ++++++++ dav_events/models/participant.py | 5 ++ .../dav_events/event_registrations.html | 7 ++ .../dav_events/registration_response.html | 45 ++++++++++ dav_events/urls.py | 1 + dav_events/views/events.py | 84 ++++++++++++++----- dav_registration/models.py | 7 ++ 9 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 dav_events/forms/registration.py create mode 100644 dav_events/migrations/0040_auto_20201216_1712.py create mode 100644 dav_events/templates/dav_events/registration_response.html diff --git a/dav_events/forms/__init__.py b/dav_events/forms/__init__.py index a13db64..c7f2392 100644 --- a/dav_events/forms/__init__.py +++ b/dav_events/forms/__init__.py @@ -1,3 +1,4 @@ from . import generic from . import events from . import participant +from . import registration diff --git a/dav_events/forms/registration.py b/dav_events/forms/registration.py new file mode 100644 index 0000000..2d28b2b --- /dev/null +++ b/dav_events/forms/registration.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +class RegistrationResponseForm(forms.Form): + apply_reduced_fee = forms.BooleanField(required=False, + label=_(u'Reduzierte Teilnahmegebühr')) diff --git a/dav_events/migrations/0040_auto_20201216_1712.py b/dav_events/migrations/0040_auto_20201216_1712.py new file mode 100644 index 0000000..d844345 --- /dev/null +++ b/dav_events/migrations/0040_auto_20201216_1712.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-12-16 16:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0039_auto_20201215_1155'), + ] + + operations = [ + migrations.AddField( + model_name='participant', + name='privacy_policy', + field=models.TextField(blank=True, verbose_name='Erklärung zur Datenspeicherung'), + ), + migrations.AddField( + model_name='participant', + name='privacy_policy_accepted', + field=models.BooleanField(default=False, verbose_name='Einwilligung zur Datenspeicherung'), + ), + migrations.AddField( + model_name='trashedparticipant', + name='privacy_policy', + field=models.TextField(blank=True, verbose_name='Erklärung zur Datenspeicherung'), + ), + migrations.AddField( + model_name='trashedparticipant', + name='privacy_policy_accepted', + field=models.BooleanField(default=False, verbose_name='Einwilligung zur Datenspeicherung'), + ), + ] diff --git a/dav_events/models/participant.py b/dav_events/models/participant.py index 01af818..da0b692 100644 --- a/dav_events/models/participant.py +++ b/dav_events/models/participant.py @@ -53,6 +53,11 @@ class AbstractParticipant(models.Model): verbose_name=_('Anmerkung'), help_text=_('Kann frei gelassen werden.')) + privacy_policy = models.TextField(blank=True, + verbose_name=_('Erklärung zur Datenspeicherung')) + privacy_policy_accepted = models.BooleanField(default=False, + verbose_name=_('Einwilligung zur Datenspeicherung')) + paid = models.BooleanField('Teilnehmerbeitrag bezahlt', default=False) purge_at = models.DateTimeField() diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index b8b3278..b565db3 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -272,10 +272,17 @@ Das musst du selbst (per E-Mail oder telefonisch) machen. {% csrf_token %} {% if has_permission_update_participants %} + {% if registration.apply_reduced_fee %} + + {% bootstrap_icon 'plus-sign' %} + + {% else %} + {% endif %}   + + {% bootstrap_icon 'remove' %} Zurück + + +
    +{% endblock page-container %} diff --git a/dav_events/urls.py b/dav_events/urls.py index b348d56..40cb2d7 100644 --- a/dav_events/urls.py +++ b/dav_events/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ views.events.EventUpdateStatusView.as_view(), name='updatestatus'), url(r'^(?P\d+)/edit', views.events.EventUpdateView.as_view(), name='update'), url(r'^(?P\d+)/', views.events.EventDetailView.as_view(), name='detail'), + url(r'^registration/(?P\d+)/', views.events.RespondRegistrationView.as_view(), name='respond_registration'), url(r'^action/(?P[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12})/', views.actions.OneClickActionRunView.as_view(), name='action_run'), ] diff --git a/dav_events/views/events.py b/dav_events/views/events.py index c234771..febf662 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -2,6 +2,7 @@ import datetime import logging import os +from django.apps import apps from django.contrib import messages from django.contrib.auth import login from django.contrib.auth.decorators import login_required @@ -271,30 +272,13 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): messages.success(request, _(u'Der Anmeldeschluss wurde gelöscht')) def _accept_registration(self, request, registration): - event = registration.event + data = registration.get_data_dict() + del data['created_at'] + del data['answered_obsolete'] + data['position'] = registration.event.participants.count() + 1 - position = event.participants.count() + 1 - - data = { - 'event': event, - 'position': position, - 'personal_names': registration.personal_names, - 'family_names': registration.family_names, - 'address': registration.address, - 'postal_code': registration.postal_code, - 'city': registration.city, - 'email_address': registration.email_address, - 'year_of_birth': registration.year_of_birth, - 'apply_reduced_fee': registration.apply_reduced_fee, - 'phone_number': registration.phone_number, - 'dav_member': registration.dav_member, - 'dav_number': registration.dav_number, - 'emergency_contact': registration.emergency_contact, - 'experience': registration.experience, - 'note': registration.note, - 'purge_at': registration.purge_at, - } participant = models.Participant.objects.create(**data) + registration.status.set_accepted() messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name()))) @@ -455,6 +439,62 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): return super(EventRegistrationsView, self).dispatch(request, *args, **kwargs) +class RespondRegistrationView(EventPermissionMixin, generic.DetailView, generic.FormView): + permission = 'update-participants' + context_object_name = 'registration' + template_name = 'dav_events/registration_response.html' + form_class = forms.registration.RegistrationResponseForm + + def _accept_registration(self, request, registration): + data = registration.get_data_dict() + del data['created_at'] + del data['answered_obsolete'] + data['position'] = registration.event.participants.count() + 1 + + participant = models.Participant.objects.create(**data) + + registration.status.set_accepted() + messages.success(request, _(u'Teilnehmer hinzugefügt: {}'.format(participant.get_full_name()))) + + def has_permission(self, permission, obj): + user = self.request.user + return obj.event.workflow.has_permission(user, permission) + + def get_queryset(self): + model = apps.get_model(app_label='dav_registration', model_name='Registration') + return model.objects.all() + + def get_success_url(self): + return reverse('dav_events:registrations', args=[self.object.event.pk]) + + def get_initial(self): + return { + 'apply_reduced_fee': self.object.apply_reduced_fee, + } + + def form_valid(self, form): + registration = self.object + registration.apply_reduced_fee = form.cleaned_data['apply_reduced_fee'] + registration.save() + self._accept_registration(self.request, registration) + return HttpResponseRedirect(self.get_success_url()) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + self.enforce_permission(self.object) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + self.enforce_permission(self.object) + return super(RespondRegistrationView, self).post(request, *args, **kwargs) + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super(RespondRegistrationView, self).dispatch(request, *args, **kwargs) + + class EventUpdateStatusView(EventPermissionMixin, generic.DetailView): model = models.Event diff --git a/dav_registration/models.py b/dav_registration/models.py index fa82b73..d5cc281 100644 --- a/dav_registration/models.py +++ b/dav_registration/models.py @@ -164,6 +164,13 @@ Anmerkung: note=self.note, ) + def get_data_dict(self): + data = {} + for field in self._meta.fields: + if not field.primary_key: + 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': _('Wenn du DAV Mitglied bist, brauchen wir deine Mitgliedsnummer.')}) From dab2e5cb76f5503b66c9aa6109228fbc44eb55ee Mon Sep 17 00:00:00 2001 From: heinzel Date: Fri, 18 Dec 2020 11:43:35 +0100 Subject: [PATCH 32/41] FIX: some participant values were removed if participant was edited Some participant fields were not included in the form template and thus every time the form was saved those values were emptied. We exclude those fileds now within the form class. --- dav_events/forms/participant.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dav_events/forms/participant.py b/dav_events/forms/participant.py index 92dd23c..e471173 100644 --- a/dav_events/forms/participant.py +++ b/dav_events/forms/participant.py @@ -7,7 +7,9 @@ from ..models import Participant class ParticipantForm(forms.ModelForm): class Meta: model = Participant - exclude = ['event', 'created_at', 'position', 'purge_at'] + exclude = ['event', 'created_at', 'position', + 'privacy_policy', 'privacy_policy_accepted', + 'paid', 'purge_at'] widgets = { 'emergency_contact': forms.Textarea(attrs={'rows': 4}), 'experience': forms.Textarea(attrs={'rows': 5}), From cbc3df97c1f52f3fcdfe66b8fb27c2b7c754f3b6 Mon Sep 17 00:00:00 2001 From: heinzel Date: Fri, 18 Dec 2020 11:48:10 +0100 Subject: [PATCH 33/41] Reduced participant fee: added toggle buttons within office views and improved html --- .../dav_event_office/event_detail.html | 57 +++++++++++++------ .../dav_event_office/participant_list.html | 23 ++++++-- dav_event_office/views.py | 5 ++ .../dav_events/event_registrations.html | 43 ++++++++------ dav_events/views/events.py | 16 ++++++ 5 files changed, 104 insertions(+), 40 deletions(-) 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 3ab06c4..fa4c9e2 100644 --- a/dav_event_office/templates/dav_event_office/event_detail.html +++ b/dav_event_office/templates/dav_event_office/event_detail.html @@ -210,6 +210,17 @@
    {% csrf_token %} + {% if event.charge and has_permission_payment %} + +   + {% endif %} {% if has_permission_update_participants %} {% elif event.charge and participant.paid %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% elif event.charge and has_permission_payment %}   - {% if participant.apply_reduced_fee %}%{% elif participant.created_at|date:'U' < '1608764400' %}? {% else %} {% endif %} {% elif event.charge %}   - - {% if participant.apply_reduced_fee %}%{% elif participant.created_at|date:'U' < '1608764400' %}? {% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + {% if participant.apply_reduced_fee %}%{% else %}? {% endif %}{% bootstrap_icon 'piggy-bank' %} {% else %} - + {% endif %}
    @@ -287,6 +303,11 @@ {% endif %} +
    +
    + {% trans 'Gebuchte Teilnahmegebühren' %}: {{ earnings|floatformat:2 }} € +
    +
    {% if participant_emails %}
    @@ -380,14 +401,16 @@ {% if event.charge and participant.paid %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% elif event.charge %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% endif %}
    diff --git a/dav_event_office/templates/dav_event_office/participant_list.html b/dav_event_office/templates/dav_event_office/participant_list.html index 99c2439..c9a9e71 100644 --- a/dav_event_office/templates/dav_event_office/participant_list.html +++ b/dav_event_office/templates/dav_event_office/participant_list.html @@ -71,24 +71,37 @@
    {% csrf_token %} + {% if event.charge %} + +   + {% endif %} {% if event.charge and participant.paid %} - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}   ({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €) {% elif event.charge %} - {% if participant.apply_reduced_fee %}%{% elif participant.created_at|date:'U' < '1608764400' %}? {% else %} {% endif %}   ({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €) {% else %} - - {% bootstrap_icon 'piggy-bank' %} - + {% endif %}
    diff --git a/dav_event_office/views.py b/dav_event_office/views.py index c44a136..148d3b1 100644 --- a/dav_event_office/views.py +++ b/dav_event_office/views.py @@ -53,6 +53,11 @@ class ParticipantListView(generic.ListView): participant = get_object_or_404(Participant, pk=participant_id) participant.paid = False participant.save() + elif action == 'toggle_reduced_fee': + participant_id = request.POST.get('id') + participant = get_object_or_404(Participant, pk=participant_id) + participant.apply_reduced_fee = not participant.apply_reduced_fee + participant.save() else: messages.error(request, 'unsupported action: {}'.format(action)) return HttpResponseRedirect(reverse('dav_event_office:participant-list')) diff --git a/dav_events/templates/dav_events/event_registrations.html b/dav_events/templates/dav_events/event_registrations.html index b565db3..531557e 100644 --- a/dav_events/templates/dav_events/event_registrations.html +++ b/dav_events/templates/dav_events/event_registrations.html @@ -305,7 +305,7 @@ Das musst du selbst (per E-Mail oder telefonisch) machen. {% if registration.apply_reduced_fee %}   - %{% bootstrap_icon 'piggy-bank' %} (reduzierte Gebühr) + %{% bootstrap_icon 'piggy-bank' %} (reduzierte Gebühr) {% endif %} @@ -399,30 +399,35 @@ Wichtig: das System verschickt keine Bestätigung an dich oder den neuen Teilneh {% endif %} {% if event.charge and participant.paid and has_permission_payment %}   - {% if participant.apply_reduced_fee %}%{% else %} {% endif %} {% elif event.charge and participant.paid %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% elif event.charge and has_permission_payment %}   - {% if participant.apply_reduced_fee %}%{% else %} {% endif %} {% elif event.charge %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% else %} - + {% endif %}
    @@ -587,14 +592,16 @@ von Position {{ participant.position }} der Teilnehmerliste entfernt. {% if event.charge and participant.paid %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% elif event.charge %}   - - {% if participant.apply_reduced_fee %}%{% else %} {% endif %}{% bootstrap_icon 'piggy-bank' %} - + %{% bootstrap_icon 'piggy-bank' %} {% endif %} diff --git a/dav_events/views/events.py b/dav_events/views/events.py index febf662..e98196e 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -219,6 +219,16 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): participants_trash = event.trashed_participants.all() context['participants_trash'] = participants_trash + earnings = 0 + if event.charge: + for participant in participants: + if participant.paid: + if participant.apply_reduced_fee: + earnings += event.charge / 2 + else: + earnings += event.charge + context['earnings'] = earnings + if participants.count() > 1: email_list = [u'"{}" <{}>'.format(p.get_full_name(), p.email_address) for p in participants] email_list.sort() @@ -344,6 +354,12 @@ class EventRegistrationsView(EventPermissionMixin, generic.DetailView): self._reset_registration(registration) else: raise FieldDoesNotExist('Event has no registrations') + elif action == 'toggle_reduced_fee': + self.enforce_permission(event, permission='payment') + participant_id = request.POST.get('id') + participant = event.participants.get(id=participant_id) + participant.apply_reduced_fee = not participant.apply_reduced_fee + participant.save() elif action == 'confirm_payment': self.enforce_permission(event, permission='payment') participant_id = request.POST.get('id') From 1f674125ec94d70ff66ee6bd26b104d265ba59d6 Mon Sep 17 00:00:00 2001 From: heinzel Date: Fri, 18 Dec 2020 14:30:17 +0100 Subject: [PATCH 34/41] UPD: dav_events: enable save as draft --- .../dav_events/event_create/SummaryForm.html | 2 -- dav_events/views/events.py | 28 ++++++++++++++++++- dav_events/workflow.py | 5 ++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/dav_events/templates/dav_events/event_create/SummaryForm.html b/dav_events/templates/dav_events/event_create/SummaryForm.html index 4c925a4..7bf5704 100644 --- a/dav_events/templates/dav_events/event_create/SummaryForm.html +++ b/dav_events/templates/dav_events/event_create/SummaryForm.html @@ -40,11 +40,9 @@ {% bootstrap_icon 'remove' %}  {% trans 'Abbrechen' %} - {% endbuttons %} {% endblock form-buttons %} diff --git a/dav_events/views/events.py b/dav_events/views/events.py index e98196e..ee68873 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -543,6 +543,18 @@ class EventUpdateStatusView(EventPermissionMixin, generic.DetailView): messages.error(request, message) return HttpResponseRedirect(event.get_absolute_url()) + if not event.workflow.has_reached_status('publishing*') and not event.workflow.has_reached_status('published*'): + cur_pub_date = event.planned_publication_date + real_pub_date, real_pub_issue = event.workflow.plan_publication(event.first_day, event.deadline) + if cur_pub_date != real_pub_date: + if real_pub_date is None: + real_pub_str = _(u'Unverzüglich') + else: + real_pub_str = u'%s (%s)' % (real_pub_date.strftime('%d.%m.%Y'), real_pub_issue) + event.planned_publication_date = real_pub_date + event.save() + messages.warning(request, _(u'Veröffentlichungsdatum wurde angepasst: %s') % real_pub_str) + event.workflow.update_status(status, request.user) if status.startswith('submit'): @@ -602,7 +614,21 @@ class EventUpdateView(EventPermissionMixin, generic.UpdateView): def form_valid(self, form): form.instance.editor = self.request.user - self.object = form.save() + event = form.save() + self.object = event + + if not event.workflow.has_reached_status('publishing*') and not event.workflow.has_reached_status('published*'): + cur_pub_date = event.planned_publication_date + real_pub_date, real_pub_issue = event.workflow.plan_publication(event.first_day, event.deadline) + if cur_pub_date != real_pub_date: + if real_pub_date is None: + real_pub_str = _(u'Unverzüglich') + else: + real_pub_str = u'%s (%s)' % (real_pub_date.strftime('%d.%m.%Y'), real_pub_issue) + event.planned_publication_date = real_pub_date + event.save() + messages.warning(self.request, _(u'Veröffentlichungsdatum wurde angepasst: %s') % real_pub_str) + return HttpResponseRedirect(self.get_success_url()) @method_decorator(login_required) diff --git a/dav_events/workflow.py b/dav_events/workflow.py index 1856480..499b09b 100644 --- a/dav_events/workflow.py +++ b/dav_events/workflow.py @@ -489,9 +489,8 @@ class BasicWorkflow(object): # # Misc logic # - # TODO: is a class method a good idea? - @classmethod - def plan_publication(cls, first_day, deadline=None): + @staticmethod + def plan_publication(first_day, deadline=None): app_config = apps.get_containing_app_config(__package__) if deadline: From 6d5c77ee71fd91669840a41b13ae30378279994d Mon Sep 17 00:00:00 2001 From: heinzel Date: Fri, 18 Dec 2020 15:37:12 +0100 Subject: [PATCH 35/41] FIX #4 --- dav_events/views/events.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dav_events/views/events.py b/dav_events/views/events.py index ee68873..7c9477d 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -688,6 +688,31 @@ class EventCreateView(EventPermissionMixin, generic.FormView): return self.render_to_response(self.get_context_data(form=next_form, event=event)) else: event.editor = self.request.user + + # Check for double submission (seems to happens accidentally if smartphone user reload the submit page) + possible_doublets = models.Event.objects.filter(owner=event.editor, + title=event.title, + first_day=event.first_day) + if possible_doublets.exists(): + accident_period = datetime.datetime.now() - datetime.timedelta(hours=24) + possible_doublets = possible_doublets.filter(created_at__gt=accident_period) + if possible_doublets.exists(): + doublet = possible_doublets.first() + doublet_created_at = doublet.created_at.strftime('%d.%m.%Y %H:%M:%S') + error_msg = _(u'Du hast bereits eine Veranstaltung' + u' mit dem Titel "%(title)s"' + u' für das Datum %(day)s angelegt' + u' (am %(created_at)s).') % {'title': event.title, + 'day': event.first_day, + 'created_at': doublet_created_at} + warn_msg = _(u'Deine eingegebenen Daten sind noch da,' + u' wenn du jetzt auf "Neue Veranstaltung anlegen" klickst.' + u' Du musst aber entweder den Titel oder das Startdatum ändern,' + u' um die Veranstaltung anlegen zu können.') + messages.error(self.request, error_msg) + messages.warning(self.request, warn_msg) + return HttpResponseRedirect(reverse('dav_events:list')) + event.save() if 'submit' in form.data: event.workflow.update_status('submitted', event.owner) From 70c82f9b93d6686a5fda10a294b3599ae3908bd0 Mon Sep 17 00:00:00 2001 From: heinzel Date: Fri, 18 Dec 2020 16:43:23 +0100 Subject: [PATCH 36/41] FIX 6d5c77ee71fd91669840a41b13ae30378279994d --- dav_events/views/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 7c9477d..8946fcb 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -690,7 +690,7 @@ class EventCreateView(EventPermissionMixin, generic.FormView): event.editor = self.request.user # Check for double submission (seems to happens accidentally if smartphone user reload the submit page) - possible_doublets = models.Event.objects.filter(owner=event.editor, + possible_doublets = models.Event.objects.filter(owner=event.owner, title=event.title, first_day=event.first_day) if possible_doublets.exists(): From 5f296ff1c0a8eac98a86fb58f2614314a7d27e8a Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 22 Dec 2020 11:51:59 +0100 Subject: [PATCH 37/41] UPD: dav_auth: refactor the ResetPasswordView for better name --- dav_auth/forms.py | 2 +- dav_auth/templates/dav_auth/forms/login.html | 3 ++- ...t_password.html => recreate_password.html} | 0 dav_auth/tests/test_forms.py | 6 +++--- dav_auth/tests/test_screenshots.py | 16 +++++++-------- dav_auth/tests/test_templates.py | 6 +++--- dav_auth/tests/test_urls.py | 2 +- dav_auth/tests/test_views.py | 20 +++++++++---------- dav_auth/urls.py | 2 +- dav_auth/views.py | 20 ++++++++++++------- 10 files changed, 42 insertions(+), 35 deletions(-) rename dav_auth/templates/dav_auth/forms/{reset_password.html => recreate_password.html} (100%) diff --git a/dav_auth/forms.py b/dav_auth/forms.py index d46ce6c..8c1faa4 100644 --- a/dav_auth/forms.py +++ b/dav_auth/forms.py @@ -62,7 +62,7 @@ class SetPasswordForm(forms.Form): return self.user -class ResetPasswordForm(forms.Form): +class CreateAndSendPasswordForm(forms.Form): username = auth_forms.UsernameField( max_length=254, label=_(u'E-Mail-Adresse'), diff --git a/dav_auth/templates/dav_auth/forms/login.html b/dav_auth/templates/dav_auth/forms/login.html index 3f592fb..f914d3f 100644 --- a/dav_auth/templates/dav_auth/forms/login.html +++ b/dav_auth/templates/dav_auth/forms/login.html @@ -15,7 +15,8 @@
    {% csrf_token %} {% bootstrap_form form %} - + {% buttons %} {% elif event.charge %}   - {% if participant.apply_reduced_fee %}%{% else %}? {% endif %}{% bootstrap_icon 'piggy-bank' %} diff --git a/dav_event_office/templates/dav_event_office/participant_list.html b/dav_event_office/templates/dav_event_office/participant_list.html index c9a9e71..30eabb5 100644 --- a/dav_event_office/templates/dav_event_office/participant_list.html +++ b/dav_event_office/templates/dav_event_office/participant_list.html @@ -90,7 +90,7 @@   ({{ event.charge|floatformat:'-2' }}{% if participant.apply_reduced_fee %} / 2{% endif %} €) {% elif event.charge %} - {% if participant.apply_reduced_fee %}%{% else %}? {% endif %}