diff --git a/dav_events/migrations/0034_eventchange.py b/dav_events/migrations/0034_eventchange.py new file mode 100644 index 0000000..a185f07 --- /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 20:15 +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'), ('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)), + ], + options={ + 'ordering': ['event', 'timestamp'], + }, + ), + ] 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/__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..bd0a147 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 @@ -14,14 +15,14 @@ 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 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,50 @@ 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=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, user=self.editor, + diff=self.diff(original, fmt='human_readable')) + + 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) + try: + json.dumps(from_value) + except TypeError: + from_value = str(from_value) + to_value = getattr(self, field_name) + try: + json.dumps(to_value) + except TypeError: + to_value = str(to_value) + if from_value != to_value: + change = { + 'field': field_name, + 'refer': from_value, + 'current': 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() @@ -468,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/models/eventchange.py b/dav_events/models/eventchange.py new file mode 100644 index 0000000..a1f3084 --- /dev/null +++ b/dav_events/models/eventchange.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +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 + + +def get_system_user_id(): + return 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, + default=get_system_user_id, + on_delete=models.SET(get_ghost_user), + related_name='+') + + operation = models.CharField(max_length=20, choices=OPERATION_CHOICES) + 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/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/templates/dav_events/event_detail.html b/dav_events/templates/dav_events/event_detail.html index f4b95c9..21ef44e 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 @@
' \ + u'' \ + u' {timestamp}' \ + u' - ' \ + u' {user}
\n' \ + u'\t{content}\n' \ + u'