522 lines
23 KiB
Python
522 lines
23 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),
|
|
'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.'
|
|
|
|
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()
|
|
|
|
#
|
|
# 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 == '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
|