Appearance: Fix form CSS media rendering

Fix the way the form CSS contained in the media attribute
is rendered. This is now an interator and not a single value.
Replace the current method with a for loop.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2018-11-29 01:12:36 -04:00
parent ef3453b48c
commit cfe1934b9b
17 changed files with 226 additions and 98 deletions

View File

@@ -97,6 +97,9 @@
cache invalidation is tied to index updates. This makes the cache invalidation is tied to index updates. This makes the
timeout less relevant. The purpose of the cache timeout is timeout less relevant. The purpose of the cache timeout is
now avoid runaway memory usage. now avoid runaway memory usage.
- 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.
3.1.9 (2018-11-01) 3.1.9 (2018-11-01)
================== ==================

View File

@@ -22,7 +22,7 @@ user = root
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = false autorestart = false
autostart = true autostart = true
command = nice -n 1 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h ${MAYAN_WORKER_FAST_CONCURRENCY}" command = nice -n 1 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h ${MAYAN_WORKER_FAST_CONCURRENCY}"
killasgroup = true killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998

View File

@@ -107,7 +107,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = true autorestart = true
autostart = true autostart = true
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h --concurrency=1 command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h --concurrency=1
killasgroup = true killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -276,7 +276,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = true autorestart = true
autostart = true autostart = true
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h
killasgroup = true killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998

View File

@@ -4,7 +4,9 @@
{% load appearance_tags %} {% load appearance_tags %}
{{ form.media.render_css|safe }} {% for asset in form.media.render_css %}
{{ asset|safe }}
{% endfor %}
{% for group, errors in form.errors.items %} {% for group, errors in form.errors.items %}
{% for error in errors %} {% for error in errors %}
@@ -38,7 +40,6 @@
{% endfor %} {% endfor %}
</tr> </tr>
{% else %} {% else %}
{% for field in form.hidden_fields %} {% for field in form.hidden_fields %}
{{ field }} {{ field }}
{% endfor %} {% endfor %}

View File

@@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
@@ -10,6 +11,7 @@ from documents.permissions import permission_document_type_view
from rest_api.filters import MayanObjectPermissionsFilter from rest_api.filters import MayanObjectPermissionsFilter
from rest_api.permissions import MayanPermission from rest_api.permissions import MayanPermission
from .literals import WORKFLOW_IMAGE_TASK_TIMEOUT
from .models import Workflow from .models import Workflow
from .permissions import ( from .permissions import (
permission_workflow_create, permission_workflow_delete, permission_workflow_create, permission_workflow_delete,
@@ -22,6 +24,8 @@ from .serializers import (
WritableWorkflowInstanceLogEntrySerializer, WritableWorkflowSerializer, WritableWorkflowInstanceLogEntrySerializer, WritableWorkflowSerializer,
WritableWorkflowTransitionSerializer WritableWorkflowTransitionSerializer
) )
from .storages import storage_workflowimagecache
from .tasks import task_generate_workflow_image
class APIDocumentTypeWorkflowListView(generics.ListAPIView): class APIDocumentTypeWorkflowListView(generics.ListAPIView):
@@ -223,6 +227,35 @@ class APIWorkflowView(generics.RetrieveUpdateDestroyAPIView):
return WritableWorkflowSerializer return WritableWorkflowSerializer
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
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')
return response
# Workflow state views # Workflow state views

View File

@@ -248,12 +248,23 @@ class DocumentStatesApp(MayanAppConfig):
), ),
) )
) )
app.conf.CELERY_QUEUES.extend(
(
Queue(
'document_states_fast', Exchange('document_states_fast'),
routing_key='document_states_fast'
),
)
)
app.conf.CELERY_ROUTES.update( app.conf.CELERY_ROUTES.update(
{ {
'document_states.tasks.task_launch_all_workflows': { 'document_states.tasks.task_generate_document_state_image': {
'queue': 'document_states' 'queue': 'document_states'
}, },
'document_states.tasks.task_launch_all_workflows': {
'queue': 'document_states_fast'
},
} }
) )

View File

@@ -178,7 +178,7 @@ class WorkflowInstanceTransitionForm(forms.Form):
class WorkflowPreviewForm(forms.Form): class WorkflowPreviewForm(forms.Form):
preview = forms.CharField(widget=WorkflowImageWidget()) preview = forms.IntegerField(widget=WorkflowImageWidget())
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
instance = kwargs.pop('instance', None) instance = kwargs.pop('instance', None)

View File

@@ -9,3 +9,4 @@ WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')), (WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')), (WORKFLOW_ACTION_ON_EXIT, _('On exit')),
) )
WORKFLOW_IMAGE_TASK_TIMEOUT = 60

View File

@@ -6,6 +6,7 @@ import logging
from graphviz import Digraph from graphviz import Digraph
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError, models from django.db import IntegrityError, models
from django.db.models import F, Max, Q from django.db.models import F, Max, Q
@@ -26,6 +27,7 @@ from .literals import (
) )
from .managers import WorkflowManager from .managers import WorkflowManager
from .permissions import permission_workflow_transition from .permissions import permission_workflow_transition
from .storages import storage_workflowimagecache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -62,6 +64,22 @@ class Workflow(models.Model):
def __str__(self): def __str__(self):
return self.label return self.label
def generate_image(self):
cache_filename = '{}'.format(self.id)
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, 'wb+') as file_object:
file_object.write(image)
return cache_filename
def get_document_types_not_in_workflow(self): def get_document_types_not_in_workflow(self):
return DocumentType.objects.exclude(pk__in=self.document_types.all()) return DocumentType.objects.exclude(pk__in=self.document_types.all())

View File

@@ -4,10 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from task_manager.classes import CeleryQueue from task_manager.classes import CeleryQueue
queue_document_states = CeleryQueue( queue_document_states = CeleryQueue(
name='document_states', label=_('Document states') name='document_states', label=_('Document states')
) )
queue_document_states_fast = CeleryQueue(
name='document_states_fast', label=_('Document states fast')
)
queue_document_states.add_task_type( queue_document_states.add_task_type(
name='document_states.tasks.task_launch_all_workflows', name='document_states.tasks.task_launch_all_workflows',
label=_('Launch all workflows') label=_('Launch all workflows')
) )
queue_document_states_fast.add_task_type(
name='document_states.tasks.task_generate_document_state_image',
label=_('Generate workflow previews')
)

View File

@@ -174,6 +174,7 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer):
document_types_url = serializers.HyperlinkedIdentityField( document_types_url = serializers.HyperlinkedIdentityField(
view_name='rest_api:workflow-document-type-list' view_name='rest_api:workflow-document-type-list'
) )
image_url = serializers.SerializerMethodField()
states = WorkflowStateSerializer(many=True, required=False) states = WorkflowStateSerializer(many=True, required=False)
transitions = WorkflowTransitionSerializer(many=True, required=False) transitions = WorkflowTransitionSerializer(many=True, required=False)
@@ -182,11 +183,18 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer):
'url': {'view_name': 'rest_api:workflow-detail'}, 'url': {'view_name': 'rest_api:workflow-detail'},
} }
fields = ( fields = (
'document_types_url', 'id', 'internal_name', 'label', 'states', 'document_types_url', 'image_url', 'id', 'internal_name', 'label',
'transitions', 'url' 'states', 'transitions', 'url'
) )
model = Workflow model = Workflow
def get_image_url(self, instance):
return reverse(
'rest_api:workflow-image', args=(
instance.pk,
), request=self.context['request'], format=self.context['format']
)
class WorkflowInstanceLogEntrySerializer(serializers.ModelSerializer): class WorkflowInstanceLogEntrySerializer(serializers.ModelSerializer):
document_workflow_url = serializers.SerializerMethodField() document_workflow_url = serializers.SerializerMethodField()

View File

@@ -0,0 +1,25 @@
from __future__ import unicode_literals
import os
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from smart_settings import Namespace
namespace = Namespace(name='document_states', label=_('Workflows'))
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__) 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) @app.task(ignore_result=True)
def task_launch_all_workflows(): def task_launch_all_workflows():
Document = apps.get_model(app_label='documents', model_name='Document') Document = apps.get_model(app_label='documents', model_name='Document')

View File

@@ -0,0 +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;" />

View File

@@ -4,9 +4,10 @@ from django.conf.urls import url
from .api_views import ( from .api_views import (
APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList, APIDocumentTypeWorkflowListView, APIWorkflowDocumentTypeList,
APIWorkflowDocumentTypeView, APIWorkflowInstanceListView, APIWorkflowDocumentTypeView, APIWorkflowImageView,
APIWorkflowInstanceView, APIWorkflowInstanceLogEntryListView, APIWorkflowInstanceListView, APIWorkflowInstanceView,
APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView, APIWorkflowInstanceLogEntryListView, APIWorkflowListView,
APIWorkflowStateListView, APIWorkflowStateView,
APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView
) )
from .views import ( from .views import (
@@ -188,6 +189,10 @@ api_urls = [
r'^workflows/(?P<pk>[0-9]+)/$', APIWorkflowView.as_view(), r'^workflows/(?P<pk>[0-9]+)/$', APIWorkflowView.as_view(),
name='workflow-detail' name='workflow-detail'
), ),
url(
r'^workflows/(?P<pk>[0-9]+)/image/$',
APIWorkflowImageView.as_view(), name='workflow-image'
),
url( url(
r'^workflows/(?P<pk>[0-9]+)/document_types/$', r'^workflows/(?P<pk>[0-9]+)/document_types/$',
APIWorkflowDocumentTypeList.as_view(), APIWorkflowDocumentTypeList.as_view(),

View File

@@ -1,8 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.urls import reverse from django.utils.html import format_html_join
from django.utils.html import format_html_join, mark_safe
def widget_transition_events(transition): def widget_transition_events(transition):
@@ -15,19 +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('document_states:workflow_image', args=(workflow.pk,))
)
)
class WorkflowImageWidget(forms.widgets.Widget): class WorkflowImageWidget(forms.widgets.Widget):
def render(self, name, value, attrs=None): template_name = 'document_states/forms/widgets/workflow_image.html'
if value:
output = [] def format_value(self, value):
output.append(widget_workflow_diagram(value)) if value == '' or value is None:
return mark_safe(''.join(output)) return None
else: return value
return ''