diff --git a/dav_auth/admin.py b/dav_auth/admin.py new file mode 100644 index 0000000..8774339 --- /dev/null +++ b/dav_auth/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.contrib.auth.models import Permission + + +@admin.register(Permission) +class PermissionAdmin(admin.ModelAdmin): + pass diff --git a/dav_events/apps.py b/dav_events/apps.py index 359494f..593da58 100644 --- a/dav_events/apps.py +++ b/dav_events/apps.py @@ -7,14 +7,15 @@ from . import signals DEFAULT_SETTINGS = ( DefaultSetting('enable_email_on_status_update', False), DefaultSetting('enable_email_on_update', False), - DefaultSetting('group_manage_all', None), - DefaultSetting('group_manage_w', None), - DefaultSetting('group_manage_s', None), - DefaultSetting('group_manage_m', None), - DefaultSetting('group_manage_k', None), - DefaultSetting('group_manage_b', None), - DefaultSetting('group_publish', None), - DefaultSetting('group_publish_incremental', None), + DefaultSetting('groups_manager_super', []), + DefaultSetting('groups_manager_w', []), + DefaultSetting('groups_manager_s', []), + DefaultSetting('groups_manager_m', []), + DefaultSetting('groups_manager_k', []), + DefaultSetting('groups_manager_b', []), + DefaultSetting('groups_publisher_print', []), + DefaultSetting('groups_publisher_web', []), + DefaultSetting('groups_publisher_facebook', []), DefaultSetting('forms_development_init', False), DefaultSetting('form_initials', dict()), DefaultSetting('matrix_config', ImproperlyConfigured), diff --git a/dav_events/django_project_config/settings-dav_events.py b/dav_events/django_project_config/settings-dav_events.py index 2a9e9dc..7b288f7 100644 --- a/dav_events/django_project_config/settings-dav_events.py +++ b/dav_events/django_project_config/settings-dav_events.py @@ -7,14 +7,15 @@ ENABLE_EMAIL_ON_STATUS_UPDATE = False ENABLE_EMAIL_ON_UPDATE = False # Authorization Roles / Groups -GROUP_MANAGE_ALL = 'Tourenreferenten' -GROUP_MANAGE_W = 'Wanderreferenten' -GROUP_MANAGE_S = 'Skireferenten' -GROUP_MANAGE_M = 'MTBReferenten' -GROUP_MANAGE_K = 'Kletterreferenten' -GROUP_MANAGE_B = 'Bergsteigerreferenten' -GROUP_PUBLISH = 'Redaktion' -GROUP_PUBLISH_INCREMENTAL = 'OnlineRedaktion' +GROUPS_MANAGER_SUPER = ['Tourenreferenten'] +GROUPS_MANAGER_W = ['Bereichsleiter_Wandern'] +GROUPS_MANAGER_S = ['Bereichsleiter_Ski'] +GROUPS_MANAGER_M = ['Bereichsleiter_MTB'] +GROUPS_MANAGER_K = ['Bereichsleiter_Klettern'] +GROUPS_MANAGER_B = ['Bereichsleiter_Bergsteigen'] +GROUPS_PUBLISHER_PRINT = ['Redaktion_KA-Alpin'] +GROUPS_PUBLISHER_WEB = ['Redaktion_Joomla'] +GROUPS_PUBLISHER_FACEBOOK = ['Redaktion_Facebook'] # ChainedForm and sub classes FORM_INITIALS = { diff --git a/dav_events/emails.py b/dav_events/emails.py index 1e29348..efa90ae 100644 --- a/dav_events/emails.py +++ b/dav_events/emails.py @@ -93,3 +93,11 @@ class EventToPublishMail(AbstractEventMail): context['editor'] = self._editor context['confirm_publication_url'] = self._confirm_publication_action.get_absolute_url() return context + + +class EventToPublishWebMail(EventToPublishMail): + _template_name = 'dav_events/emails/event_to_publish_web.txt' + + +class EventToPublishFacebookMail(EventToPublishMail): + _template_name = 'dav_events/emails/event_to_publish_facebook.txt' diff --git a/dav_events/migrations/0024_auto_20190117_1515.py b/dav_events/migrations/0024_auto_20190117_1515.py new file mode 100644 index 0000000..39be6d0 --- /dev/null +++ b/dav_events/migrations/0024_auto_20190117_1515.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-01-17 15:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0023_auto_20181121_1422'), + ] + + operations = [ + migrations.AlterField( + model_name='oneclickaction', + name='command', + field=models.CharField(choices=[(b'EA', b'accept event'), (b'EP', b'confirm publication of an event'), (b'EL', b'login and go to event list'), (b'EVENT_STATUS_UPDATE', b'update event status')], max_length=254), + ), + ] diff --git a/dav_events/migrations/0025_auto_20190117_1518.py b/dav_events/migrations/0025_auto_20190117_1518.py new file mode 100644 index 0000000..4470540 --- /dev/null +++ b/dav_events/migrations/0025_auto_20190117_1518.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-01-17 15:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0024_auto_20190117_1515'), + ] + + operations = [ + migrations.AlterModelOptions( + name='event', + options={'ordering': ['first_day'], 'verbose_name': 'Veranstaltung', 'verbose_name_plural': 'Veranstaltungen'}, + ), + ] diff --git a/dav_events/migrations/0026_auto_20190123_1528.py b/dav_events/migrations/0026_auto_20190123_1528.py new file mode 100644 index 0000000..5f6a7fe --- /dev/null +++ b/dav_events/migrations/0026_auto_20190123_1528.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.17 on 2019-01-23 15:28 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dav_events', '0025_auto_20190117_1518'), + ] + + operations = [ + migrations.AlterField( + model_name='eventstatus', + name='code', + field=models.CharField(max_length=254, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(b'^[0-9a-z_.-]*$', b'Only lower case latin characters (a-z), digits (0-9), dots (.), underscores (_) and hyphens (-) are allowed.')]), + ), + migrations.AlterField( + model_name='oneclickaction', + name='command', + field=models.CharField(choices=[(b'EVENT_LIST', b'login and go to event list (user id)'), (b'EVENT_STATUS_UPDATE', b'update event status (event id,status code,user id)')], max_length=254), + ), + ] diff --git a/dav_events/models/event.py b/dav_events/models/event.py index 00682bf..2fc4339 100644 --- a/dav_events/models/event.py +++ b/dav_events/models/event.py @@ -246,7 +246,7 @@ class Event(models.Model): verbose_name = _(u'Veranstaltung') verbose_name_plural = _(u'Veranstaltungen') ordering = ['first_day'] - default_permissions = ('view', 'accept', 'edit', 'delete') + # default_permissions = ('view', 'edit', 'delete') def __unicode__(self): return u'{number} - {title} ({date})'.format(number=self.get_number(), @@ -331,8 +331,12 @@ class Event(models.Model): return True return False + def get_status_flags(self): + return workflow.get_status_flags(self) + def get_status(self): workflow.status_code_update(self) + last_flag = self.flags.last() if last_flag: return last_flag.status @@ -472,7 +476,6 @@ class Event(models.Model): r = { 'event': self, - 'status': self.get_status(), 'number': self.get_number(), 'title': self.title, 'description': self.description, diff --git a/dav_events/models/eventstatus.py b/dav_events/models/eventstatus.py index fd13180..a3ba2d0 100644 --- a/dav_events/models/eventstatus.py +++ b/dav_events/models/eventstatus.py @@ -2,7 +2,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from ..validators import LowerAlphanumericValidator +from ..validators import IdStringValidator BOOTSTRAP_CONTEXT_CHOICES = ( ('default', 'default'), @@ -17,7 +17,11 @@ DEFAULT_EVENT_STATI = { '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), } @@ -39,7 +43,7 @@ def get_event_status(code): class EventStatus(models.Model): - code = models.CharField(primary_key=True, max_length=254, validators=[LowerAlphanumericValidator]) + code = models.CharField(primary_key=True, max_length=254, validators=[IdStringValidator]) 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) @@ -50,9 +54,9 @@ class EventStatus(models.Model): ordering = ['severity'] def __unicode__(self): - return u'{label} ({severity} {code})'.format(code=self.code, - severity=self.severity, - label=self.label) + 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' diff --git a/dav_events/models/oneclickaction.py b/dav_events/models/oneclickaction.py index b5f9066..461e015 100644 --- a/dav_events/models/oneclickaction.py +++ b/dav_events/models/oneclickaction.py @@ -14,9 +14,8 @@ 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') + ('EVENT_LIST', 'login and go to event list (user id)'), + ('EVENT_STATUS_UPDATE', 'update event status (event id,status code,user id)'), ) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -28,7 +27,7 @@ class OneClickAction(models.Model): done_at = models.DateTimeField(blank=True, null=True) - command = models.CharField(max_length=2, choices=COMMANDS) + command = models.CharField(max_length=254, choices=COMMANDS) parameters = models.TextField(blank=True) @@ -56,79 +55,7 @@ class OneClickAction(models.Model): 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) - flag = event.flags.filter(status__code='accepted').first() - if flag: - status = 'info' - message = (ugettext(u'Veranstaltung wurde bereits von %(fullname)s freigegeben.') % - {'fullname': flag.user.get_full_name()}) - text = unicode(event) - text += u'\n' - text += (ugettext(u'Freigegeben am: %(date)s') % - {'date': flag.timestamp.strftime('%d.%m.%Y %H:%M:%S')}) - else: - event.confirm_status('accepted', user) - status = 'success' - message = ugettext(u'Veranstaltung freigegeben.') - 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 == '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) - flag = event.flags.filter(status__code__in=('publishing', 'published')).first() - if flag: - status = 'info' - message = (ugettext(u'Veröffentlichung wurde bereits von %(fullname)s bestätigt.') % - {'fullname': flag.user.get_full_name()}) - text = unicode(event) - text += u'\n' - text += (ugettext(u'Bestätigt am: %(date)s') % - {'date': flag.timestamp.strftime('%d.%m.%Y %H:%M:%S')}) - else: - if event.planned_publication_date: - new_state = 'publishing' - else: - new_state = 'published' - event.confirm_status(new_state, 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': + if self.command == 'EVENT_LIST': try: user_id = self.parameters user = get_user_model().objects.get(id=user_id) @@ -142,6 +69,42 @@ class OneClickAction(models.Model): 'status': 'danger', 'message': message, } + elif self.command == 'EVENT_STATUS_UPDATE': + try: + event_id, status_code, user_id = self.parameters.split(',') + event = Event.objects.get(id=event_id) + user = get_user_model().objects.get(id=user_id) + + flag = event.flags.filter(status__code=status_code).first() + if flag: + message = (ugettext(u'Der Status wurde bereits' + u' am %(date)s' + u' von %(user)s' + u' auf \'%(status)s\' gesetzt.') % { + 'status': flag.status.label, + 'date': flag.timestamp.strftime('%d.%m.%Y %H:%M:%S'), + 'user': flag.user.get_full_name(), + }) + else: + flag = event.confirm_status(status_code, user) + message = (ugettext(u'Der Status wurde auf \'%(status)s\' gesetzt.') % + {'status': flag.status.label}) + + result['context'] = { + 'status': 'success', + 'message': message, + } + + self.done = True + self.done_at = timezone.now() + self.save() + 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', diff --git a/dav_events/templates/dav_events/emails/event_to_publish_facebook.txt b/dav_events/templates/dav_events/emails/event_to_publish_facebook.txt new file mode 100644 index 0000000..dccaa94 --- /dev/null +++ b/dav_events/templates/dav_events/emails/event_to_publish_facebook.txt @@ -0,0 +1,25 @@ +{% load i18n %}Hallo {{ recipient.first_name }}, + +{{ trainer_firstname }} {{ trainer_familyname }} hat eine neue Veranstaltung eingereicht. +Die Veranstaltung wurde {% if editor %}von {{ editor.get_full_name }} {% endif %}zur Veröffentlichung +auf Facebook frei gegeben. + +Der folgende Link führt zur Veranstaltung: + {{ base_url }}{{ event.get_absolute_url }} +Über den folgenden Link kannst du die Veröffentlichung auf Facebook unmittelbar bestätigen: + {{ base_url }}{{ confirm_publication_url }} + +Veröffentlichung: ({% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}sofort{% endif %}) +================= +{% if planned_publication_date %}Veröffentlichung starten: {{ planned_publication_date|date:'d.m.Y' }} 00:00:00 +{% endif %}Veröffentlichung beenden: {{ day_after|date:'d.m.Y' }} 00:00:00 +Erstellungsdatum: {{ first_day|date:'d.m.Y' }} 00:00:00 +Titel: {{ number }} - {{ title }} +{% if internal_note %} +Bearbeitungshinweis: +==================== +{{ internal_note }} +{% endif %} +Ausschreibung: +============== +{{ event.render_as_text }} \ No newline at end of file diff --git a/dav_events/templates/dav_events/emails/event_to_publish_web.txt b/dav_events/templates/dav_events/emails/event_to_publish_web.txt new file mode 100644 index 0000000..4972d9a --- /dev/null +++ b/dav_events/templates/dav_events/emails/event_to_publish_web.txt @@ -0,0 +1,73 @@ +{% load i18n %}Hallo {{ recipient.first_name }}, + +{{ trainer_firstname }} {{ trainer_familyname }} hat eine neue Veranstaltung eingereicht. +Die Veranstaltung wurde {% if editor %}von {{ editor.get_full_name }} {% endif %}zur Veröffentlichung +auf der Webseite frei gegeben. + +Der folgende Link führt zur Veranstaltung: + {{ base_url }}{{ event.get_absolute_url }} +Über den folgenden Link kannst du die Veröffentlichung auf der Webseite unmittelbar bestätigen: + {{ base_url }}{{ confirm_publication_url }} + +Veröffentlichung: ({% if planned_publication_date %}{{ planned_publication_date|date:'l, d. F Y' }}{% else %}sofort{% endif %}) +================= +{% if planned_publication_date %}Veröffentlichung starten: {{ planned_publication_date|date:'d.m.Y' }} 00:00:00 +{% endif %}Veröffentlichung beenden: {{ day_after|date:'d.m.Y' }} 00:00:00 +Erstellungsdatum: {{ first_day|date:'d.m.Y' }} 00:00:00 +Titel: {{ number }} - {{ title }} +{% if internal_note %} +Bearbeitungshinweis: +==================== +{{ internal_note }} +{% endif %} +Joomla HTML +=========== +
{{ normalized_long_date }}{% if alt_normalized_long_date %}
+({% trans 'Ersatztermin' %}: {{ alt_normalized_long_date }}) +{% endif %}
+

{{ description|urlize|linebreaksbr }}

+{% if mode == 'training' %} +

{% trans 'Kursinhalte' %}:

+{% if course_topic_2 %} +{% else %}

{{ course_topic_1|urlize|linebreaksbr }}

{% endif %} +

{% trans 'Kursziele' %}:

+{% if course_goal_2 %} +{% else %}

{{ course_goal_1|urlize|linebreaksbr }}

{% endif %} +{% endif %}
+ +

+{% if requirements %}{% trans 'Anforderungen' %}: {{ requirements }}
+{% endif %}{% if equipment %}{% trans 'Ausrüstung' %}: {{ equipment }}
+{% endif %}{% if location %}{% trans 'Ort' %}: {{ location }} {% if country and country != 'DE' and country != 'XX' %}({{ country }}){% endif %}
+{% endif %}{% if basecamp %}{% trans 'Stützpunkt' %}: {{ basecamp }}
+{% endif %}{% if accommodation and accommodation != 'NONE' %}{% trans 'Unterkunft' %}: {% if accommodation == 'OTHER' %}{{ accommodation_other }}{% else %}{{ event.get_accommodation_display }}{% endif %}
+{% endif %}{% if meals and meals != 'NONE' %}{% trans 'Verpflegung' %}: {% if meals == 'OTHER' %}{{ meals_other }}{% else %}{{ event.get_meals_display }}{% endif %}
+{% endif %}{% if transport and transport != 'NONE' and transport != 'public' %}{% trans 'Hin- / Rückfahrt' %}: {% if transport == 'OTHER' %}{{ transport_other }}{% else %}{{ event.get_transport_display }}{% endif %}
+{% endif %}{% if meeting_point and meeting_point != 'NONE' %}{% trans 'Treffpunkt' %}: {% if meeting_time %}{{ meeting_time|time:'G:i'|cut:':00' }} Uhr, {% endif %}{% if meeting_point == 'OTHER' %}{{ meeting_point_other }}{% else %}{{ event.get_meeting_point_display }}{% endif %}
+{% endif %}{% if transport == 'public' %}{% if departure_time or departure_ride %}{% trans 'Abfahrt' %}: {% if departure_time %}{{ departure_time|time:'G:i'|cut:':00' }} Uhr{% endif %}{% if departure_time and departure_ride %}, {% endif %}{% if departure_ride %}{{ departure_ride }}{% endif %}
+{% endif %}{% endif %}{% if return_departure_time or return_arrival_time %}{% trans 'Rückfahrt' %}: {% if return_arrival_time %}{{ return_arrival_time|time:'G:i'|cut:':00' }} Uhr ({% trans 'Ankunft in' %} Karlsruhe){% elif return_departure_time %}{{ return_departure_time|time:'G:i'|cut:':00' }} Uhr ({% trans 'Abfahrt am Tourenort' %}){% endif %}
+{% endif %}{% if pre_meeting_1 %}{% if pre_meeting_2 %}{% trans 'Vortreffen' %} 1: {{ pre_meeting_1|date:'l, d. F Y, G:i'|cut:':00' }} {% trans 'Uhr' %}, DAV {% trans 'Sektionszentrum' %}
+{% trans 'Vortreffen' %} 2: {{ pre_meeting_2|date:'l, d. F Y, G:i'|cut:':00' }} {% trans 'Uhr' %}, DAV {% trans 'Sektionszentrum' %}
+{% else %}{% trans 'Vortreffen' %}: {{ pre_meeting_1|date:'l, d. F Y, G:i'|cut:':00' }} {% trans 'Uhr' %}, DAV {% trans 'Sektionszentrum' %}
{% endif %} +{% endif %}{% if min_participants > 0 or max_participants > 0 %}{% trans 'Teilnehmerzahl' %}: {% if min_participants == max_participants %}{{ max_participants }} {% trans 'Teilnehmer' %}{% elif min_participants > 0 and max_participants > 0 %}{{ min_participants }} - {{ max_participants }} {% trans 'Teilnehmer' %}{% elif min_participants > 0 %}min. {{ min_participants }} {% trans 'Teilnehmer' %}{% else %}max. {{ max_participants }} {% trans 'Teilnehmer' %}{% endif %}
+{% endif %}{% if charge > 0 or additional_costs %}{% trans 'Kosten' %}: {% if charge > 0 %}{{ charge|floatformat:'-2' }} € {% trans 'Teilnahmegebühr' %}{% endif %}{% if additional_costs %}{% if charge > 0 %} {% trans 'zzgl.' %} {% endif %}{{ additional_costs }}{% endif %}
+{% endif %}{% if registration_required and deadline %}{% trans 'Anmeldeschluss' %}: {{ deadline|date:'l, d. F Y' }}
+{% endif %}{% if trainer_2_fullname %}{% if mode == 'training' %}{% trans 'Ausbildungsteam' %}:{% else %}{% trans 'Team' %}:{% endif %} {{ trainer_firstname }} {{ trainer_familyname }}, {{ trainer_2_fullname }}{% if trainer_3_fullname %}, {{ trainer_3_fullname }}{% endif %}
+{% endif %}{% if trainer_familyname %}{% trans 'Leitung' %}: {{ trainer_firstname }} {{ trainer_familyname }}{% if trainer_email or trainer_phone %} ({% if trainer_email %}{{ trainer_email }}{% endif %}{% if trainer_email and trainer_phone %}, {% endif %}{% if trainer_phone %}{{ trainer_phone }}{% endif %}){% endif %} +{% endif %}

+{% if registration_required and registration_howto %}

{{ registration_howto|urlize }}

+{% endif %} diff --git a/dav_events/templates/dav_events/event/default.html b/dav_events/templates/dav_events/event/default.html index 634c21f..7b578fe 100644 --- a/dav_events/templates/dav_events/event/default.html +++ b/dav_events/templates/dav_events/event/default.html @@ -3,12 +3,12 @@
- {% if status.code != 'void' %} - {{ status.get_bootstrap_label|safe }} - {% if status.code == 'publishing' %} - {{ planned_publication_date|date:'d.m.Y' }} - {% endif %} + {% for flag in event.get_status_flags %} + {{ flag.status.get_bootstrap_label|safe }} + {% if planned_publication_date and flag.status.code|slice:":10" == 'publishing' %} + {{ planned_publication_date|date:'d.m.Y' }} {% endif %} + {% endfor %}
{{ number }} - {{ title }}
diff --git a/dav_events/templates/dav_events/event_detail.html b/dav_events/templates/dav_events/event_detail.html index f3f8391..0c11e64 100644 --- a/dav_events/templates/dav_events/event_detail.html +++ b/dav_events/templates/dav_events/event_detail.html @@ -102,7 +102,7 @@
{% if has_permission_submit %} - {% if 'submitted' in event.get_status_codes %} {% bootstrap_icon 'check' %}  @@ -113,7 +113,7 @@ {% endif %} {% if has_permission_accept %} - {% if 'accepted' in event.get_status_codes %} {% bootstrap_icon 'check' %}  diff --git a/dav_events/templates/dav_events/event_list.html b/dav_events/templates/dav_events/event_list.html index 6488127..9b51f16 100644 --- a/dav_events/templates/dav_events/event_list.html +++ b/dav_events/templates/dav_events/event_list.html @@ -65,10 +65,14 @@ {{ event.get_numeric_date }} - {{ event.get_status.get_bootstrap_label|safe }} - {% if event.get_status.code == 'publishing' %} - {{ event.planned_publication_date|date:'d.m.Y' }} + {% for flag in event.get_status_flags %} + {{ flag.status.get_bootstrap_label|safe }} + {% if event.planned_publication_date and flag.status.code|slice:':10' == 'publishing' %} + {{ event.planned_publication_date|date:'d.m.Y' }} {% endif %} + {% empty %} + {{ event.get_status.get_bootstrap_label|safe }} + {% endfor %} {% endfor %} diff --git a/dav_events/utils.py b/dav_events/utils.py index cf51ef4..6c3c981 100644 --- a/dav_events/utils.py +++ b/dav_events/utils.py @@ -2,6 +2,7 @@ import logging from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from django.db.models import Q app_config = apps.get_containing_app_config(__package__) logger = logging.getLogger(__name__) @@ -26,21 +27,50 @@ def get_group_members(group_name, ignore_missing=False): return users -def get_group_name_by_role(role): - group_name_var = 'group_{}'.format(role) - return getattr(app_config.settings, group_name_var, None) +def get_group_names_by_role(role): + if role == 'publisher': + sub_roles = ('publisher_print', 'publisher_web', 'publisher_facebook') + elif role.startswith('publisher_'): + sub_roles = (role, ) + elif role == 'manager': + sub_roles = ('manager_super', 'manager_w', 'manager_s', 'manager_m', 'manager_k', 'manager_b') + elif role == 'manager_super': + sub_roles = (role, ) + elif role.startswith('manager_'): + sub_roles = ('manager_super', role) + else: + sub_roles = (role, ) + + group_names = [] + for sub_role in sub_roles: + var_name = 'groups_{}'.format(sub_role) + if hasattr(app_config.settings, var_name): + group_names += getattr(app_config.settings, var_name) + + return list(set(group_names)) def get_users_by_role(role): users = [] - group_name = get_group_name_by_role(role) - if group_name: - users = get_group_members(group_name, ignore_missing=True) - return users + group_names = get_group_names_by_role(role) + for group_name in group_names: + users += get_group_members(group_name, ignore_missing=True) + return list(set(users)) def has_role(user, role): - group_name = get_group_name_by_role(role) - if group_name and user.groups.filter(name=group_name).count(): + group_names = get_group_names_by_role(role) + if group_names and user.groups.filter(name__in=group_names).count(): return True return False + + +def get_users_by_permission(permission_name, include_superusers=False): + appname, codename = permission_name.split('.') + + query = Q(user_permissions__codename=codename, user_permissions__content_type__app_label=appname) + query |= Q(groups__permissions__codename=codename, groups__permissions__content_type__app_label=appname) + if include_superusers: + query |= Q(is_superuser=True) + + return get_user_model().objects.filter(query).distinct() diff --git a/dav_events/validators.py b/dav_events/validators.py index b73fe1a..edaa0b4 100644 --- a/dav_events/validators.py +++ b/dav_events/validators.py @@ -1,5 +1,10 @@ from django.core.validators import RegexValidator -AlphanumericValidator = RegexValidator(r'^[0-9a-zA-Z]*$', 'Only characters A-Z, a-z and digits 0-9 are allowed.') -LowerAlphanumericValidator = RegexValidator(r'^[0-9a-z]*$', 'Only characters a-z and digits 0-9 are allowed.') +AlphanumericValidator = RegexValidator(r'^[0-9a-zA-Z]*$', 'Only latin characters (A-Z, a-z)' + ' and digits (0-9) are allowed.') +LowerAlphanumericValidator = RegexValidator(r'^[0-9a-z]*$', 'Only lower case latin characters (a-z)' + ' and digits (0-9) are allowed.') +IdStringValidator = RegexValidator(r'^[0-9a-z_.-]*$', 'Only lower case latin characters (a-z),' + ' digits (0-9),' + ' dots (.), underscores (_) and hyphens (-) are allowed.') diff --git a/dav_events/views/events.py b/dav_events/views/events.py index fc34371..ff3a661 100644 --- a/dav_events/views/events.py +++ b/dav_events/views/events.py @@ -31,20 +31,20 @@ class EventListView(generic.ListView): user = self.request.user if user.is_superuser: qs = self.model.objects.all() - elif has_role(user, 'manage_all'): + elif has_role(user, 'manager_super'): qs = self.model.objects.all() else: filter = Q(owner=user) user_sports_list = list() for k in ('W', 'S', 'M', 'K', 'B'): - role = 'manage_{}'.format(k.lower()) + role = 'manager_{}'.format(k.lower()) if has_role(user, role): user_sports_list.append(k) filter |= Q(sport__in=user_sports_list) - if has_role(user, 'publish') or has_role(user, 'publish_incremental'): + if has_role(user, 'publisher'): filter |= Q(flags__status__code='accepted') qs = self.model.objects.filter(filter) @@ -54,7 +54,7 @@ class EventListView(generic.ListView): def get_context_data(self, **kwargs): context = super(EventListView, self).get_context_data(**kwargs) user = self.request.user - context['has_permission_export'] = user.is_superuser or has_role(user, 'publish') + context['has_permission_export'] = workflow.has_global_permission(user, 'export') return context @method_decorator(login_required) @@ -112,8 +112,8 @@ class EventListExportView(generic.FormView): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): user = request.user - if not user.is_superuser and not has_role(user, 'publish'): - raise PermissionDenied('publish') + if not workflow.has_global_permission(user, 'export'): + raise PermissionDenied('export') return super(EventListExportView, self).dispatch(request, *args, **kwargs) @@ -122,45 +122,7 @@ class EventPermissionMixin(object): def has_permission(self, permission, obj): user = self.request.user - - if user.is_superuser: - return True - - if permission == 'view': - if user == obj.owner: - return True - if has_role(user, 'manage_all'): - return True - if has_role(user, 'manage_{}'.format(obj.sport.lower())): - return True - if has_role(user, 'publish_incremental') and obj.is_flagged('accepted'): - return True - if has_role(user, 'publish') and obj.is_flagged('accepted'): - return True - elif permission == 'submit': - if user == obj.owner: - return True - elif permission == 'accept': - if has_role(user, 'manage_all'): - return True - if has_role(user, 'manage_{}'.format(obj.sport.lower())): - return True - elif permission == 'publish': - if has_role(user, 'publish') or has_role(user, 'publish_incremental'): - return True - elif permission == 'update': - if not obj.is_flagged('submitted'): - if user == obj.owner: - return True - elif not obj.is_flagged('accepted'): - if has_role(user, 'manage_all'): - return True - if has_role(user, 'manage_{}'.format(obj.sport.lower())): - return True - elif has_role(user, 'publish') or has_role(user, 'publish_incremental'): - return True - - return False + return workflow.has_object_permission(user, permission, obj) def enforce_permission(self, obj): permission = self.permission @@ -197,13 +159,13 @@ class EventConfirmStatusView(EventPermissionMixin, generic.DetailView): status = kwargs.get('status') event = self.get_object() - if status == 'submitted': + if status.startswith('submit'): if not self.has_permission('submit', event): raise PermissionDenied(status) - elif status == 'accepted': + elif status.startswith('accept'): if not self.has_permission('accept', event): raise PermissionDenied(status) - elif status == 'publishing' or status == 'published': + elif status.startswith('publish'): if not self.has_permission('publish', event): raise PermissionDenied(status) else: @@ -221,11 +183,11 @@ class EventConfirmStatusView(EventPermissionMixin, generic.DetailView): event.confirm_status(status, request.user) - if status == 'submitted': + if status.startswith('submit'): messages.success(request, _(u'Veranstaltung eingereicht.')) - elif status == 'accepted': + elif status.startswith('accept'): messages.success(request, _(u'Veranstaltung freigegeben.')) - elif status == 'publishing' or status == 'published': + elif status.startswith('publish'): messages.success(request, _(u'Veröffentlichung registriert.')) else: messages.success(request, _(u'Veranstaltungsstatus registriert.')) diff --git a/dav_events/workflow.py b/dav_events/workflow.py index d744ee5..48443c4 100644 --- a/dav_events/workflow.py +++ b/dav_events/workflow.py @@ -4,9 +4,10 @@ from django.apps import apps from django.utils import timezone from . import emails -from .utils import get_users_by_role +from .utils import get_users_by_role, has_role logger = logging.getLogger(__name__) +today = datetime.date.today() class BasicWorkflow(object): @@ -18,16 +19,26 @@ class BasicWorkflow(object): valid = True return_code = 'OK' message = u'OK' - if code == 'accepted': + if code.startswith('accept'): if not event.is_flagged('submitted'): valid = False return_code = 'not-submitted' message = u'Event is not submitted.' - elif code == 'publishing' or code == 'published': + elif code.startswith('publishing'): if not event.is_flagged('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 event.is_flagged('accepted'): + valid = False + return_code = 'not-accepted' + message = u'Event is not accepted.' + if callback is not None: callback(valid, return_code, message, *args, **kwargs) return valid, return_code, message @@ -37,7 +48,6 @@ class BasicWorkflow(object): if not event.id: return - today = datetime.date.today() midnight = datetime.time(00, 00, 00) if code in (None, 'draft'): @@ -53,18 +63,86 @@ class BasicWorkflow(object): event.set_next_number() logger.info('Setting number on Event %s', event) - if code in (None, 'published', 'publishing'): - # Check if event with publishing flag should now have a published flag also. - if (event.flags.filter(status__code='publishing').exists() and - not event.flags.filter(status__code='published').exists()): - if not event.planned_publication_date: - logger.info('Detected published state of Event %s', event) - flag = event.flags.filter(status__code='publishing').last() - event.set_flag(status='published', timestamp=flag.timestamp) - elif event.planned_publication_date <= today: - logger.info('Detected published state of Event %s', event) - timestamp = timezone.make_aware(datetime.datetime.combine(event.planned_publication_date, midnight)) - event.set_flag(status='published', timestamp=timestamp) + 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_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 is due to be published. + + # Timestamp of the detected action flag. No very good. + 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 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) + event.set_flag(status='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) + event.set_flag(status='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) + event.set_flag(status='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) + event.set_flag(status='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) + event.set_flag(status='published', timestamp=timestamp) + elif publishing_web.exists() and publishing_facebook.exists(): + # Event is not due to be published yet, + # 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 + event.set_flag(status='publishing', timestamp=timestamp) if code in (None, 'expired'): # Check if event is expired now and need a expired flag. @@ -88,6 +166,26 @@ class BasicWorkflow(object): timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight)) event.set_flag(status='expired', timestamp=timestamp) + @classmethod + def get_status_flags(cls, event): + cls.status_code_update(event) + last_flag = event.flags.last() + if not last_flag: + #last_flag = event.set_flag('void') + return [] + flags = [last_flag] + + last_status = last_flag.status + if last_status.code.startswith('publishing_'): + flags += event.flags.filter(status__code='accepted') + elif last_status.code.startswith('published_'): + if event.is_flagged('publishing'): + flags += event.flags.filter(status__code='publishing') + else: + flags += event.flags.filter(status__code='accepted') + + return flags + @classmethod def get_number(cls, event): if event.number and event.flags.filter(status__code='accepted').exists(): @@ -99,25 +197,50 @@ class BasicWorkflow(object): # Permissions # @classmethod - def has_permission(cls, event, user, permission): - raise NotImplementedError('not ready yet') + def has_global_permission(cls, user, permission): + if user.is_superuser: + return True + if permission == 'export': + return has_role(user, 'publisher') + return False + + @classmethod + def has_object_permission(cls, user, permission, obj): if user.is_superuser: return True if permission == 'view': - if user == event.owner: + if user == obj.owner: + return True + if has_role(user, 'manager_super'): + return True + if has_role(user, 'manager_{}'.format(obj.sport.lower())): + return True + if has_role(user, 'publisher') and obj.is_flagged('accepted'): return True - raise Exception('must check roles') elif permission == 'submit': - if user == event.owner: + if user == obj.owner: return True elif permission == 'accept': - raise Exception('must check roles') + if has_role(user, 'manager_super'): + return True + if has_role(user, 'manager_{}'.format(obj.sport.lower())): + return True elif permission == 'publish': - raise Exception('must check roles') + if has_role(user, 'publisher'): + return True elif permission == 'update': - raise Exception('must check roles') + if not obj.is_flagged('submitted'): + if user == obj.owner: + return True + elif not obj.is_flagged('accepted'): + if has_role(user, 'manager_super'): + return True + if has_role(user, 'manager_{}'.format(obj.sport.lower())): + return True + elif has_role(user, 'publisher'): + return True return False # @@ -142,11 +265,12 @@ class BasicWorkflow(object): recipients = [event.owner] if event.is_flagged('submitted'): # If the event is already submitted, add managers to the recipients. - recipients += get_users_by_role('manage_all') - recipients += get_users_by_role('manage_{}'.format(event.sport.lower())) + recipients += get_users_by_role('manager_super') + recipients += get_users_by_role('manager_{}'.format(event.sport.lower())) if event.is_flagged('accepted'): # If the event is already published, add publishers to the recipients. - recipients += get_users_by_role('publish_incremental') + 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: @@ -172,13 +296,13 @@ class BasicWorkflow(object): # 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('manage_all') - recipients += get_users_by_role('manage_{}'.format(event.sport.lower())) + recipients = get_users_by_role('manager_super') + 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='EA') - action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id) + 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() @@ -192,15 +316,35 @@ class BasicWorkflow(object): # 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. - recipients = get_users_by_role('publish_incremental') + + # 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='EP') - action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id) + 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.EventToPublishMail(recipient=recipient, event=event, editor=updater, - confirm_publication_action=action) + 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() # @@ -215,8 +359,6 @@ class BasicWorkflow(object): else: publication_deadline = first_day - datetime.timedelta(app_config.settings.publish_before_begin_days) - today = datetime.date.today() - 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):