From 9addc237bd2c455f3dc7c60f81ab4f1e82a2e12b Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 15:28:48 +0200 Subject: [PATCH 1/6] 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): -- 2.52.0 From 10ea6affafbe61c6adde4278115ada3cf15553ce Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 18:24:38 +0200 Subject: [PATCH 2/6] 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) -- 2.52.0 From 09bfbeedc4ac65ba09be0326e27b3c45f33e8bab Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 19:02:55 +0200 Subject: [PATCH 3/6] 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) -- 2.52.0 From 96d6dc72fbebeac3b7a78b5cace67400a5f6bddf Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 19:36:42 +0200 Subject: [PATCH 4/6] 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: -- 2.52.0 From 5237d815517f48d072cbb6bc43f40c918269d310 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 29 Sep 2020 22:19:43 +0200 Subject: [PATCH 5/6] 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 -- 2.52.0 From 37519d59e5a0428f000dd25ba7e30db2cb9fcda5 Mon Sep 17 00:00:00 2001 From: heinzel Date: Tue, 3 Nov 2020 11:18:48 +0100 Subject: [PATCH 6/6] 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 """ -- 2.52.0