diff --git a/shared/utils/models/workflow.py b/shared/utils/models/workflow.py new file mode 100644 index 0000000..55c3700 --- /dev/null +++ b/shared/utils/models/workflow.py @@ -0,0 +1,123 @@ +from django.conf import settings +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +# +# Workflow Helpers + +""" +States: +- inactive (either active=False or publication date not reached) +- public (active or archived) +- active (public, not archived) +- archived (public, not active) +""" + + +class WorkflowQuerySet(models.QuerySet): + @classmethod + def exclude_inactive_filter(cls): + """Usage: qs.exclude(**self.exclude_inactive_filter())""" + return { + "is_published": True, + "publication_datetime__lte": timezone.now().date(), + } + + @classmethod + def active_filter(cls): + return { + "is_published": True, + "publication_datetime__lte": timezone.now().date(), + "archiving_datetime__gte": timezone.now().date(), + } + + @classmethod + def archived_filter(cls): + return { + "is_published": True, + "publication_datetime__lte": timezone.now().date(), + "archiving_datetime__lte": timezone.now().date(), + } + + def public(self): + # Active or archived + return self.exclude(**self.exclude_inactive_filter()) + + def active(self): + return self.filter(**self.active_filter()) + + def archived(self): + return self.filter(**self.archived_filter()) + + +class ManyToManyWorkflowQuerySet(WorkflowQuerySet): + def _build_related_filters(self, filter_template): + """ + Transforms all filter rules to match any related models which + itself is workflow managed. + """ + filter = {} + for field in self.model._meta.get_fields(): + if field.many_to_one and isinstance(field.related_model._meta.default_manager, WorkflowManager): + field.name + for path, value in filter_template.items(): + filter[f'{field.name}__{path}'] = value + return filter + + def public(self): + # Active or archived + return self.exclude(**self._build_related_filters(self.exclude_inactive_filter())) + + def active(self): + return self.filter(**self._build_related_filters(self.active_filter())) + + def archived(self): + return self.filter(**self._build_related_filters(self.archived_filter())) + + +class WorkflowManager(models.Manager.from_queryset(WorkflowQuerySet)): + pass + + +class ManyToManyWorkflowManager(models.Manager.from_queryset(ManyToManyWorkflowQuerySet)): + pass + + +class WorkflowMixin(models.Model): + creation_datetime = models.DateTimeField(auto_now_add=True) + modification_datetime = models.DateTimeField(auto_now=True) + creation_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, + editable=False, + on_delete=models.SET_NULL, related_name='+') + modification_user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, + editable=False, + on_delete=models.SET_NULL, related_name='+') + + is_published = models.BooleanField(_("Publicly visible"), default=False) + publication_datetime = models.DateTimeField(_("Publication Date"), default=timezone.now) + archiving_datetime = models.DateTimeField(_("Archiving Date"), default=timezone.datetime.max) + + objects = WorkflowManager() + + class Meta: + abstract = True + ordering = ['-publication_datetime'] # Most recent first + + @property + def worflow_status(self): + now = timezone.now() + if not self.is_active or self.publication_datetime < now: + return 'inactive' + elif self.publication_datetime >= now and self.archiving_datetime < now: + return 'active' + else: + return 'archived' + + +def display_is_active(obj): + return obj.is_publised and obj.publication_datetime <= timezone.now() + + +display_is_active.boolean = True +display_is_active.short_description = _("Aktiv")