diff --git a/HISTORY.rst b/HISTORY.rst
index 64cd15ae0e..8ea2bef805 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -5,6 +5,7 @@
links.
- Use Select2 widget for the document type selection form.
- Backport the vertical main menu update.
+- Backport workflow preview refactor. GitLab issue #532.
3.2.5 (2019-07-05)
==================
diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst
index b75446368b..9edd482f8b 100644
--- a/docs/releases/3.3.rst
+++ b/docs/releases/3.3.rst
@@ -17,6 +17,7 @@ Changes
The vertical menu remain open even when clicking on items and upon
a browser refresh will also restore its state to match the selected
view.
+- Backport workflow preview refactor. GitLab issue #532.
Removals
--------
@@ -105,6 +106,6 @@ Backward incompatible changes
Bugs fixed or issues closed
---------------------------
-- :gitlab-issue:`XX`
+- :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py
index 78e4e31893..5f998db115 100644
--- a/mayan/apps/document_states/api_views.py
+++ b/mayan/apps/document_states/api_views.py
@@ -1,6 +1,8 @@
from __future__ import absolute_import, unicode_literals
+from django.http import HttpResponse
from django.shortcuts import get_object_or_404
+from django.views.decorators.cache import cache_control, patch_cache_control
from rest_framework import generics
@@ -10,6 +12,7 @@ from mayan.apps.documents.permissions import permission_document_type_view
from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter
from mayan.apps.rest_api.permissions import MayanPermission
+from .literals import WORKFLOW_IMAGE_TASK_TIMEOUT
from .models import Workflow
from .permissions import (
permission_workflow_create, permission_workflow_delete,
@@ -23,6 +26,10 @@ from .serializers import (
WritableWorkflowTransitionSerializer
)
+from .settings import settings_workflow_image_cache_time
+from .storages import storage_workflowimagecache
+from .tasks import task_generate_workflow_image
+
class APIDocumentTypeWorkflowListView(generics.ListAPIView):
"""
@@ -172,6 +179,41 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView):
self.get_workflow().document_types.remove(instance)
+class APIWorkflowImageView(generics.RetrieveAPIView):
+ """
+ get: Returns an image representation of the selected workflow.
+ """
+ filter_backends = (MayanObjectPermissionsFilter,)
+ mayan_object_permissions = {
+ 'GET': (permission_workflow_view,),
+ }
+ queryset = Workflow.objects.all()
+
+ def get_serializer(self, *args, **kwargs):
+ return None
+
+ def get_serializer_class(self):
+ return None
+
+ @cache_control(private=True)
+ def retrieve(self, request, *args, **kwargs):
+ task = task_generate_workflow_image.apply_async(
+ kwargs=dict(
+ document_state_id=self.get_object().pk,
+ )
+ )
+
+ cache_filename = task.get(timeout=WORKFLOW_IMAGE_TASK_TIMEOUT)
+ with storage_workflowimagecache.open(cache_filename) as file_object:
+ response = HttpResponse(file_object.read(), content_type='image')
+ if '_hash' in request.GET:
+ patch_cache_control(
+ response,
+ max_age=settings_workflow_image_cache_time.value
+ )
+ return response
+
+
class APIWorkflowListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the workflows.
diff --git a/mayan/apps/document_states/fields.py b/mayan/apps/document_states/fields.py
new file mode 100644
index 0000000000..75a3e7b958
--- /dev/null
+++ b/mayan/apps/document_states/fields.py
@@ -0,0 +1,9 @@
+from __future__ import absolute_import, unicode_literals
+
+from django import forms
+
+from .widgets import WorkflowImageWidget
+
+
+class WorfklowImageField(forms.fields.Field):
+ widget = WorkflowImageWidget
diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py
index 971b17bd2d..930dbf49e9 100644
--- a/mayan/apps/document_states/forms.py
+++ b/mayan/apps/document_states/forms.py
@@ -12,10 +12,10 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.forms import DynamicModelForm
from .classes import WorkflowAction
+from .fields import WorfklowImageField
from .models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
)
-from .widgets import WorkflowImageWidget
class WorkflowActionSelectionForm(forms.Form):
@@ -188,9 +188,9 @@ class WorkflowInstanceTransitionForm(forms.Form):
class WorkflowPreviewForm(forms.Form):
- preview = forms.CharField(widget=WorkflowImageWidget())
+ workflow = WorfklowImageField()
def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance', None)
super(WorkflowPreviewForm, self).__init__(*args, **kwargs)
- self.fields['preview'].initial = instance
+ self.fields['workflow'].initial = instance
diff --git a/mayan/apps/document_states/literals.py b/mayan/apps/document_states/literals.py
index 79fc51f828..674bbeebef 100644
--- a/mayan/apps/document_states/literals.py
+++ b/mayan/apps/document_states/literals.py
@@ -9,3 +9,4 @@ WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)
+WORKFLOW_IMAGE_TASK_TIMEOUT = 60
diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py
index a5da8ab106..edfa431691 100644
--- a/mayan/apps/document_states/models.py
+++ b/mayan/apps/document_states/models.py
@@ -1,12 +1,16 @@
from __future__ import absolute_import, unicode_literals
+import hashlib
import json
import logging
+from furl import furl
from graphviz import Digraph
from django.conf import settings
+from django.core import serializers
from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.files.base import ContentFile
from django.db import IntegrityError, models, transaction
from django.db.models import F, Max, Q
from django.urls import reverse
@@ -27,6 +31,7 @@ from .literals import (
)
from .managers import WorkflowManager
from .permissions import permission_workflow_transition
+from .storages import storage_workflowimagecache
logger = logging.getLogger(__name__)
@@ -63,9 +68,49 @@ class Workflow(models.Model):
def __str__(self):
return self.label
+ def generate_image(self):
+ cache_filename = '{}-{}'.format(self.id, self.get_hash())
+ image = self.render()
+
+ # Since open "wb+" doesn't create files, check if the file
+ # exists, if not then create it
+ if not storage_workflowimagecache.exists(cache_filename):
+ storage_workflowimagecache.save(
+ name=cache_filename, content=ContentFile(content='')
+ )
+
+ with storage_workflowimagecache.open(cache_filename, mode='wb+') as file_object:
+ file_object.write(image)
+
+ return cache_filename
+
+ def get_api_image_url(self, *args, **kwargs):
+ final_url = furl()
+ final_url.args = kwargs
+ final_url.path = reverse(
+ viewname='rest_api:workflow-image',
+ kwargs={'pk': self.pk}
+ )
+ final_url.args['_hash'] = self.get_hash()
+
+ return final_url.tostr()
+
def get_document_types_not_in_workflow(self):
return DocumentType.objects.exclude(pk__in=self.document_types.all())
+ def get_hash(self):
+ objects_lists = list(
+ Workflow.objects.filter(pk=self.pk)
+ ) + list(
+ WorkflowState.objects.filter(workflow__pk=self.pk)
+ ) + list(
+ WorkflowTransition.objects.filter(workflow__pk=self.pk)
+ )
+
+ return hashlib.sha256(
+ serializers.serialize('json', objects_lists)
+ ).hexdigest()
+
def get_initial_state(self):
try:
return self.states.get(initial=True)
diff --git a/mayan/apps/document_states/queues.py b/mayan/apps/document_states/queues.py
index 6b269d5b99..b68354e8b2 100644
--- a/mayan/apps/document_states/queues.py
+++ b/mayan/apps/document_states/queues.py
@@ -3,12 +3,21 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
-from mayan.apps.task_manager.workers import worker_slow
+from mayan.apps.task_manager.workers import worker_fast, worker_slow
queue_document_states = CeleryQueue(
- name='document_states', label=_('Document states'), worker=worker_slow
+ label=_('Document states'), name='document_states', worker=worker_slow
)
+queue_document_states_fast = CeleryQueue(
+ label=_('Document states fast'), name='document_states_fast',
+ worker=worker_fast
+)
+
queue_document_states.add_task_type(
- dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows',
- label=_('Launch all workflows')
+ label=_('Launch all workflows'),
+ dotted_path='mayan.apps.document_states.tasks.task_launch_all_workflows'
+)
+queue_document_states_fast.add_task_type(
+ label=_('Generate workflow previews'),
+ dotted_path='mayan.apps.document_states.tasks.task_generate_workflow_image'
)
diff --git a/mayan/apps/document_states/settings.py b/mayan/apps/document_states/settings.py
new file mode 100644
index 0000000000..b90ae5e382
--- /dev/null
+++ b/mayan/apps/document_states/settings.py
@@ -0,0 +1,32 @@
+from __future__ import unicode_literals
+
+import os
+
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+
+from mayan.apps.smart_settings import Namespace
+
+namespace = Namespace(label=_('Workflows'), name='document_states')
+
+settings_workflow_image_cache_time = namespace.add_setting(
+ global_name='WORKFLOWS_IMAGE_CACHE_TIME', default='31556926',
+ help_text=_(
+ 'Time in seconds that the browser should cache the supplied workflow '
+ 'images. The default of 31559626 seconds corresponde to 1 year.'
+ )
+)
+setting_workflowimagecache_storage = namespace.add_setting(
+ global_name='WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND',
+ default='django.core.files.storage.FileSystemStorage', help_text=_(
+ 'Path to the Storage subclass to use when storing the cached '
+ 'workflow image files.'
+ )
+)
+setting_workflowimagecache_storage_arguments = namespace.add_setting(
+ global_name='WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS',
+ default={'location': os.path.join(settings.MEDIA_ROOT, 'workflows')},
+ help_text=_(
+ 'Arguments to pass to the WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND.'
+ )
+)
diff --git a/mayan/apps/document_states/storages.py b/mayan/apps/document_states/storages.py
new file mode 100644
index 0000000000..8a689634c7
--- /dev/null
+++ b/mayan/apps/document_states/storages.py
@@ -0,0 +1,12 @@
+from __future__ import unicode_literals
+
+from django.utils.module_loading import import_string
+
+from .settings import (
+ setting_workflowimagecache_storage,
+ setting_workflowimagecache_storage_arguments
+)
+
+storage_workflowimagecache = import_string(
+ dotted_path=setting_workflowimagecache_storage.value
+)(**setting_workflowimagecache_storage_arguments.value)
diff --git a/mayan/apps/document_states/tasks.py b/mayan/apps/document_states/tasks.py
index 2156a45ead..3ec8d90acb 100644
--- a/mayan/apps/document_states/tasks.py
+++ b/mayan/apps/document_states/tasks.py
@@ -9,6 +9,17 @@ from mayan.celery import app
logger = logging.getLogger(__name__)
+@app.task()
+def task_generate_workflow_image(document_state_id):
+ Workflow = apps.get_model(
+ app_label='document_states', model_name='Workflow'
+ )
+
+ workflow = Workflow.objects.get(pk=document_state_id)
+
+ return workflow.generate_image()
+
+
@app.task(ignore_result=True)
def task_launch_all_workflows():
Document = apps.get_model(app_label='documents', model_name='Document')
diff --git a/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html b/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html
new file mode 100644
index 0000000000..6aa520bc92
--- /dev/null
+++ b/mayan/apps/document_states/templates/document_states/forms/widgets/workflow_image.html
@@ -0,0 +1,2 @@
+
+
diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py
index e139e3ff8a..f37af45452 100644
--- a/mayan/apps/document_states/urls.py
+++ b/mayan/apps/document_states/urls.py
@@ -4,9 +4,10 @@ from django.conf.urls import url
from .api_views import (
APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList,
- APIWorkflowDocumentTypeView, APIWorkflowInstanceListView,
- APIWorkflowInstanceView, APIWorkflowInstanceLogEntryListView,
- APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView,
+ APIWorkflowDocumentTypeView, APIWorkflowImageView,
+ APIWorkflowInstanceListView, APIWorkflowInstanceView,
+ APIWorkflowInstanceLogEntryListView, APIWorkflowListView,
+ APIWorkflowStateListView, APIWorkflowStateView,
APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView
)
from .views import (
@@ -22,7 +23,7 @@ from .views import (
SetupWorkflowTransitionEditView,
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView,
- WorkflowImageView, WorkflowInstanceTransitionView, WorkflowListView,
+ WorkflowInstanceTransitionView, WorkflowListView,
WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
)
from .views.workflow_views import SetupDocumentTypeWorkflowsView
@@ -167,11 +168,6 @@ urlpatterns = [
view=WorkflowStateListView.as_view(),
name='workflow_state_list'
),
- url(
- regex=r'^(?P\d+)/image/$',
- view=WorkflowImageView.as_view(),
- name='workflow_image'
- ),
url(
regex=r'^(?P\d+)/preview/$',
view=WorkflowPreviewView.as_view(),
@@ -204,6 +200,10 @@ api_urls = [
view=APIWorkflowDocumentTypeView.as_view(),
name='workflow-document-type-detail'
),
+ url(
+ regex=r'^workflows/(?P\d+)/image/$',
+ name='workflow-image', view=APIWorkflowImageView.as_view()
+ ),
url(
regex=r'^workflows/(?P[0-9]+)/states/$',
view=APIWorkflowStateListView.as_view(), name='workflowstate-list'
diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py
index 423134e0b1..4abab87386 100644
--- a/mayan/apps/document_states/views/workflow_views.py
+++ b/mayan/apps/document_states/views/workflow_views.py
@@ -1,7 +1,6 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
-from django.core.files.base import ContentFile
from django.db import transaction
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
@@ -13,7 +12,7 @@ from mayan.apps.common.generics import (
AddRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
- SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView
+ SingleObjectEditView, SingleObjectListView
)
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.events import event_document_type_edited
@@ -49,7 +48,6 @@ from ..permissions import (
from ..tasks import task_launch_all_workflows
__all__ = (
- 'WorkflowImageView', 'WorkflowPreviewView',
'SetupWorkflowListView', 'SetupWorkflowCreateView', 'SetupWorkflowEditView',
'SetupWorkflowDeleteView', 'SetupWorkflowDocumentTypesView',
'SetupWorkflowStateActionCreateView', 'SetupWorkflowStateActionDeleteView',
@@ -59,7 +57,8 @@ __all__ = (
'SetupWorkflowStateListView', 'SetupWorkflowTransitionCreateView',
'SetupWorkflowTransitionDeleteView', 'SetupWorkflowTransitionEditView',
'SetupWorkflowTransitionListView',
- 'SetupWorkflowTransitionTriggerEventListView', 'ToolLaunchAllWorkflows'
+ 'SetupWorkflowTransitionTriggerEventListView', 'ToolLaunchAllWorkflows',
+ 'WorkflowPreviewView'
)
@@ -750,26 +749,15 @@ class ToolLaunchAllWorkflows(ConfirmView):
)
-class WorkflowImageView(SingleObjectDownloadView):
- attachment = False
- model = Workflow
- object_permission = permission_workflow_view
-
- def get_file(self):
- workflow = self.get_object()
- return ContentFile(workflow.render(), name=workflow.label)
-
- def get_mimetype(self):
- return 'image'
-
-
class WorkflowPreviewView(SingleObjectDetailView):
form_class = WorkflowPreviewForm
model = Workflow
object_permission = permission_workflow_view
+ pk_url_kwarg = 'pk'
def get_extra_context(self):
return {
'hide_labels': True,
+ 'object': self.get_object(),
'title': _('Preview of: %s') % self.get_object()
}
diff --git a/mayan/apps/document_states/widgets.py b/mayan/apps/document_states/widgets.py
index 75e563baa1..92e761b7b9 100644
--- a/mayan/apps/document_states/widgets.py
+++ b/mayan/apps/document_states/widgets.py
@@ -1,8 +1,7 @@
from __future__ import unicode_literals
from django import forms
-from django.urls import reverse
-from django.utils.html import format_html_join, mark_safe
+from django.utils.html import format_html_join
def widget_transition_events(transition):
@@ -15,23 +14,10 @@ def widget_transition_events(transition):
)
-def widget_workflow_diagram(workflow):
- return mark_safe(
- '
'.format(
- reverse(
- viewname='document_states:workflow_image', kwargs={
- 'pk': workflow.pk
- }
- )
- )
- )
-
-
class WorkflowImageWidget(forms.widgets.Widget):
- def render(self, name, value, attrs=None):
- if value:
- output = []
- output.append(widget_workflow_diagram(value))
- return mark_safe(''.join(output))
- else:
- return ''
+ template_name = 'document_states/forms/widgets/workflow_image.html'
+
+ def format_value(self, value):
+ if value == '' or value is None:
+ return None
+ return value