Backport workflow preview refactor

GitLab issue #532.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-07-06 02:41:16 -04:00
parent 9e068c3e83
commit fbb0f0b9bd
15 changed files with 194 additions and 55 deletions

View File

@@ -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)
==================

View File

@@ -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/

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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'
)

View File

@@ -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.'
)
)

View File

@@ -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)

View File

@@ -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')

View File

@@ -0,0 +1,2 @@
<img class="img-responsive" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} src="{{ widget.value.get_api_image_url }}" style="margin:auto;" />

View File

@@ -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<pk>\d+)/image/$',
view=WorkflowImageView.as_view(),
name='workflow_image'
),
url(
regex=r'^(?P<pk>\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<pk>\d+)/image/$',
name='workflow-image', view=APIWorkflowImageView.as_view()
),
url(
regex=r'^workflows/(?P<pk>[0-9]+)/states/$',
view=APIWorkflowStateListView.as_view(), name='workflowstate-list'

View File

@@ -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()
}

View File

@@ -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(
'<img class="img-responsive" src="{}" style="margin:auto;">'.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