Workflows: Refactor workflow preview generation

Refactor the workflow preview generation to work as a
background task API service. Solves GitLab issue #532.

The image generation runs as an out of process task
ensuring that the HTTP request is never compromised.

A new task queue named "document_states_fast" was created.
The settings WORKFLOWS_IMAGE_CACHE_TIME,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS we added.

Images generated are stored by default under /mayan/media/workflows.

The Dockerfile and deployment instructions are updated
to include the new queue.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2018-11-29 02:08:55 -04:00
parent cfe1934b9b
commit e0d900d952
10 changed files with 62 additions and 27 deletions

View File

@@ -100,6 +100,10 @@
- Refactored the workflow preview generation to work as an
background task API service. Solves GitLab issue #532.
A new task queue named "document_states_fast" was created.
The settings WORKFLOWS_IMAGE_CACHE_TIME,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS we added.
3.1.9 (2018-11-01)
==================

View File

@@ -80,6 +80,7 @@ Bugs fixed or issues closed
* :gitlab-issue:`487` gnupg1 Issue with Ubuntu 16.04 - Could not show/view documents
* :gitlab-issue:`498` Can't scan subdirectories
* :gitlab-issue:`522` Office 365 SMTP
* :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified
* :gitlab-issue:`539` Setting for default email sender is missing

View File

@@ -2,6 +2,7 @@ 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
@@ -24,6 +25,7 @@ from .serializers import (
WritableWorkflowInstanceLogEntrySerializer, WritableWorkflowSerializer,
WritableWorkflowTransitionSerializer
)
from .settings import settings_workflow_image_cache_time
from .storages import storage_workflowimagecache
from .tasks import task_generate_workflow_image
@@ -243,6 +245,7 @@ class APIWorkflowImageView(generics.RetrieveAPIView):
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(
@@ -253,6 +256,11 @@ class APIWorkflowImageView(generics.RetrieveAPIView):
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

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 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):
@@ -178,9 +178,9 @@ class WorkflowInstanceTransitionForm(forms.Form):
class WorkflowPreviewForm(forms.Form):
preview = forms.IntegerField(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

@@ -1,11 +1,14 @@
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.files.base import ContentFile
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, models
@@ -65,7 +68,7 @@ class Workflow(models.Model):
return self.label
def generate_image(self):
cache_filename = '{}'.format(self.id)
cache_filename = '{}-{}'.format(self.id, self.get_hash())
image = self.render()
# Since open "wb+" doesn't create files, check if the file
@@ -80,9 +83,30 @@ class Workflow(models.Model):
return cache_filename
def get_api_image_url(self, *args, **kwargs):
final_url = furl()
final_url.args = kwargs
final_url.path = reverse('rest_api:workflow-image', args=(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

@@ -9,6 +9,13 @@ from smart_settings import Namespace
namespace = Namespace(name='document_states', label=_('Workflows'))
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=_(

View File

@@ -1 +1 @@
<img class="img-responsive" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %} src="{% url 'rest_api:workflow-image' widget.value.pk %}" style="margin:auto;" />
<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

@@ -23,8 +23,8 @@ from .views import (
SetupWorkflowTransitionEditView,
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView,
WorkflowImageView, WorkflowInstanceTransitionView, WorkflowListView,
WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
WorkflowInstanceTransitionView, WorkflowListView, WorkflowPreviewView,
WorkflowStateDocumentListView, WorkflowStateListView,
)
urlpatterns = [
@@ -166,11 +166,6 @@ urlpatterns = [
WorkflowStateListView.as_view(),
name='workflow_state_list'
),
url(
r'^(?P<pk>\d+)/image/$',
WorkflowImageView.as_view(),
name='workflow_image'
),
url(
r'^(?P<pk>\d+)/preview/$',
WorkflowPreviewView.as_view(),

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.db.utils import IntegrityError
from django.http import Http404, HttpResponseRedirect
@@ -15,7 +14,7 @@ from common.views import (
AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView
SingleObjectEditView, SingleObjectListView
)
from documents.models import Document
from documents.views import DocumentListView
@@ -907,19 +906,6 @@ 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
@@ -928,5 +914,6 @@ class WorkflowPreviewView(SingleObjectDetailView):
def get_extra_context(self):
return {
'hide_labels': True,
'object': self.get_object(),
'title': _('Preview of: %s') % self.get_object()
}