From 6ecda70b5426116f99bf6f152c444f65d7266ef1 Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Thu, 22 Nov 2018 18:00:26 +0100 Subject: [PATCH] UPD: improved workflow code. --- dav_events/apps.py | 2 +- dav_events/models/event.py | 131 ++++++------------- dav_events/signals.py | 1 + dav_events/views/events.py | 16 ++- dav_events/workflow.py | 256 +++++++++++++++++++++++++++---------- 5 files changed, 238 insertions(+), 168 deletions(-) diff --git a/dav_events/apps.py b/dav_events/apps.py index e30a866..8dfbc2a 100644 --- a/dav_events/apps.py +++ b/dav_events/apps.py @@ -1,7 +1,6 @@ from django.core.exceptions import ImproperlyConfigured from . import signals -from . import workflow from .config import AppConfig as _AppConfig, DefaultSetting DEFAULT_SETTINGS = ( @@ -32,5 +31,6 @@ class AppConfig(_AppConfig): default_settings = DEFAULT_SETTINGS def ready(self): + from .workflow import workflow signals.event_updated.connect(workflow.send_emails_on_event_update) signals.event_status_updated.connect(workflow.send_emails_on_event_status_update) diff --git a/dav_events/models/event.py b/dav_events/models/event.py index c0fd23b..ed1abf5 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -19,6 +19,7 @@ from .. import choices from .. import config from .. import signals from ..utils import get_ghost_user, get_system_user +from ..workflow import workflow from .eventstatus import EventStatus, get_event_status @@ -255,7 +256,7 @@ class Event(models.Model): def get_absolute_url(self): return reverse('dav_events:event_detail', kwargs={'pk': self.pk}) - def save(self, **kwargs): + def save(self, implicit_update=False, **kwargs): creating = False original_text = '' @@ -289,7 +290,7 @@ 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 - else: + elif not implicit_update: original = Event.objects.get(id=self.id) original_text = original.render_as_text(show_internal_fields=True) @@ -300,98 +301,45 @@ class Event(models.Model): if creating: logger.info('Event created: %s', self) + signals.event_created.send(sender=self.__class__, event=self) self.confirm_status('draft', self.editor) - else: + 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='')) - signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor) logger.info('Event updated: %s', self) + signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor) - def _internal_update(self): - """Safe changes on model instance without sending event_updated signal.""" - 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 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 draft state of Event %s', self) - - 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 code in (None, 'expired'): - if not self.flags.filter(status__code='expired').exists(): - expired_at = None - - 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 set_flag(self, status, **kwargs): + if not isinstance(status, EventStatus): + status = get_event_status(status) + kwargs['event'] = self + kwargs['status'] = status + flag = EventFlag(**kwargs) + flag.save() + logger.info('Flagging status \'%s\' for %s', status.code, self) + return flag def is_flagged(self, status): - self.update_flags(status) if isinstance(status, EventStatus): code = status.code else: code = status + workflow.status_code_update(self, code) if self.flags.filter(status__code=code).exists(): return True return False def get_status(self): - self.update_flags() + workflow.status_code_update(self) last_flag = self.flags.last() if last_flag: return last_flag.status return get_event_status('void') def get_status_codes(self): - self.update_flags() + workflow.status_code_update(self) return [flag.status.code for flag in self.flags.all()] def confirm_status(self, status, user): @@ -404,25 +352,25 @@ class Event(models.Model): if flag: return flag - if code == 'accepted': - if not self.is_flagged('submitted'): - 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._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) + valid, return_code, message = workflow.validate_status_code_update(code, self) + if not valid: + logger.warning(u'Invalid status update to \'%s\': %s Event: %s', code, message, self) - status_obj = get_event_status(code) - flag = EventFlag(event=self, status=status_obj, user=user) - flag.save() - logger.info('Flagging status \'%s\' for %s', code, self) + flag = self.set_flag(status=code, user=user) + + workflow.status_code_update(self, code) signals.event_status_updated.send(sender=self.__class__, event=self, flag=flag) return flag - def get_next_number(self): + def get_number(self): + number = workflow.get_number(self) + if number: + return number + else: + return '%s**/%d' % (self.sport, self.first_day.year % 100) + + def set_next_number(self): counter = 0 year = self.first_day.year @@ -431,8 +379,8 @@ class Event(models.Model): qs = Event.objects.filter(number__isnull=False, sport=self.sport, first_day__gte=year_begin, - first_day__lte=year_end).order_by('-number') - last = qs.first() + first_day__lte=year_end).order_by('number') + last = qs.last() if last: match = re.match(r'^(?P[A-Z])(?P[0-9][0-9]*)/(?P[0-9][0-9]*)', last.number) if match: @@ -440,14 +388,9 @@ class Event(models.Model): counter = int(gd['count']) counter += 1 - n = '%s%02d/%d' % (self.sport, counter, year % 100) - return n - - def get_number(self): - if self.is_flagged('accepted') and self.number: - return self.number - else: - return '%s**/%d' % (self.sport, self.first_day.year % 100) + self.number = '%s%02d/%d' % (self.sport, counter, year % 100) + self.save(implicit_update=True) + return self.number def get_formated_date(self, begin_date=None, end_date=None, format='normalized_long'): if begin_date is None: diff --git a/dav_events/signals.py b/dav_events/signals.py index ec68a9b..1c61115 100644 --- a/dav_events/signals.py +++ b/dav_events/signals.py @@ -1,4 +1,5 @@ from django.dispatch import Signal +event_created = Signal(providing_args=['event']) event_updated = Signal(providing_args=['event', 'diff', 'user']) event_status_updated = Signal(providing_args=['event', 'flag']) diff --git a/dav_events/views/events.py b/dav_events/views/events.py index b4f7db1..6938265 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -18,6 +18,7 @@ from .. import choices from .. import forms from .. import models from ..utils import has_role +from ..workflow import workflow app_config = apps.get_containing_app_config(__package__) logger = logging.getLogger(__name__) @@ -195,19 +196,22 @@ class EventConfirmStatusView(EventPermissionMixin, generic.DetailView): elif status == 'accepted': if not self.has_permission('accept', event): raise PermissionDenied(status) - if not event.is_flagged('submitted'): - messages.error(request, _(u'Veranstaltung ist noch nicht eingereicht.')) - return HttpResponseRedirect(event.get_absolute_url()) elif status == 'publishing' or status == 'published': if not self.has_permission('publish', event): raise PermissionDenied(status) - if not event.is_flagged('accepted'): - messages.error(request, _(u'Veranstaltung ist noch nicht freigegeben.')) - return HttpResponseRedirect(event.get_absolute_url()) else: if not self.has_permission('update', event): raise PermissionDenied(status) + valid, return_code, message = workflow.validate_status_code_update(status, event) + if not valid: + if return_code == 'not-submitted': + message = _(u'Veranstaltung ist noch nicht eingereicht.') + elif return_code == 'not-accepted': + message = _(u'Veranstaltung ist noch nicht freigegeben.') + messages.error(request, message) + return HttpResponseRedirect(event.get_absolute_url()) + event.confirm_status(status, request.user) if status == 'submitted': diff --git a/dav_events/workflow.py b/dav_events/workflow.py index 6ea085b..d62f003 100644 --- a/dav_events/workflow.py +++ b/dav_events/workflow.py @@ -1,89 +1,211 @@ +import datetime import logging from django.apps import apps +from django.utils import timezone from . import emails logger = logging.getLogger(__name__) -def send_emails_on_event_update(sender, **kwargs): - event = kwargs.get('event') - diff = kwargs.get('diff') - updater = kwargs.get('user') +class BasicWorkflow(object): + # + # Status updates + # - app_config = apps.get_containing_app_config(__package__) - if not app_config.settings.enable_email_notifications: - return + @classmethod + def validate_status_code_update(cls, code, event, callback=None, *args, **kwargs): + valid = True + return_code = 'OK' + message = u'OK' + if code == 'accepted': + if not event.is_flagged('submitted'): + valid = False + return_code = 'not-submitted' + message = u'Event is not submitted.' + elif code == 'publishing' or code == 'published': + if not event.is_flagged('accepted'): + valid = False + return_code = 'not-accepted' + message = u'Event is not accepted.' + if callback is not None: + callback(valid, return_code, message, *args, **kwargs) + return valid, return_code, message - if len(diff) < 1: - logger.debug('send_emails_on_event_update(): No diff data -> Skip sending mails.') - return - diff_text = '\n'.join(diff[3:]) + @classmethod + def status_code_update(cls, event, code=None): + if not event.id: + 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') + today = datetime.date.today() + midnight = datetime.time(00, 00, 00) - 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.send() + if code in (None, 'draft'): + # Check if event has a draft flag. + if not event.flags.filter(status__code='draft').exists(): + logger.info('Detected draft state of Event %s', event) + event.set_flag(status='draft', timestamp=event.created_at) + if code in (None, 'accepted'): + # Check if event with accepted flag has a number. + if event.flags.filter(status__code='accepted').exists(): + if not event.number: + event.set_next_number() + logger.info('Setting number on Event %s', event) -def send_emails_on_event_status_update(sender, **kwargs): - event = kwargs.get('event') - flag = kwargs.get('flag') - updator = flag.user + if code in (None, 'published', 'publishing'): + # Check if event with publishing flag should now have a published flag also. + if (event.flags.filter(status__code='publishing').exists() and + not event.flags.filter(status__code='published').exists()): + if not event.planned_publication_date: + logger.info('Detected published state of Event %s', event) + flag = event.flags.filter(status__code='publishing').last() + event.set_flag(status='published', timestamp=flag.timestamp) + elif event.planned_publication_date <= today: + logger.info('Detected published state of Event %s', event) + timestamp = timezone.make_aware(datetime.datetime.combine(event.planned_publication_date, midnight)) + event.set_flag(status='published', timestamp=timestamp) - app_config = apps.get_containing_app_config(__package__) - if not app_config.settings.enable_email_notifications: - return + if code in (None, 'expired'): + # Check if event is expired now and need a expired flag. + if not event.flags.filter(status__code='expired').exists(): + expired_at = None - 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() + if event.alt_last_day: + if event.alt_last_day < today: + expired_at = event.alt_last_day + elif event.last_day: + if event.last_day < today: + expired_at = event.last_day + elif event.alt_first_day: + if event.alt_first_day < today: + expired_at = event.alt_first_day + elif event.first_day and event.first_day < today: + expired_at = event.first_day + + if expired_at: + logger.info('Detected expired state of Event %s', event) + timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight)) + event.set_flag(status='expired', timestamp=timestamp) + + @classmethod + def get_number(cls, event): + if event.number and event.flags.filter(status__code='accepted').exists(): + return event.number + else: + return None + + # + # Permissions + # + @classmethod + def has_permission(cls, event, user, permission): + raise NotImplementedError('not ready yet') + + if user.is_superuser: + return True + + if permission == 'view': + if user == event.owner: + return True + raise Exception('must check roles') + elif permission == 'submit': + if user == event.owner: + return True + elif permission == 'accept': + raise Exception('must check roles') + elif permission == 'publish': + raise Exception('must check roles') + elif permission == 'update': + raise Exception('must check roles') + return False + + # + # Signal handlers + # + + @classmethod + def send_emails_on_event_update(cls, 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 + + if len(diff) < 1: + logger.debug('send_emails_on_event_update(): No diff data -> Skip sending mails.') + return + diff_text = '\n'.join(diff[3:]) + + # 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') - # 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) + if recipient.email and recipient.email != updater.email: + email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff_text) 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, editor=updator) - email.send() + @classmethod + def send_emails_on_event_status_update(cls, sender, **kwargs): + event = kwargs.get('event') + flag = kwargs.get('flag') + updater = flag.user - # 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, editor=updator, - confirm_publication_action=action) + 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, editor=updater) + 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, editor=updater, + confirm_publication_action=action) + email.send() + + +workflow = BasicWorkflow