diff --git a/dav_events/apps.py b/dav_events/apps.py index 7557a5d..af30b48 100644 --- a/dav_events/apps.py +++ b/dav_events/apps.py @@ -1,6 +1,7 @@ from django.core.exceptions import ImproperlyConfigured from . import signals +from . import workflow from .config import AppConfig as _AppConfig, DefaultSetting DEFAULT_SETTINGS = ( @@ -31,7 +32,5 @@ class AppConfig(_AppConfig): default_settings = DEFAULT_SETTINGS def ready(self): - signals.event_submitted.connect(signals.notify_submitted_event) - signals.event_submitted.connect(signals.notify_to_accept_event) - signals.event_accepted.connect(signals.notify_accepted_event) - signals.event_accepted.connect(signals.notify_to_publish_event) + signals.event_updated.connect(workflow.email_event_update) + signals.event_status_updated.connect(workflow.email_event_status_update) \ No newline at end of file diff --git a/dav_events/emails.py b/dav_events/emails.py index 76eaefa..5d0fceb 100644 --- a/dav_events/emails.py +++ b/dav_events/emails.py @@ -21,7 +21,7 @@ class AbstractMail(object): app_config = apps.get_containing_app_config(__package__) if app_config.settings.email_subject_prefix: s = u'%s %s' % (app_config.settings.email_subject_prefix, s) - s.format(**kwargs) + s = s.format(**kwargs) return s def _get_template(self): @@ -64,6 +64,12 @@ class AbstractEventMail(AbstractMail): self._recipient = recipient self._event = event + def _get_subject(self, **kwargs): + if 'number' not in kwargs: + kwargs['number'] = self._event.get_number() + s = super(AbstractEventMail, self)._get_subject(**kwargs) + return s + def _get_recipients(self): r = u'"{fullname}" <{email}>'.format(fullname=self._recipient.get_full_name(), email=self._recipient.email) @@ -76,6 +82,22 @@ class AbstractEventMail(AbstractMail): return context +class EventUpdatedMail(AbstractEventMail): + _subject = u'{number}: Veranstaltung geändert' + _template_name = 'dav_events/emails/event_updated.txt' + + def __init__(self, diff=None, editor=None, *args, **kwargs): + self._diff = diff + self._editor = editor + super(EventUpdatedMail, self).__init__(*args, **kwargs) + + def _get_context_data(self, extra_context=None): + context = super(EventUpdatedMail, self)._get_context_data(extra_context=extra_context) + context['diff'] = self._diff + context['editor'] = self._editor.get_full_name() + return context + + class EventSubmittedMail(AbstractEventMail): _template_name = 'dav_events/emails/event_submitted.txt' diff --git a/dav_events/models/event.py b/dav_events/models/event.py index ada9929..9048f69 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import datetime +import difflib import logging import os import re @@ -252,6 +253,16 @@ class Event(models.Model): internal_note = models.TextField(blank=True, verbose_name=_(u'Bearbeitungshinweis')) + @property + def editor(self): + if not hasattr(self, '_editor'): + self._editor = None + return self._editor + + @editor.setter + def editor(self, editor): + self._editor = editor + class Meta: verbose_name = _(u'Veranstaltung') verbose_name_plural = _(u'Veranstaltungen') @@ -268,6 +279,7 @@ class Event(models.Model): def save(self, **kwargs): creating = False + original_text = '' if not self.id: user_model = get_user_model() @@ -295,65 +307,96 @@ class Event(models.Model): owner.save() logger.info('Owner created: %s', owner.username) + if self.editor and self.editor.is_authenticated and self.editor != owner: + logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner) self.owner = owner creating = True + else: + original = Event.objects.get(id=self.id) + original_text = original.render_as_text() + + if not self.editor or not self.editor.is_authenticated: + self.editor = self.owner super(Event, self).save(**kwargs) if creating: - self.confirm_status('draft', self.owner) logger.info('Event created: %s', self) + self.confirm_status('draft', self.editor) + else: + modified_text = self.render_as_text() + 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='')) + diff_text = '\n'.join(diff_lines[3:]) + signals.event_updated.send(sender=self.__class__, event=self, diff=diff_text, user=self.editor) + logger.info('Event updated: %s', self) - def update_flags(self): + def _internal_update(self): + if not self.id: + logger.critical('Event._internal_update() was called before Event was saved properly.') + raise Exception('Code is on fire!') + super(Event, self).save() + + def update_flags(self, for_status=None): if not self.id: return + if isinstance(for_status, EventStatus): + code = for_status.code + else: + code = for_status + today = datetime.date.today() midnight = datetime.time(00, 00, 00) - if not self.flags.filter(status__code='draft').exists(): - new_flag = EventFlag(event=self, status=get_event_status('draft'), timestamp=self.created_at) - new_flag.save() - logger.info('Detected draft state of Event %s', self) - - if (self.flags.filter(status__code='publishing').exists() and - not self.flags.filter(status__code='published').exists()): - if not self.planned_publication_date: - flag = self.flags.filter(status__code='publishing').last() - new_status = get_event_status('published') - new_flag = EventFlag(event=self, status=new_status, timestamp=flag.timestamp) + if code in (None, 'draft'): + if not self.flags.filter(status__code='draft').exists(): + new_flag = EventFlag(event=self, status=get_event_status('draft'), timestamp=self.created_at) new_flag.save() - logger.info('Detected published state of Event %s', self) - elif self.planned_publication_date <= today: - new_status = get_event_status('published') - new_timestamp = timezone.make_aware(datetime.datetime.combine(self.planned_publication_date, midnight)) - new_flag = EventFlag(event=self, status=new_status, timestamp=new_timestamp) - new_flag.save() - logger.info('Detected published state of Event %s', self) + logger.info('Detected draft state of Event %s', self) - if not self.flags.filter(status__code='expired').exists(): - expired_at = None + if code in (None, 'published', 'publishing'): + if (self.flags.filter(status__code='publishing').exists() and + not self.flags.filter(status__code='published').exists()): + if not self.planned_publication_date: + flag = self.flags.filter(status__code='publishing').last() + new_status = get_event_status('published') + new_flag = EventFlag(event=self, status=new_status, timestamp=flag.timestamp) + new_flag.save() + logger.info('Detected published state of Event %s', self) + elif self.planned_publication_date <= today: + new_status = get_event_status('published') + new_timestamp = timezone.make_aware(datetime.datetime.combine(self.planned_publication_date, + midnight)) + new_flag = EventFlag(event=self, status=new_status, timestamp=new_timestamp) + new_flag.save() + logger.info('Detected published state of Event %s', self) - if self.alt_last_day: - if self.alt_last_day < today: - expired_at = self.alt_last_day - elif self.last_day: - if self.last_day < today: - expired_at = self.last_day - elif self.alt_first_day: - if self.alt_first_day < today: - expired_at = self.alt_first_day - elif self.first_day and self.first_day < today: - expired_at = self.first_day + if code in (None, 'expired'): + if not self.flags.filter(status__code='expired').exists(): + expired_at = None - if expired_at: - new_timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight)) - new_flag = EventFlag(event=self, status=get_event_status('expired'), timestamp=new_timestamp) - new_flag.save() - logger.info('Detected expired state of Event %s', self) + if self.alt_last_day: + if self.alt_last_day < today: + expired_at = self.alt_last_day + elif self.last_day: + if self.last_day < today: + expired_at = self.last_day + elif self.alt_first_day: + if self.alt_first_day < today: + expired_at = self.alt_first_day + elif self.first_day and self.first_day < today: + expired_at = self.first_day + + if expired_at: + new_timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight)) + new_flag = EventFlag(event=self, status=get_event_status('expired'), timestamp=new_timestamp) + new_flag.save() + logger.info('Detected expired state of Event %s', self) def is_flagged(self, status): - self.update_flags() + self.update_flags(status) if isinstance(status, EventStatus): code = status.code else: @@ -388,7 +431,7 @@ class Event(models.Model): logger.warning('Event.confirm_status(): yet not submitted event got accepted! (Event: %s)', self) if not self.number: self.number = self.get_next_number() - self.save() + self._internal_update() elif code == 'publishing' or code == 'published': if not self.is_flagged('accepted'): logger.warning('Event.confirm_status(): yet not accepted event got published! (Event: %s)', self) @@ -397,17 +440,7 @@ class Event(models.Model): flag = EventFlag(event=self, status=status_obj, user=user) flag.save() logger.info('Flagging status \'%s\' for %s', code, self) - - if code == 'submitted': - signals.event_submitted.send(sender=self.__class__, event=self) - elif code == 'accepted': - signals.event_accepted.send(sender=self.__class__, event=self) - elif code == 'publishing': - signals.event_publishing.send(sender=self.__class__, event=self) - elif code == 'published': - signals.event_published.send(sender=self.__class__, event=self) - elif code == 'expired': - signals.event_expired.send(sender=self.__class__, event=self) + signals.event_status_updated.send(sender=self.__class__, event=self, flag=flag) return flag diff --git a/dav_events/signals.py b/dav_events/signals.py index 1bb0eb9..ec68a9b 100644 --- a/dav_events/signals.py +++ b/dav_events/signals.py @@ -1,61 +1,4 @@ -from django.apps import apps from django.dispatch import Signal -from . import emails - -event_submitted = Signal(providing_args=['event']) -event_accepted = Signal(providing_args=['event']) -event_publishing = Signal(providing_args=['event']) -event_published = Signal(providing_args=['event']) -event_expired = Signal(providing_args=['event']) - - -def notify_submitted_event(sender, **kwargs): - event = kwargs.get('event') - app_config = apps.get_containing_app_config(__package__) - if app_config.settings.enable_email_notifications: - if event.owner.email: - email = emails.EventSubmittedMail(recipient=event.owner, event=event) - email.send() - - -def notify_accepted_event(sender, **kwargs): - event = kwargs.get('event') - app_config = apps.get_containing_app_config(__package__) - if app_config.settings.enable_email_notifications: - if event.owner.email: - email = emails.EventAcceptedMail(recipient=event.owner, event=event) - email.send() - - -def notify_to_accept_event(sender, **kwargs): - event = kwargs.get('event') - app_config = apps.get_containing_app_config(__package__) - if app_config.settings.enable_email_notifications: - from .utils import get_users_by_role - managers = get_users_by_role('manage_all') - managers += get_users_by_role('manage_{}'.format(event.sport.lower())) - OneClickAction = app_config.get_model('OneClickAction') - for user in managers: - if user.email: - action = OneClickAction(command='EA') - action.parameters = '{event},{user}'.format(event=event.id, user=user.id) - action.save() - email = emails.EventToAcceptMail(recipient=user, event=event, accept_action=action) - email.send() - - -def notify_to_publish_event(sender, **kwargs): - event = kwargs.get('event') - app_config = apps.get_containing_app_config(__package__) - if app_config.settings.enable_email_notifications: - from .utils import get_users_by_role - publishers = get_users_by_role('publish_incremental') - OneClickAction = app_config.get_model('OneClickAction') - for user in publishers: - if user.email: - action = OneClickAction(command='EP') - action.parameters = '{event},{user}'.format(event=event.id, user=user.id) - action.save() - email = emails.EventToPublishMail(recipient=user, event=event, confirm_publication_action=action) - email.send() +event_updated = Signal(providing_args=['event', 'diff', 'user']) +event_status_updated = Signal(providing_args=['event', 'flag']) diff --git a/dav_events/templates/dav_events/emails/event_updated.txt b/dav_events/templates/dav_events/emails/event_updated.txt new file mode 100644 index 0000000..2c8ebfe --- /dev/null +++ b/dav_events/templates/dav_events/emails/event_updated.txt @@ -0,0 +1,14 @@ +Hallo {{ recipient.first_name }}, + +{{ editor }} hat die folgende Veranstaltung geändert: + {{ event }} + +Link zur Veranstaltung: + {{ base_url }}{{ event.get_absolute_url }} + +Voraussichtliche Veröffentlichung: {% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}In wenigen Tagen{% endif %} + +---------- +{{ diff }}---------- +{% if internal_note %}Bearbeitungshinweis: +{{ internal_note }}{% endif %} \ No newline at end of file diff --git a/dav_events/views/events.py b/dav_events/views/events.py index 94d801d..33983e2 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -242,6 +242,11 @@ class EventUpdateView(EventPermissionMixin, generic.UpdateView): context['has_permission_publish'] = self.has_permission('publish', obj) return context + def form_valid(self, form): + form.instance.editor = self.request.user + self.object = form.save() + return HttpResponseRedirect(self.get_success_url()) + @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(EventUpdateView, self).dispatch(request, *args, **kwargs) @@ -298,6 +303,7 @@ class EventCreateView(EventPermissionMixin, generic.FormView): next_form = next_form_class(request=self.request) return self.render_to_response(self.get_context_data(form=next_form, event=event)) else: + event.editor = self.request.user event.save() if 'submit' in form.data: event.confirm_status('submitted', event.owner) @@ -309,6 +315,12 @@ class EventCreateView(EventPermissionMixin, generic.FormView): self.clean_session_data() if self.request.user.is_authenticated: next_url = reverse('dav_events:event_list') + if self.request.user != event.owner: + messages.warning(self.request, + u'%s %s' % ( + _(u'Du hast jemand anderen als Tourenleiter eingetragen.'), + _(u'Warum machst du sowas?') + )) elif owner.has_usable_password(): next_url = reverse('dav_events:event_list') else: diff --git a/dav_events/workflow.py b/dav_events/workflow.py new file mode 100644 index 0000000..8416bad --- /dev/null +++ b/dav_events/workflow.py @@ -0,0 +1,79 @@ +from django.apps import apps + +from . import emails + + +def email_event_update(sender, **kwargs): + event = kwargs.get('event') + diff = kwargs.get('diff') + updater = kwargs.get('user') + + app_config = apps.get_containing_app_config(__package__) + if not app_config.settings.enable_email_notifications: + return + + # Who should be informed about the update? + recipients = [event.owner] + if event.is_flagged('submitted'): + # If the event is already submitted, add managers to the recipients. + from .utils import get_users_by_role + recipients += get_users_by_role('manage_all') + recipients += get_users_by_role('manage_{}'.format(event.sport.lower())) + if event.is_flagged('accepted'): + # If the event is already published, add publishers to the recipients. + recipients += get_users_by_role('publish_incremental') + + for recipient in recipients: + if recipient.email and recipient.email != updater.email: + email = emails.EventUpdatedMail(recipient=recipient, event=event, diff=diff, editor=updater) + email.send() + + +def email_event_status_update(sender, **kwargs): + event = kwargs.get('event') + flag = kwargs.get('flag') + + app_config = apps.get_containing_app_config(__package__) + if not app_config.settings.enable_email_notifications: + return + + if flag.status.code == 'submitted': + # Inform event owner about his event (so he can keep the mail as a reminder for the event). + if event.owner.email: + email = emails.EventSubmittedMail(recipient=event.owner, event=event) + email.send() + + # Inform managers that they have to accept the event. + # Also create OneClickActions for all of them and add the link to the mail, + # so they can accept the event with a click into the mail. + from .utils import get_users_by_role + recipients = get_users_by_role('manage_all') + recipients += get_users_by_role('manage_{}'.format(event.sport.lower())) + OneClickAction = app_config.get_model('OneClickAction') + for recipient in recipients: + if recipient.email: + action = OneClickAction(command='EA') + action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id) + action.save() + email = emails.EventToAcceptMail(recipient=recipient, event=event, accept_action=action) + email.send() + + elif flag.status.code == 'accepted': + # Inform event owner about the acceptance of his event. + if event.owner.email: + email = emails.EventAcceptedMail(recipient=event.owner, event=event) + email.send() + + # Inform publishers that they have to publish the event. + # Also create OneClickActions for all of them and add the link to the mail, + # so they can confirm the publication with a click into the mail. + from .utils import get_users_by_role + recipients = get_users_by_role('publish_incremental') + OneClickAction = app_config.get_model('OneClickAction') + for recipient in recipients: + if recipient.email: + action = OneClickAction(command='EP') + action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id) + action.save() + email = emails.EventToPublishMail(recipient=recipient, event=event, confirm_publication_action=action) + email.send()