Files
django-dav-events/dav_events/workflow.py
2021-01-07 12:52:21 +01:00

562 lines
25 KiB
Python

# -*- 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('Flagging status \'%s\' for %s', status.code, event)
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('Setting number on Event %s', event)
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')
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)
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<sport>[A-Z])(?P<count>[0-9][0-9]*)/(?P<year>[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()
#
# 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