UPD: improved workflow code.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from . import signals
|
from . import signals
|
||||||
from . import workflow
|
|
||||||
from .config import AppConfig as _AppConfig, DefaultSetting
|
from .config import AppConfig as _AppConfig, DefaultSetting
|
||||||
|
|
||||||
DEFAULT_SETTINGS = (
|
DEFAULT_SETTINGS = (
|
||||||
@@ -32,5 +31,6 @@ class AppConfig(_AppConfig):
|
|||||||
default_settings = DEFAULT_SETTINGS
|
default_settings = DEFAULT_SETTINGS
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from .workflow import workflow
|
||||||
signals.event_updated.connect(workflow.send_emails_on_event_update)
|
signals.event_updated.connect(workflow.send_emails_on_event_update)
|
||||||
signals.event_status_updated.connect(workflow.send_emails_on_event_status_update)
|
signals.event_status_updated.connect(workflow.send_emails_on_event_status_update)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .. import choices
|
|||||||
from .. import config
|
from .. import config
|
||||||
from .. import signals
|
from .. import signals
|
||||||
from ..utils import get_ghost_user, get_system_user
|
from ..utils import get_ghost_user, get_system_user
|
||||||
|
from ..workflow import workflow
|
||||||
|
|
||||||
from .eventstatus import EventStatus, get_event_status
|
from .eventstatus import EventStatus, get_event_status
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ class Event(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dav_events:event_detail', kwargs={'pk': self.pk})
|
return reverse('dav_events:event_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, implicit_update=False, **kwargs):
|
||||||
creating = False
|
creating = False
|
||||||
original_text = ''
|
original_text = ''
|
||||||
|
|
||||||
@@ -289,7 +290,7 @@ class Event(models.Model):
|
|||||||
logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner)
|
logger.warning('Event is not created by its owner (Current user: %s, Owner: %s)!', self.editor, owner)
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
creating = True
|
creating = True
|
||||||
else:
|
elif not implicit_update:
|
||||||
original = Event.objects.get(id=self.id)
|
original = Event.objects.get(id=self.id)
|
||||||
original_text = original.render_as_text(show_internal_fields=True)
|
original_text = original.render_as_text(show_internal_fields=True)
|
||||||
|
|
||||||
@@ -300,98 +301,45 @@ class Event(models.Model):
|
|||||||
|
|
||||||
if creating:
|
if creating:
|
||||||
logger.info('Event created: %s', self)
|
logger.info('Event created: %s', self)
|
||||||
|
signals.event_created.send(sender=self.__class__, event=self)
|
||||||
self.confirm_status('draft', self.editor)
|
self.confirm_status('draft', self.editor)
|
||||||
else:
|
elif not implicit_update:
|
||||||
modified_text = self.render_as_text(show_internal_fields=True)
|
modified_text = self.render_as_text(show_internal_fields=True)
|
||||||
o_lines = original_text.split('\n')
|
o_lines = original_text.split('\n')
|
||||||
m_lines = modified_text.split('\n')
|
m_lines = modified_text.split('\n')
|
||||||
diff_lines = list(difflib.unified_diff(o_lines, m_lines, n=len(m_lines), lineterm=''))
|
diff_lines = list(difflib.unified_diff(o_lines, m_lines, n=len(m_lines), lineterm=''))
|
||||||
signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor)
|
|
||||||
logger.info('Event updated: %s', self)
|
logger.info('Event updated: %s', self)
|
||||||
|
signals.event_updated.send(sender=self.__class__, event=self, diff=diff_lines, user=self.editor)
|
||||||
|
|
||||||
def _internal_update(self):
|
def set_flag(self, status, **kwargs):
|
||||||
"""Safe changes on model instance without sending event_updated signal."""
|
if not isinstance(status, EventStatus):
|
||||||
if not self.id:
|
status = get_event_status(status)
|
||||||
logger.critical('Event._internal_update() was called before Event was saved properly.')
|
kwargs['event'] = self
|
||||||
raise Exception('Code is on fire!')
|
kwargs['status'] = status
|
||||||
super(Event, self).save()
|
flag = EventFlag(**kwargs)
|
||||||
|
flag.save()
|
||||||
def update_flags(self, for_status=None):
|
logger.info('Flagging status \'%s\' for %s', status.code, self)
|
||||||
if not self.id:
|
return flag
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(for_status, EventStatus):
|
|
||||||
code = for_status.code
|
|
||||||
else:
|
|
||||||
code = for_status
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
|
||||||
midnight = datetime.time(00, 00, 00)
|
|
||||||
|
|
||||||
if code in (None, 'draft'):
|
|
||||||
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)
|
|
||||||
|
|
||||||
if code in (None, 'published', 'publishing'):
|
|
||||||
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 code in (None, 'expired'):
|
|
||||||
if not self.flags.filter(status__code='expired').exists():
|
|
||||||
expired_at = None
|
|
||||||
|
|
||||||
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):
|
def is_flagged(self, status):
|
||||||
self.update_flags(status)
|
|
||||||
if isinstance(status, EventStatus):
|
if isinstance(status, EventStatus):
|
||||||
code = status.code
|
code = status.code
|
||||||
else:
|
else:
|
||||||
code = status
|
code = status
|
||||||
|
workflow.status_code_update(self, code)
|
||||||
if self.flags.filter(status__code=code).exists():
|
if self.flags.filter(status__code=code).exists():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
self.update_flags()
|
workflow.status_code_update(self)
|
||||||
last_flag = self.flags.last()
|
last_flag = self.flags.last()
|
||||||
if last_flag:
|
if last_flag:
|
||||||
return last_flag.status
|
return last_flag.status
|
||||||
return get_event_status('void')
|
return get_event_status('void')
|
||||||
|
|
||||||
def get_status_codes(self):
|
def get_status_codes(self):
|
||||||
self.update_flags()
|
workflow.status_code_update(self)
|
||||||
return [flag.status.code for flag in self.flags.all()]
|
return [flag.status.code for flag in self.flags.all()]
|
||||||
|
|
||||||
def confirm_status(self, status, user):
|
def confirm_status(self, status, user):
|
||||||
@@ -404,25 +352,25 @@ class Event(models.Model):
|
|||||||
if flag:
|
if flag:
|
||||||
return flag
|
return flag
|
||||||
|
|
||||||
if code == 'accepted':
|
valid, return_code, message = workflow.validate_status_code_update(code, self)
|
||||||
if not self.is_flagged('submitted'):
|
if not valid:
|
||||||
logger.warning('Event.confirm_status(): yet not submitted event got accepted! (Event: %s)', self)
|
logger.warning(u'Invalid status update to \'%s\': %s Event: %s', code, message, self)
|
||||||
if not self.number:
|
|
||||||
self.number = self.get_next_number()
|
|
||||||
self._internal_update()
|
|
||||||
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 = self.set_flag(status=code, user=user)
|
||||||
flag = EventFlag(event=self, status=status_obj, user=user)
|
|
||||||
flag.save()
|
workflow.status_code_update(self, code)
|
||||||
logger.info('Flagging status \'%s\' for %s', code, self)
|
|
||||||
signals.event_status_updated.send(sender=self.__class__, event=self, flag=flag)
|
signals.event_status_updated.send(sender=self.__class__, event=self, flag=flag)
|
||||||
|
|
||||||
return flag
|
return flag
|
||||||
|
|
||||||
def get_next_number(self):
|
def get_number(self):
|
||||||
|
number = workflow.get_number(self)
|
||||||
|
if number:
|
||||||
|
return number
|
||||||
|
else:
|
||||||
|
return '%s**/%d' % (self.sport, self.first_day.year % 100)
|
||||||
|
|
||||||
|
def set_next_number(self):
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
year = self.first_day.year
|
year = self.first_day.year
|
||||||
@@ -431,8 +379,8 @@ class Event(models.Model):
|
|||||||
qs = Event.objects.filter(number__isnull=False,
|
qs = Event.objects.filter(number__isnull=False,
|
||||||
sport=self.sport,
|
sport=self.sport,
|
||||||
first_day__gte=year_begin,
|
first_day__gte=year_begin,
|
||||||
first_day__lte=year_end).order_by('-number')
|
first_day__lte=year_end).order_by('number')
|
||||||
last = qs.first()
|
last = qs.last()
|
||||||
if 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)
|
match = re.match(r'^(?P<sport>[A-Z])(?P<count>[0-9][0-9]*)/(?P<year>[0-9][0-9]*)', last.number)
|
||||||
if match:
|
if match:
|
||||||
@@ -440,14 +388,9 @@ class Event(models.Model):
|
|||||||
counter = int(gd['count'])
|
counter = int(gd['count'])
|
||||||
|
|
||||||
counter += 1
|
counter += 1
|
||||||
n = '%s%02d/%d' % (self.sport, counter, year % 100)
|
self.number = '%s%02d/%d' % (self.sport, counter, year % 100)
|
||||||
return n
|
self.save(implicit_update=True)
|
||||||
|
|
||||||
def get_number(self):
|
|
||||||
if self.is_flagged('accepted') and self.number:
|
|
||||||
return self.number
|
return self.number
|
||||||
else:
|
|
||||||
return '%s**/%d' % (self.sport, self.first_day.year % 100)
|
|
||||||
|
|
||||||
def get_formated_date(self, begin_date=None, end_date=None, format='normalized_long'):
|
def get_formated_date(self, begin_date=None, end_date=None, format='normalized_long'):
|
||||||
if begin_date is None:
|
if begin_date is None:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.dispatch import Signal
|
from django.dispatch import Signal
|
||||||
|
|
||||||
|
event_created = Signal(providing_args=['event'])
|
||||||
event_updated = Signal(providing_args=['event', 'diff', 'user'])
|
event_updated = Signal(providing_args=['event', 'diff', 'user'])
|
||||||
event_status_updated = Signal(providing_args=['event', 'flag'])
|
event_status_updated = Signal(providing_args=['event', 'flag'])
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .. import choices
|
|||||||
from .. import forms
|
from .. import forms
|
||||||
from .. import models
|
from .. import models
|
||||||
from ..utils import has_role
|
from ..utils import has_role
|
||||||
|
from ..workflow import workflow
|
||||||
|
|
||||||
app_config = apps.get_containing_app_config(__package__)
|
app_config = apps.get_containing_app_config(__package__)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -195,19 +196,22 @@ class EventConfirmStatusView(EventPermissionMixin, generic.DetailView):
|
|||||||
elif status == 'accepted':
|
elif status == 'accepted':
|
||||||
if not self.has_permission('accept', event):
|
if not self.has_permission('accept', event):
|
||||||
raise PermissionDenied(status)
|
raise PermissionDenied(status)
|
||||||
if not event.is_flagged('submitted'):
|
|
||||||
messages.error(request, _(u'Veranstaltung ist noch nicht eingereicht.'))
|
|
||||||
return HttpResponseRedirect(event.get_absolute_url())
|
|
||||||
elif status == 'publishing' or status == 'published':
|
elif status == 'publishing' or status == 'published':
|
||||||
if not self.has_permission('publish', event):
|
if not self.has_permission('publish', event):
|
||||||
raise PermissionDenied(status)
|
raise PermissionDenied(status)
|
||||||
if not event.is_flagged('accepted'):
|
|
||||||
messages.error(request, _(u'Veranstaltung ist noch nicht freigegeben.'))
|
|
||||||
return HttpResponseRedirect(event.get_absolute_url())
|
|
||||||
else:
|
else:
|
||||||
if not self.has_permission('update', event):
|
if not self.has_permission('update', event):
|
||||||
raise PermissionDenied(status)
|
raise PermissionDenied(status)
|
||||||
|
|
||||||
|
valid, return_code, message = workflow.validate_status_code_update(status, event)
|
||||||
|
if not valid:
|
||||||
|
if return_code == 'not-submitted':
|
||||||
|
message = _(u'Veranstaltung ist noch nicht eingereicht.')
|
||||||
|
elif return_code == 'not-accepted':
|
||||||
|
message = _(u'Veranstaltung ist noch nicht freigegeben.')
|
||||||
|
messages.error(request, message)
|
||||||
|
return HttpResponseRedirect(event.get_absolute_url())
|
||||||
|
|
||||||
event.confirm_status(status, request.user)
|
event.confirm_status(status, request.user)
|
||||||
|
|
||||||
if status == 'submitted':
|
if status == 'submitted':
|
||||||
|
|||||||
@@ -1,12 +1,131 @@
|
|||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from . import emails
|
from . import emails
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def send_emails_on_event_update(sender, **kwargs):
|
class BasicWorkflow(object):
|
||||||
|
#
|
||||||
|
# Status updates
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_status_code_update(cls, code, event, callback=None, *args, **kwargs):
|
||||||
|
valid = True
|
||||||
|
return_code = 'OK'
|
||||||
|
message = u'OK'
|
||||||
|
if code == 'accepted':
|
||||||
|
if not event.is_flagged('submitted'):
|
||||||
|
valid = False
|
||||||
|
return_code = 'not-submitted'
|
||||||
|
message = u'Event is not submitted.'
|
||||||
|
elif code == 'publishing' or code == 'published':
|
||||||
|
if 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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def status_code_update(cls, event, code=None):
|
||||||
|
if not event.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
today = datetime.date.today()
|
||||||
|
midnight = datetime.time(00, 00, 00)
|
||||||
|
|
||||||
|
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)
|
||||||
|
event.set_flag(status='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:
|
||||||
|
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, '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
|
||||||
|
elif event.last_day:
|
||||||
|
if event.last_day < today:
|
||||||
|
expired_at = event.last_day
|
||||||
|
elif event.alt_first_day:
|
||||||
|
if event.alt_first_day < today:
|
||||||
|
expired_at = event.alt_first_day
|
||||||
|
elif event.first_day and event.first_day < today:
|
||||||
|
expired_at = event.first_day
|
||||||
|
|
||||||
|
if expired_at:
|
||||||
|
logger.info('Detected expired state of Event %s', event)
|
||||||
|
timestamp = timezone.make_aware(datetime.datetime.combine(expired_at, midnight))
|
||||||
|
event.set_flag(status='expired', timestamp=timestamp)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_number(cls, event):
|
||||||
|
if event.number and event.flags.filter(status__code='accepted').exists():
|
||||||
|
return event.number
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
#
|
||||||
|
# Permissions
|
||||||
|
#
|
||||||
|
@classmethod
|
||||||
|
def has_permission(cls, event, user, permission):
|
||||||
|
raise NotImplementedError('not ready yet')
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if permission == 'view':
|
||||||
|
if user == event.owner:
|
||||||
|
return True
|
||||||
|
raise Exception('must check roles')
|
||||||
|
elif permission == 'submit':
|
||||||
|
if user == event.owner:
|
||||||
|
return True
|
||||||
|
elif permission == 'accept':
|
||||||
|
raise Exception('must check roles')
|
||||||
|
elif permission == 'publish':
|
||||||
|
raise Exception('must check roles')
|
||||||
|
elif permission == 'update':
|
||||||
|
raise Exception('must check roles')
|
||||||
|
return False
|
||||||
|
|
||||||
|
#
|
||||||
|
# Signal handlers
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_emails_on_event_update(cls, sender, **kwargs):
|
||||||
event = kwargs.get('event')
|
event = kwargs.get('event')
|
||||||
diff = kwargs.get('diff')
|
diff = kwargs.get('diff')
|
||||||
updater = kwargs.get('user')
|
updater = kwargs.get('user')
|
||||||
@@ -36,11 +155,11 @@ def send_emails_on_event_update(sender, **kwargs):
|
|||||||
email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff_text)
|
email = emails.EventUpdatedMail(recipient=recipient, event=event, editor=updater, diff=diff_text)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def send_emails_on_event_status_update(sender, **kwargs):
|
def send_emails_on_event_status_update(cls, sender, **kwargs):
|
||||||
event = kwargs.get('event')
|
event = kwargs.get('event')
|
||||||
flag = kwargs.get('flag')
|
flag = kwargs.get('flag')
|
||||||
updator = flag.user
|
updater = flag.user
|
||||||
|
|
||||||
app_config = apps.get_containing_app_config(__package__)
|
app_config = apps.get_containing_app_config(__package__)
|
||||||
if not app_config.settings.enable_email_notifications:
|
if not app_config.settings.enable_email_notifications:
|
||||||
@@ -70,7 +189,7 @@ def send_emails_on_event_status_update(sender, **kwargs):
|
|||||||
elif flag.status.code == 'accepted':
|
elif flag.status.code == 'accepted':
|
||||||
# Inform event owner about the acceptance of his event.
|
# Inform event owner about the acceptance of his event.
|
||||||
if event.owner.email:
|
if event.owner.email:
|
||||||
email = emails.EventAcceptedMail(recipient=event.owner, event=event, editor=updator)
|
email = emails.EventAcceptedMail(recipient=event.owner, event=event, editor=updater)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Inform publishers that they have to publish the event.
|
# Inform publishers that they have to publish the event.
|
||||||
@@ -84,6 +203,9 @@ def send_emails_on_event_status_update(sender, **kwargs):
|
|||||||
action = OneClickAction(command='EP')
|
action = OneClickAction(command='EP')
|
||||||
action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id)
|
action.parameters = '{event},{user}'.format(event=event.id, user=recipient.id)
|
||||||
action.save()
|
action.save()
|
||||||
email = emails.EventToPublishMail(recipient=recipient, event=event, editor=updator,
|
email = emails.EventToPublishMail(recipient=recipient, event=event, editor=updater,
|
||||||
confirm_publication_action=action)
|
confirm_publication_action=action)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
|
||||||
|
workflow = BasicWorkflow
|
||||||
|
|||||||
Reference in New Issue
Block a user