# -*- coding: utf-8 -*- import datetime import logging import re from django.apps import apps from django.db.models.functions import Length from django.utils import timezone 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 logger = logging.getLogger(__name__) today = datetime.date.today() midnight = datetime.time(00, 00, 00) oneday = datetime.timedelta(1) DEFAULT_EVENT_STATI = { 'void': (0, _(u'Ungültig'), None), 'draft': (10, _(u'Entwurf'), 'info'), 'submitted': (30, _(u'Eingereicht'), 'danger'), 'accepted': (50, _(u'Freigegeben'), 'warning'), 'publishing_facebook': (68, _(u'Veröffentlichung (Facebook)'), 'warning'), 'publishing_web': (69, _(u'Veröffentlichung (Web)'), 'warning'), 'publishing': (70, _(u'Veröffentlichung'), 'warning'), 'published_facebook': (78, _(u'Veröffentlicht (Facebook)'), 'success'), 'published_web': (79, _(u'Veröffentlicht (Web)'), 'success'), 'published': (80, _(u'Veröffentlicht'), 'success'), 'expired': (100, _(u'Ausgelaufen'), None), 'canceled': (101, _(u'Abgesagt'), 'dav-mandarin'), 'realized': (102, _(u'Durchgeführt'), 'dav-lime'), 'cleared': (110, _(u'Abgerechnet'), 'black'), } class BasicWorkflow(object): def __init__(self, event=None): self._event = event # # Status changes # # We use EventFlags to store the status information def _add_flag(self, code, **kwargs): event = self._event status = get_or_create_event_status(code) kwargs['event'] = event 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('Flagged status \'%s\' for %s (ID: %d)', status.code, event, event.id) return flag # TODO: the name/intention of this method is unclear. Could we make it obsolete? def _check_status(self, code=None): event = self._event if not event.id: return 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) self._add_flag('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: self.set_number() logger.info('Assigned number to Event %s (ID: %d)', event, event.id) if code in (None, 'publishing_facebook', 'publishing_web', 'publishing', 'published_facebook', 'published_web', 'published'): if not event.flags.filter(status__code='published').exists(): # Event is not fully published yet. # We use some querysets, just to make code more readable. pub_flags = event.flags.filter(status__code__in=('publishing_facebook', 'publishing_web', 'publishing', 'published_facebook', 'published_web', 'published')).order_by('timestamp') publishing = pub_flags.filter(status__code='publishing') publishing_web = pub_flags.filter(status__code='publishing_web') publishing_facebook = pub_flags.filter(status__code='publishing_facebook') published_web = pub_flags.filter(status__code='published_web') published_facebook = pub_flags.filter(status__code='published_facebook') if not event.planned_publication_date or event.planned_publication_date <= today: # Event should be published now, so we can set the flag. # Event has a planned publication date, so this should be the # time, were the event could be flagged as published. if event.planned_publication_date: timestamp = timezone.make_aware(datetime.datetime.combine( event.planned_publication_date, midnight) ) else: timestamp = None if event.flags.filter(status__code='publishing').exists(): # The publishers have confirmed the (now elapsed) publications date, # so we can flag the complete publication. if not timestamp: timestamp = event.flags.filter(status__code='publishing').last().timestamp logger.info('Detected published state of Event %s', event) self._add_flag('published', timestamp=timestamp) elif ((publishing_web.exists() or published_web.exists()) and (publishing_facebook.exists() or published_facebook.exists())): # All publishers have confirmed the publication date or have published already # so we can flag the complete published state. if not timestamp: timestamp = pub_flags.last().timestamp logger.info('Detected general published state of Event %s', event) self._add_flag('published', timestamp=timestamp) else: if publishing_web.exists() and not published_web.exists(): # One publisher has confirmed the publication date, # so we can flag, that he/she has published. if not timestamp: timestamp = event.flags.filter(status__code='publishing_web').last().timestamp logger.info('Detected published_web state of Event %s', event) self._add_flag('published_web', timestamp=timestamp) if publishing_facebook.exists() and not published_facebook.exists(): # One publisher has confirmed the publication date, # so we can flag, that he/she has published. if not timestamp: timestamp = event.flags.filter(status__code='publishing_facebook').last().timestamp logger.info('Detected published_facebook state of Event %s', event) self._add_flag('published_facebook', timestamp=timestamp) if published_web.exists() and published_facebook.exists(): # All publishers have published, # so we can flag the complete published state. timestamp = pub_flags.last().timestamp logger.info('Detected general published state of Event %s', event) self._add_flag('published', timestamp=timestamp) elif not publishing.exists() and publishing_web.exists() and publishing_facebook.exists(): # Event is not due to be published yet, # does not have a general publishing flag, # but all publishers have confirmed the publication date, # so we set a general publishing flag. logger.info('Detected publishing state of Event %s', event) flags = event.flags.filter(status__code__in=('publishing_web', 'publishing_facebook')) timestamp = flags.order_by('timestamp').last().timestamp self._add_flag('publishing', timestamp=timestamp) 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 event.alt_last_day: if event.alt_last_day < today: expired_at = event.alt_last_day + oneday elif event.last_day: if event.last_day < today: expired_at = event.last_day + oneday elif event.alt_first_day: if event.alt_first_day < today: expired_at = event.alt_first_day + oneday elif event.first_day and event.first_day < today: expired_at = event.first_day + oneday if expired_at: logger.info('Detected expired state of Event %s', event) timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight)) self._add_flag('expired', timestamp=timestamp) def has_reached_status(self, code): self._check_status(code) if code == 'publishing*': codes = ['publishing', 'publishing_web', 'publishing_facebook'] elif code == 'published*': codes = ['published', 'published_web', 'published_facebook'] else: codes = [code] return self._event.flags.filter(status__code__in=codes).exists() def validate_status_update(self, code, callback=None, *args, **kwargs): valid = True return_code = 'OK' message = u'OK' event = self._event if code.startswith('accept'): if not self.has_reached_status('submitted'): valid = False return_code = 'not-submitted' message = u'Event is not submitted.' elif code.startswith('publishing'): if not self.has_reached_status('accepted'): valid = False return_code = 'not-accepted' message = u'Event is not accepted.' elif code.startswith('published'): if event.planned_publication_date and event.planned_publication_date > today: valid = False return_code = 'not-due' message = u'Event is not due to publication.' elif not self.has_reached_status('accepted'): valid = False return_code = 'not-accepted' message = u'Event is not accepted.' elif code.startswith('cancel'): if not self.has_reached_status('submitted'): valid = False return_code = 'not-submitted' message = u'Event is not submitted.' elif self.has_reached_status('realized'): valid = False return_code = 'already-realized' message = u'Event was already realized.' elif code.startswith('realize'): if event.first_day > today: valid = False return_code = 'not-started' message = u'Event has not begun.' elif not self.has_reached_status('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 def update_status(self, code, user): event = self._event flag = event.flags.filter(status__code=code).last() if flag: return flag valid, return_code, message = self.validate_status_update(code) if not valid: logger.warning(u'Invalid status update to \'%s\': %s Event: %s', code, message, event) flag = self._add_flag(code, user=user) self._check_status(code) signals.event_status_updated.send(sender=self.__class__, event=event, flag=flag) return flag def get_status_list(self): event = self._event status_list = [] self._check_status() heaviest_flag = event.flags.order_by('status').last() if heaviest_flag: flags = [] last_status = heaviest_flag.status if last_status.code.startswith('publishing_'): flags += event.flags.filter(status__code='accepted') flags.append(heaviest_flag) elif last_status.code.startswith('published_'): if event.flags.filter(status__code='publishing').exists(): flags += event.flags.filter(status__code='publishing') else: flags += event.flags.filter(status__code='accepted') flags.append(heaviest_flag) elif last_status.code.startswith('clear'): flags += event.flags.filter(status__code__in=('cleared', 'canceled', 'realized')) else: flags.append(heaviest_flag) deferred_publishing = event.planned_publication_date and event.planned_publication_date > today add_publishing_date = False for flag in flags: status_list.append(flag.status) if deferred_publishing and flag.status.code.startswith('publishing'): add_publishing_date = True if add_publishing_date: date_str = event.planned_publication_date.strftime('%d.%m.%Y') label = _(u'Veröffentlichung am {date}').format(date=date_str) status_list.append({'label': label}) return status_list # # Status related event properties # def get_number(self): event = self._event if event.number and event.flags.filter(status__code='accepted').exists(): return event.number else: return '%s**/%d' % (event.sport, event.first_day.year % 100) def set_number(self): event = self._event counter = 0 year = event.first_day.year year_begin = datetime.date(year, 1, 1) year_end = datetime.date(year, 12, 31) qs = event.__class__.objects.filter(number__isnull=False, sport=event.sport, first_day__gte=year_begin, first_day__lte=year_end).annotate(number_length=Length('number')) last = qs.order_by('number_length', 'number').last() if last: match = re.match(r'^(?P[A-Z])(?P[0-9][0-9]*)/(?P[0-9][0-9]*)', last.number) if match: gd = match.groupdict() counter = int(gd['count']) counter += 1 event.number = '%s%02d/%d' % (event.sport, counter, year % 100) event.save(implicit_update=True) return event.number # # Notifications (loose coupled via signals) # def send_emails_on_update(self, diff, updater): event = self._event app_config = apps.get_containing_app_config(__package__) if not app_config.settings.enable_email_on_update: return if not diff: logger.debug('send_emails_on_update(): No diff data -> Skip sending mails.') return # Who should be informed about the update? recipients = [event.owner] if self.has_reached_status('submitted'): # If the event is already submitted, add managers to the recipients. recipients += get_users_by_role('manager_{}'.format(event.sport.lower())) if self.has_reached_status('accepted'): # If the event is already published, add publishers to the recipients. recipients += get_users_by_role('publisher_web') recipients += get_users_by_role('publisher_facebook') for recipient in recipients: if recipient.email and recipient.email != updater.email: email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff) email.send() def send_emails_on_status_update(self, flag): event = self._event updater = flag.user app_config = apps.get_containing_app_config(__package__) if not app_config.settings.enable_email_on_status_update: 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. recipients = get_users_by_role('manager_{}'.format(event.sport.lower())) OneClickAction = app_config.get_model('OneClickAction') for recipient in recipients: if recipient.email: action = OneClickAction(command='EVENT_STATUS_UPDATE') action.parameters = '{event},accepted,{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. # Website recipients = get_users_by_role('publisher_web') status_code = 'publishing_web' OneClickAction = app_config.get_model('OneClickAction') for recipient in recipients: if recipient.email: action = OneClickAction(command='EVENT_STATUS_UPDATE') action.parameters = '{event},{status_code},{user}'.format(event=event.id, status_code=status_code, user=recipient.id) action.save() email = emails.EventToPublishWebMail(recipient=recipient, event=event, editor=updater, confirm_publication_action=action) email.send() # Facebook recipients = get_users_by_role('publisher_facebook') status_code = 'publishing_facebook' OneClickAction = app_config.get_model('OneClickAction') for recipient in recipients: if recipient.email: action = OneClickAction(command='EVENT_STATUS_UPDATE') action.parameters = '{event},{status_code},{user}'.format(event=event.id, status_code=status_code, user=recipient.id) action.save() email = emails.EventToPublishFacebookMail(recipient=recipient, event=event, editor=updater, confirm_publication_action=action) email.send() elif flag.status.code == 'canceled': # Who should be informed about the cancelation? recipients = [event.owner] recipients += get_users_by_role('manager_super') recipients += get_users_by_role('office') for recipient in recipients: if recipient.email and recipient.email != updater.email: email = emails.EventCanceledMail(recipient=recipient, event=event, editor=updater) email.send() def send_emails_on_registration_closed(self, updater): event = self._event app_config = apps.get_containing_app_config(__package__) if not app_config.settings.enable_email_on_registration_closed: return recipients = get_users_by_role('publisher_web') recipients += get_users_by_role('publisher_facebook') for recipient in recipients: if recipient.email: email = emails.EventRegistrationClosedMail(recipient=recipient, event=event, editor=updater) email.send() # # Permissions # def has_permission(self, user, permission): if user.is_superuser: return True event = self._event if permission == 'view': if user == event.owner: return True if has_role(user, 'manager_super'): return True if has_role(user, 'manager_{}'.format(event.sport.lower())): return True if has_role(user, 'publisher') and self.has_reached_status('accepted'): return True if has_role(user, 'office') and self.has_reached_status('submitted'): return True elif permission == 'submit': if user == event.owner: return True elif permission == 'accept': if has_role(user, 'manager_super'): return True if has_role(user, 'manager_{}'.format(event.sport.lower())): return True elif permission == 'publish': if has_role(user, 'publisher'): return True elif permission == 'cancel': if user == event.owner: return True if has_role(user, 'manager_super'): return True if has_role(user, 'office'): return True elif permission == 'realize': if user == event.owner: return True elif permission == 'clear': if has_role(user, 'manager_super'): return True elif permission == 'update': if not self.has_reached_status('submitted'): if user == event.owner: return True elif not self.has_reached_status('accepted'): if has_role(user, 'manager_super'): return True if has_role(user, 'manager_{}'.format(event.sport.lower())): return True elif has_role(user, 'publisher'): return True elif permission == 'view-participants': if user == event.owner: return True if has_role(user, 'manager_super'): return True if has_role(user, 'manager_{}'.format(event.sport.lower())): return True if has_role(user, 'publisher'): return True if has_role(user, 'office'): return True elif permission == 'update-participants': if user == event.owner: return True if has_role(user, 'manager_super'): return True elif permission == 'update-registration': if user == event.owner: return True if has_role(user, 'manager_super'): return True if has_role(user, 'publisher'): return True elif permission == 'payment': if has_role(user, 'office'): return True return False # TODO: is a class method a good idea? @classmethod def has_global_permission(cls, user, permission): if user.is_superuser: return True if permission == 'export': return has_role(user, 'publisher') elif permission == 'payment': return has_role(user, 'office') return False # # Misc logic # @staticmethod def plan_publication(first_day, deadline=None): app_config = apps.get_containing_app_config(__package__) if deadline: publication_deadline = deadline - datetime.timedelta(app_config.settings.publish_before_deadline_days) else: publication_deadline = first_day - datetime.timedelta(app_config.settings.publish_before_begin_days) for year in (today.year, today.year + 1): for issue in app_config.settings.publish_issues: if not ('issue' in issue and 'release' in issue and 'deadline' in issue): logger.error('workflow.plan_publication(): invalid configured issue.') continue issue_release = datetime.date(year, issue['release'][1], issue['release'][0]) if issue_release < today: continue issue_deadline = datetime.date(year, issue['deadline'][1], issue['deadline'][0]) if issue_deadline > issue_release: issue_deadline = datetime.date(year - 1, issue['deadline'][1], issue['deadline'][0]) if publication_deadline > issue_release and today <= issue_deadline: return issue_release, u'%s/%s' % (issue['issue'], year) return None, None DefaultWorkflow = BasicWorkflow