From 3c7ef05099d444b8ab5c0d229c616704f61b362e Mon Sep 17 00:00:00 2001 From: Jens Kleineheismann Date: Wed, 4 Jul 2018 16:56:13 +0200 Subject: [PATCH] Added a model for event state (including data migration, signal based notifications, etc.) --- README.rst | 2 +- dav_events/admin.py | 20 +- dav_events/apps.py | 7 + dav_events/emails.py | 36 +- dav_events/forms/generic.py | 8 +- dav_events/migrations/0001_initial.py | 2 +- .../migrations/0002_auto_20180124_1514.py | 2 +- .../migrations/0004_auto_20180124_1650.py | 2 +- .../migrations/0013_auto_20180224_1401.py | 2 +- .../migrations/0019_auto_20180306_2101.py | 6 +- .../migrations/0020_auto_20180704_1202.py | 57 +++ dav_events/migrations/0021_create_flags.py | 61 ++++ dav_events/models/__init__.py | 3 + dav_events/{models.py => models/event.py} | 330 +++++++----------- dav_events/models/eventstatus.py | 60 ++++ dav_events/models/oneclickaction.py | 145 ++++++++ dav_events/signals.py | 61 ++++ dav_events/static/dav_events/css/local.css | 5 + .../templates/dav_events/event/default.html | 15 +- .../dav_events/event_create/SummaryForm.html | 8 +- .../templates/dav_events/event_detail.html | 121 ++++--- .../templates/dav_events/event_list.html | 15 +- .../dav_events/event_update_form.html | 6 +- dav_events/urls.py | 5 +- dav_events/utils.py | 9 + dav_events/validators.py | 5 + dav_events/views/events.py | 74 ++-- 27 files changed, 712 insertions(+), 355 deletions(-) create mode 100644 dav_events/migrations/0020_auto_20180704_1202.py create mode 100644 dav_events/migrations/0021_create_flags.py create mode 100644 dav_events/models/__init__.py rename dav_events/{models.py => models/event.py} (74%) create mode 100644 dav_events/models/eventstatus.py create mode 100644 dav_events/models/oneclickaction.py create mode 100644 dav_events/signals.py create mode 100644 dav_events/validators.py diff --git a/README.rst b/README.rst index 3de63fb..1dc082a 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ INSTALLATION The creation of a separated python environment is very easy with the virtualenv tool (a python package). - If you decide to not use virtualenv, proceed with step 2. + If you decide to not use virtualenv, confirm_status with step 2. - Create the python environment in a directory called ./env/python: diff --git a/dav_events/admin.py b/dav_events/admin.py index cbeb122..eb98b30 100644 --- a/dav_events/admin.py +++ b/dav_events/admin.py @@ -1,15 +1,23 @@ from django.contrib import admin -from .models import Event, OneClickAction +from .models import EventStatus, EventFlag, Event, OneClickAction -class EventAdmin(admin.ModelAdmin): +@admin.register(EventStatus) +class EventStatusAdmin(admin.ModelAdmin): pass +class EventFlagInline(admin.TabularInline): + model = EventFlag + extra = 1 + + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + inlines = [EventFlagInline] + + +@admin.register(OneClickAction) class OneClickActionAdmin(admin.ModelAdmin): pass - - -admin.site.register(Event, EventAdmin) -admin.site.register(OneClickAction, OneClickActionAdmin) diff --git a/dav_events/apps.py b/dav_events/apps.py index 99d17ca..7557a5d 100644 --- a/dav_events/apps.py +++ b/dav_events/apps.py @@ -1,5 +1,6 @@ from django.core.exceptions import ImproperlyConfigured +from . import signals from .config import AppConfig as _AppConfig, DefaultSetting DEFAULT_SETTINGS = ( @@ -28,3 +29,9 @@ class AppConfig(_AppConfig): name = 'dav_events' verbose_name = u'DAV Veranstaltungen' 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) diff --git a/dav_events/emails.py b/dav_events/emails.py index 7a2052f..872df5e 100644 --- a/dav_events/emails.py +++ b/dav_events/emails.py @@ -5,38 +5,20 @@ from django.core.exceptions import ImproperlyConfigured from django.core.mail import EmailMessage from django.template.loader import get_template -from .utils import get_users_by_role - -app_config = apps.get_containing_app_config(__package__) logger = logging.getLogger(__name__) -def get_recipients(task, sport=None): - users = [] - if task == 'accept': - role = 'manage_all' - users += get_users_by_role(role) - - if sport: - role = 'manage_{}'.format(sport.lower()) - users += get_users_by_role(role) - - elif task == 'publish': - role = 'incremental_publisher' - users += get_users_by_role(role) - else: - raise ValueError('utils.get_recipients(): invalid value for task') - - return [u'{name} <{addr}>'.format(name=u.get_full_name(), addr=u.email) for u in users] - - class AbstractMail(object): - _sender = app_config.settings.email_sender _subject = u'' _template_name = None + def _get_sender(self): + app_config = apps.get_containing_app_config(__package__) + return app_config.settings.email_sender + def _get_subject(self, **kwargs): s = self._subject + 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) @@ -48,6 +30,7 @@ class AbstractMail(object): return get_template(self._template_name) def _get_context_data(self, extra_context=None): + app_config = apps.get_containing_app_config(__package__) context = { 'base_url': app_config.settings.email_base_url, } @@ -66,7 +49,7 @@ class AbstractMail(object): def send(self): subject = self._get_subject() body = self._get_body() - sender = self._sender + sender = self._get_sender() recipients = self._get_recipients() emo = EmailMessage(subject=subject, body=body, from_email=sender, to=recipients) @@ -92,11 +75,6 @@ class AbstractEventMail(AbstractMail): context.update(self._event.get_template_context()) return context - def send(self): - if not app_config.settings.enable_email_notifications: - return None - return super(AbstractEventMail, self).send() - class NewEventMail(AbstractEventMail): _template_name = 'dav_events/emails/new_event.txt' diff --git a/dav_events/forms/generic.py b/dav_events/forms/generic.py index ecf2fff..18a7546 100644 --- a/dav_events/forms/generic.py +++ b/dav_events/forms/generic.py @@ -2,6 +2,7 @@ import logging from django import forms from django.apps import apps from django.core.exceptions import ImproperlyConfigured +from django.db.models.manager import Manager from .. import converters @@ -165,8 +166,11 @@ class ModelMixin(object): data = {} for field in instance._meta.get_fields(): v = getattr(instance, field.name) - if v is not None: - data[field.name] = getattr(instance, field.name) + if v is None: + continue + if isinstance(v, Manager): + continue + data[field.name] = getattr(instance, field.name) self.is_bound = True self.data = data return data diff --git a/dav_events/migrations/0001_initial.py b/dav_events/migrations/0001_initial.py index 212c5f5..fcb13f1 100644 --- a/dav_events/migrations/0001_initial.py +++ b/dav_events/migrations/0001_initial.py @@ -79,7 +79,7 @@ class Migration(migrations.Migration): ('trainer_3_phone', models.CharField(blank=True, max_length=250)), ('charge', models.FloatField(default=0)), ('additional_costs', models.CharField(blank=True, max_length=250)), - ('owner', models.ForeignKey(null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='events', to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='events', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ['first_day'], diff --git a/dav_events/migrations/0002_auto_20180124_1514.py b/dav_events/migrations/0002_auto_20180124_1514.py index 721c3bb..b1cd6b3 100644 --- a/dav_events/migrations/0002_auto_20180124_1514.py +++ b/dav_events/migrations/0002_auto_20180124_1514.py @@ -19,7 +19,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='event', name='accepted_by', - field=models.ForeignKey(null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='event', diff --git a/dav_events/migrations/0004_auto_20180124_1650.py b/dav_events/migrations/0004_auto_20180124_1650.py index be18d0b..0c60eea 100644 --- a/dav_events/migrations/0004_auto_20180124_1650.py +++ b/dav_events/migrations/0004_auto_20180124_1650.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='accepted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='event', diff --git a/dav_events/migrations/0013_auto_20180224_1401.py b/dav_events/migrations/0013_auto_20180224_1401.py index 0701453..11bedd3 100644 --- a/dav_events/migrations/0013_auto_20180224_1401.py +++ b/dav_events/migrations/0013_auto_20180224_1401.py @@ -28,6 +28,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='event', name='published_by', - field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), ), ] diff --git a/dav_events/migrations/0019_auto_20180306_2101.py b/dav_events/migrations/0019_auto_20180306_2101.py index 20a178e..c0893fb 100644 --- a/dav_events/migrations/0019_auto_20180306_2101.py +++ b/dav_events/migrations/0019_auto_20180306_2101.py @@ -29,7 +29,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='accepted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Freigegeben durch'), + field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Freigegeben durch'), ), migrations.AlterField( model_name='event', @@ -239,7 +239,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='owner', - field=models.ForeignKey(null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='events', to=settings.AUTH_USER_MODEL, verbose_name='Ersteller'), + field=models.ForeignKey(null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='events', to=settings.AUTH_USER_MODEL, verbose_name='Ersteller'), ), migrations.AlterField( model_name='event', @@ -269,7 +269,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='publication_confirmed_by', - field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.models.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Ver\xf6ffentlichung best\xe4tigt durch'), + field=models.ForeignKey(blank=True, null=True, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Ver\xf6ffentlichung best\xe4tigt durch'), ), migrations.AlterField( model_name='event', diff --git a/dav_events/migrations/0020_auto_20180704_1202.py b/dav_events/migrations/0020_auto_20180704_1202.py new file mode 100644 index 0000000..be25515 --- /dev/null +++ b/dav_events/migrations/0020_auto_20180704_1202.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-07-04 12:02 +from __future__ import unicode_literals + +import dav_events.models.event +import dav_events.utils +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('dav_events', '0019_auto_20180306_2101'), + ] + + operations = [ + migrations.CreateModel( + name='EventFlag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flags', to='dav_events.Event')), + ], + options={ + 'ordering': ['event', 'status', 'timestamp'], + }, + ), + migrations.CreateModel( + name='EventStatus', + fields=[ + ('code', models.CharField(max_length=254, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(b'^[0-9a-z]*$', b'Only characters a-z and digits 0-9 are allowed.')])), + ('severity', models.IntegerField(unique=True)), + ('label', models.CharField(max_length=254, unique=True)), + ('bootstrap_context', models.CharField(blank=True, choices=[(b'default', b'default'), (b'primary', b'primary'), (b'success', b'success'), (b'info', b'info'), (b'warning', b'warning'), (b'danger', b'danger')], max_length=20)), + ], + options={ + 'ordering': ['severity'], + 'verbose_name': 'Veranstaltungsstatus', + 'verbose_name_plural': 'Veranstaltungsstati', + }, + ), + migrations.AddField( + model_name='eventflag', + name='status', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='dav_events.EventStatus'), + ), + migrations.AddField( + model_name='eventflag', + name='user', + field=models.ForeignKey(default=dav_events.models.event.get_system_user_id, on_delete=models.SET(dav_events.utils.get_ghost_user), related_name='+', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/dav_events/migrations/0021_create_flags.py b/dav_events/migrations/0021_create_flags.py new file mode 100644 index 0000000..4a0ef37 --- /dev/null +++ b/dav_events/migrations/0021_create_flags.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +from dav_events.models.eventstatus import get_event_status + + +def create_stati(apps, schema_editor): + l = ('draft', 'submitted', 'accepted', 'publishing', 'published', 'expired') + for c in l: + get_event_status(c) + + +def create_flags(apps, schema_editor): + EventStatus = apps.get_model('dav_events', 'EventStatus') + EventFlag = apps.get_model('dav_events', 'EventFlag') + Event = apps.get_model('dav_events', 'Event') + for event in Event.objects.all(): + if not len(event.flags.filter(status__code='draft')): + status = EventStatus.objects.get(code='draft') + flag = EventFlag(event=event, status=status, timestamp=event.created_at, user=event.owner) + flag.save() + + if not len(event.flags.filter(status__code='submitted')): + status = EventStatus.objects.get(code='submitted') + flag = EventFlag(event=event, status=status, timestamp=event.created_at, user=event.owner) + flag.save() + + if event.accepted and not len(event.flags.filter(status__code='accepted')): + status = EventStatus.objects.get(code='accepted') + flag = EventFlag(event=event, status=status, timestamp=event.accepted_at, user=event.accepted_by) + flag.save() + + if event.publication_confirmed: + if event.planned_publication_date: + if not len(event.flags.filter(status__code='publishing')): + status = EventStatus.objects.get(code='publishing') + flag = EventFlag(event=event, status=status, + timestamp=event.publication_confirmed_at, + user=event.publication_confirmed_by) + flag.save() + else: + if not len(event.flags.filter(status__code='published')): + status = EventStatus.objects.get(code='published') + flag = EventFlag(event=event, status=status, + timestamp=event.publication_confirmed_at, + user=event.publication_confirmed_by) + flag.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0020_auto_20180704_1202'), + ] + + operations = [ + migrations.RunPython(create_stati), + migrations.RunPython(create_flags), + ] diff --git a/dav_events/models/__init__.py b/dav_events/models/__init__.py new file mode 100644 index 0000000..e2f3afe --- /dev/null +++ b/dav_events/models/__init__.py @@ -0,0 +1,3 @@ +from .event import Event, EventFlag +from .eventstatus import EventStatus +from .oneclickaction import OneClickAction diff --git a/dav_events/models.py b/dav_events/models/event.py similarity index 74% rename from dav_events/models.py rename to dav_events/models/event.py index c5db4f7..89d7595 100644 --- a/dav_events/models.py +++ b/dav_events/models/event.py @@ -4,7 +4,6 @@ import logging import os import re import unicodedata -import uuid from babel.dates import format_date from django.conf import settings from django.contrib.auth import get_user_model @@ -12,19 +11,22 @@ from django.core.urlresolvers import reverse from django.db import models from django.template.loader import get_template from django.utils import timezone -from django.utils.translation import get_language, ugettext, ugettext_lazy as _ +from django.utils.translation import get_language, ugettext_lazy as _ from django_countries.fields import CountryField -from . import choices -from . import config -from . import emails -from .utils import get_users_by_role +from .. import choices +from .. import config +from .. import signals +from ..utils import get_ghost_user, get_system_user + +from .eventstatus import EventStatus, get_event_status + logger = logging.getLogger(__name__) -def get_ghost_user(): - return get_user_model().objects.get_or_create(username='-deleted-')[0] +def get_system_user_id(): + return get_system_user().id class Event(models.Model): @@ -294,90 +296,115 @@ class Event(models.Model): super(Event, self).save(**kwargs) if creating: + self.confirm_status('draft', self.owner) logger.info('Event created: %s', self) - managers = get_users_by_role('manage_all') - managers += get_users_by_role('manage_{}'.format(self.sport.lower())) - for user in managers: - if user.email: - action = OneClickAction(command='EA') - action.parameters = '{event},{user}'.format(event=self.id, user=user.id) - action.save() - email = emails.EventToAcceptMail(recipient=user, event=self, accept_action=action) - email.send() + def update_flags(self): + if not self.id: + return - if self.owner.email: - email = emails.NewEventMail(recipient=self.owner, event=self) - email.send() + today = datetime.date.today() + midnight = datetime.time(00, 00, 00) - def accept(self, user=None): - if not self.accepted: - if not self.number: - self.number = self.get_next_number() - self.accepted = True - self.accepted_at = timezone.now() - if user: - self.accepted_by = user - else: - logger.warning('Event.accept(): no user given! (Event: %s)', self.event) - self.save() - logger.info('Event is accepted: %s', self) + 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) - publishers = get_users_by_role('publish_incremental') - for user in publishers: - if user.email: - action = OneClickAction(command='EP') - action.parameters = '{event},{user}'.format(event=self.id, user=user.id) - action.save() - email = emails.EventToPublishMail(recipient=user, event=self, confirm_publication_action=action) - email.send() + 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.owner.email: - email = emails.EventAcceptedMail(recipient=self.owner, event=self) - email.send() + if not self.flags.filter(status__code='expired').exists(): + expired_at = None - return self.number + 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() + if isinstance(status, EventStatus): + code = status.code else: - return None - - def confirm_publication(self, user=None): - if not self.accepted: - logger.warning('Event.confirm_publication(): event is not accepted yet! (Event: %s)', self.event) - - if not self.publication_confirmed: - self.publication_confirmed = True - self.publication_confirmed_at = timezone.now() - if user: - self.publication_confirmed_by = user - else: - logger.warning('Event.confirm_publication(): no user given! (Event: %s)', self.event) - self.save() - logger.info('Event is published: %s', self) + code = status + if self.flags.filter(status__code=code).exists(): + return True + return False def get_status(self): - today = datetime.date.today() - if self.alt_last_day: - if self.alt_last_day < today: - return 'expired' - elif self.last_day: - if self.last_day < today: - return 'expired' - elif self.alt_first_day: - if self.alt_first_day < today: - return 'expired' - elif self.first_day and self.first_day < today: - return 'expired' + self.update_flags() + last_flag = self.flags.last() + if last_flag: + return last_flag.status + return get_event_status('void') - if self.publication_confirmed and self.planned_publication_date and self.planned_publication_date > today: - return 'publishing' - elif self.publication_confirmed: - return 'published' - elif self.accepted: - return 'accepted' - elif self.owner: - return 'submitted' + def get_status_codes(self): + self.update_flags() + return [flag.status.code for flag in self.flags.all()] - return 'draft' + def confirm_status(self, status, user): + if isinstance(status, EventStatus): + code = status.code + else: + code = status + + flag = self.flags.filter(status__code=code).last() + 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.save() + 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) + + 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) + + 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) + + return flag def get_next_number(self): counter = 0 @@ -576,134 +603,23 @@ class Event(models.Model): return template.render(self.get_template_context()) -class OneClickAction(models.Model): - COMMANDS = ( - ('EA', 'accept event'), - ('EP', 'confirm publication of an event'), - ('EL', 'login and go to event list') - ) - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - created_at = models.DateTimeField(auto_now_add=True) - - allow_repeat = models.BooleanField(default=False) - - done = models.BooleanField(default=False) - done_at = models.DateTimeField(blank=True, - null=True) - - command = models.CharField(max_length=2, choices=COMMANDS) - - parameters = models.TextField(blank=True) +class EventFlag(models.Model): + event = models.ForeignKey(Event, related_name='flags') + status = models.ForeignKey(EventStatus, + on_delete=models.PROTECT, + related_name='+') + timestamp = models.DateTimeField(default=timezone.now) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + default=get_system_user_id, + on_delete=models.SET(get_ghost_user), + related_name='+') class Meta: - verbose_name = _(u'One-Click-Action') + ordering = ['event', 'status', 'timestamp'] def __unicode__(self): - s = u'{command}({parameters}) - {description}'.format(description=self.get_command_display(), - command=self.command, - parameters=self.parameters) - if self.done and not self.allow_repeat: - s += u' - done' - return s - - def get_absolute_url(self): - return reverse('dav_events:action_run', kwargs={'pk': self.pk}) - - def run(self): - result = {} - if self.done and not self.allow_repeat: - result['context'] = { - 'status': 'warning', - 'message': ugettext(u'Diese Aktion hast du bereits ausgeführt.'), - } - return result - - logger.info('OneClickAction.run(): %s(%s)', self.command, self.parameters) - if self.command == 'EA': - text = u'' - try: - event_id, user_id = self.parameters.split(',') - event = Event.objects.get(id=event_id) - user = get_user_model().objects.get(id=user_id) - number = event.accept(user) - if number: - status = 'success' - message = ugettext(u'Veranstaltung freigegeben.') - text = unicode(event) - else: - status = 'info' - message = (ugettext(u'Veranstaltung wurde bereits von %(fullname)s freigegeben.') % - {'fullname': event.accepted_by.get_full_name()}) - text = unicode(event) - text += u'\n' - text += (ugettext(u'Freigegeben am: %(date)s') % - {'date': event.accepted_at.strftime('%d.%m.%Y %H:%M:%S')}) - - self.done = True - self.done_at = timezone.now() - self.save() - except Exception as e: - status = 'danger' - message = str(e) - logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) - - result['context'] = { - 'status': status, - 'message': message, - 'text': text, - } - elif self.command == 'EP': - text = u'' - try: - event_id, user_id = self.parameters.split(',') - event = Event.objects.get(id=event_id) - user = get_user_model().objects.get(id=user_id) - if event.publication_confirmed: - status = 'info' - message = (ugettext(u'Veröffentlichung wurde bereits von %(fullname)s bestätigt.') % - {'fullname': event.publication_confirmed_by.get_full_name()}) - text = unicode(event) - text += u'\n' - text += (ugettext(u'Bestätigt am: %(date)s') % - {'date': event.publication_confirmed_at.strftime('%d.%m.%Y %H:%M:%S')}) - else: - event.confirm_publication(user) - status = 'success' - message = ugettext(u'Veröffentlichung registriert.') - text = unicode(event) - - self.done = True - self.done_at = timezone.now() - self.save() - except Exception as e: - status = 'danger' - message = str(e) - logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) - - result['context'] = { - 'status': status, - 'message': message, - 'text': text, - } - elif self.command == 'EL': - try: - user_id = self.parameters - user = get_user_model().objects.get(id=user_id) - url = reverse('dav_events:event_list') - result['location'] = url - result['login'] = user - except Exception as e: - message = str(e) - logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) - result['context'] = { - 'status': 'danger', - 'message': message, - } - else: - result['context'] = { - 'status': 'danger', - 'message': ugettext(u'Invalid Command. Code on fire!'), - } - - return result + s = u'{status} - {timestamp}' + if self.user: + s += u' by user {user}' + return s.format(status=self.status, timestamp=self.timestamp.strftime('%d.%m.%Y %H:%M:%S'), + user=self.user) diff --git a/dav_events/models/eventstatus.py b/dav_events/models/eventstatus.py new file mode 100644 index 0000000..fd13180 --- /dev/null +++ b/dav_events/models/eventstatus.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from ..validators import LowerAlphanumericValidator + +BOOTSTRAP_CONTEXT_CHOICES = ( + ('default', 'default'), + ('primary', 'primary'), + ('success', 'success'), + ('info', 'info'), + ('warning', 'warning'), + ('danger', 'danger'), +) +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': (70, _(u'Veröffentlichung'), 'warning'), + 'published': (80, _(u'Veröffentlicht'), 'success'), + 'expired': (100, _(u'Ausgelaufen'), None), +} + + +def get_event_status(code): + try: + obj = EventStatus.objects.get(code=code) + except EventStatus.DoesNotExist as e: + if code not in DEFAULT_EVENT_STATI: + raise e + severity = DEFAULT_EVENT_STATI[code][0] + label = DEFAULT_EVENT_STATI[code][1] + obj = EventStatus(code=code, severity=severity, label=label) + if DEFAULT_EVENT_STATI[code][2]: + obj.bootstrap_context = DEFAULT_EVENT_STATI[code][2] + obj.save() + return obj + + +class EventStatus(models.Model): + code = models.CharField(primary_key=True, max_length=254, validators=[LowerAlphanumericValidator]) + severity = models.IntegerField(unique=True) + label = models.CharField(unique=True, max_length=254) + bootstrap_context = models.CharField(max_length=20, blank=True, choices=BOOTSTRAP_CONTEXT_CHOICES) + + class Meta: + verbose_name = _(u'Veranstaltungsstatus') + verbose_name_plural = _(u'Veranstaltungsstati') + ordering = ['severity'] + + def __unicode__(self): + return u'{label} ({severity} {code})'.format(code=self.code, + severity=self.severity, + label=self.label) + + def get_bootstrap_label(self): + context = self.bootstrap_context or 'default' + return u'{label}'.format(context=context, + label=self.label) diff --git a/dav_events/models/oneclickaction.py b/dav_events/models/oneclickaction.py new file mode 100644 index 0000000..5d49e79 --- /dev/null +++ b/dav_events/models/oneclickaction.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +import logging +import uuid +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext, ugettext_lazy as _ + +from .event import Event + +logger = logging.getLogger(__name__) + + +class OneClickAction(models.Model): + COMMANDS = ( + ('EA', 'accept event'), + ('EP', 'confirm publication of an event'), + ('EL', 'login and go to event list') + ) + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + + allow_repeat = models.BooleanField(default=False) + + done = models.BooleanField(default=False) + done_at = models.DateTimeField(blank=True, + null=True) + + command = models.CharField(max_length=2, choices=COMMANDS) + + parameters = models.TextField(blank=True) + + class Meta: + verbose_name = _(u'One-Click-Action') + + def __unicode__(self): + s = u'{command}({parameters}) - {description}'.format(description=self.get_command_display(), + command=self.command, + parameters=self.parameters) + if self.done and not self.allow_repeat: + s += u' - done' + return s + + def get_absolute_url(self): + return reverse('dav_events:action_run', kwargs={'pk': self.pk}) + + def run(self): + result = {} + if self.done and not self.allow_repeat: + result['context'] = { + 'status': 'warning', + 'message': ugettext(u'Diese Aktion hast du bereits ausgeführt.'), + } + return result + + logger.info('OneClickAction.run(): %s(%s)', self.command, self.parameters) + if self.command == 'EA': + text = u'' + try: + event_id, user_id = self.parameters.split(',') + event = Event.objects.get(id=event_id) + user = get_user_model().objects.get(id=user_id) + number = event.accept(user) + if number: + status = 'success' + message = ugettext(u'Veranstaltung freigegeben.') + text = unicode(event) + else: + status = 'info' + message = (ugettext(u'Veranstaltung wurde bereits von %(fullname)s freigegeben.') % + {'fullname': event.accepted_by.get_full_name()}) + text = unicode(event) + text += u'\n' + text += (ugettext(u'Freigegeben am: %(date)s') % + {'date': event.accepted_at.strftime('%d.%m.%Y %H:%M:%S')}) + + self.done = True + self.done_at = timezone.now() + self.save() + except Exception as e: + status = 'danger' + message = str(e) + logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) + + result['context'] = { + 'status': status, + 'message': message, + 'text': text, + } + elif self.command == 'EP': + text = u'' + try: + event_id, user_id = self.parameters.split(',') + event = Event.objects.get(id=event_id) + user = get_user_model().objects.get(id=user_id) + if event.publication_confirmed: + status = 'info' + message = (ugettext(u'Veröffentlichung wurde bereits von %(fullname)s bestätigt.') % + {'fullname': event.publication_confirmed_by.get_full_name()}) + text = unicode(event) + text += u'\n' + text += (ugettext(u'Bestätigt am: %(date)s') % + {'date': event.publication_confirmed_at.strftime('%d.%m.%Y %H:%M:%S')}) + else: + event.confirm_publication(user) + status = 'success' + message = ugettext(u'Veröffentlichung registriert.') + text = unicode(event) + + self.done = True + self.done_at = timezone.now() + self.save() + except Exception as e: + status = 'danger' + message = str(e) + logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) + + result['context'] = { + 'status': status, + 'message': message, + 'text': text, + } + elif self.command == 'EL': + try: + user_id = self.parameters + user = get_user_model().objects.get(id=user_id) + url = reverse('dav_events:event_list') + result['location'] = url + result['login'] = user + except Exception as e: + message = str(e) + logger.error('OneClickAction.run(): %s(%s): %s', self.command, self.parameters, message) + result['context'] = { + 'status': 'danger', + 'message': message, + } + else: + result['context'] = { + 'status': 'danger', + 'message': ugettext(u'Invalid Command. Code on fire!'), + } + + return result diff --git a/dav_events/signals.py b/dav_events/signals.py new file mode 100644 index 0000000..6a89a42 --- /dev/null +++ b/dav_events/signals.py @@ -0,0 +1,61 @@ +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.NewEventMail(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() diff --git a/dav_events/static/dav_events/css/local.css b/dav_events/static/dav_events/css/local.css index ec4b109..f93b5c8 100644 --- a/dav_events/static/dav_events/css/local.css +++ b/dav_events/static/dav_events/css/local.css @@ -63,6 +63,11 @@ thead input { margin-bottom: 1.5rem; } +#page-body h5 { + margin-top: 0px; + font-weight: bold; +} + #page-footer { position: absolute; bottom: 0px; diff --git a/dav_events/templates/dav_events/event/default.html b/dav_events/templates/dav_events/event/default.html index 76c70ff..50ba5bc 100644 --- a/dav_events/templates/dav_events/event/default.html +++ b/dav_events/templates/dav_events/event/default.html @@ -3,16 +3,11 @@
- {% if status == 'expired' %} - {% trans 'Ausgelaufen' %} - {% elif status == 'published' %} - {% trans 'Veröffentlicht' %} - {% elif status == 'publishing' %} - {% trans 'Veröffentlichung' %}: {{ event.planned_publication_date|date:'d.m.Y' }} - {% elif status == 'accepted' %} - {% trans 'Freigegeben' %} - {% elif status == 'submitted' %} - {% trans 'Eingereicht' %} + {% if status.code != 'void' %} + {{ status.get_bootstrap_label|safe }} + {% if status.code == 'publishing' %} + {{ planned_publication_date|date:'d.m.Y' }} + {% endif %} {% endif %}
{{ number }} - {{ title }} diff --git a/dav_events/templates/dav_events/event_create/SummaryForm.html b/dav_events/templates/dav_events/event_create/SummaryForm.html index 6a3fbc4..d8eedf5 100644 --- a/dav_events/templates/dav_events/event_create/SummaryForm.html +++ b/dav_events/templates/dav_events/event_create/SummaryForm.html @@ -28,9 +28,9 @@ {% endblock form-fields-visible %} {% block form-buttons %} {% buttons %} - {% bootstrap_icon 'repeat' %}  @@ -40,5 +40,9 @@ {% bootstrap_icon 'remove' %}  {% trans 'Abbrechen' %} + {% endbuttons %} {% endblock form-buttons %} diff --git a/dav_events/templates/dav_events/event_detail.html b/dav_events/templates/dav_events/event_detail.html index 7d6d1e3..cb34cb0 100644 --- a/dav_events/templates/dav_events/event_detail.html +++ b/dav_events/templates/dav_events/event_detail.html @@ -5,6 +5,36 @@ {% block head-title %}{{ event }} - {{ block.super }}{% endblock head-title %} {% block modals %} +