diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index 043e51bf40..368c212a47 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -36,8 +36,9 @@ from .links import ( link_setup_workflow_transition_delete, link_setup_workflow_transition_edit, link_tool_launch_all_workflows, link_workflow_instance_detail, link_workflow_instance_transition, link_workflow_document_list, - link_workflow_list, link_workflow_state_document_list, - link_workflow_state_list, link_workflow_instance_transition_events + link_workflow_list, link_workflow_preview, + link_workflow_state_document_list, link_workflow_state_list, + link_workflow_instance_transition_events ) from .permissions import permission_workflow_transition from .queues import * # NOQA @@ -220,7 +221,8 @@ class DocumentStatesApp(MayanAppConfig): links=( link_setup_workflow_states, link_setup_workflow_transitions, link_setup_workflow_document_types, link_setup_workflow_edit, - link_acl_list, link_setup_workflow_delete + link_acl_list, link_workflow_preview, + link_setup_workflow_delete ), sources=(Workflow,) ) menu_object.bind_links( diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index 1117bd1e81..e5cb7ee93b 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -12,6 +12,7 @@ from .classes import WorkflowAction from .models import ( Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition ) +from .widgets import WorkflowImageWidget class WorkflowActionSelectionForm(forms.Form): @@ -157,3 +158,12 @@ class WorkflowInstanceTransitionForm(forms.Form): comment = forms.CharField( label=_('Comment'), required=False, widget=forms.widgets.Textarea() ) + + +class WorkflowPreviewForm(forms.Form): + preview = forms.CharField(widget=WorkflowImageWidget()) + + def __init__(self, *args, **kwargs): + instance = kwargs.pop('instance', None) + super(WorkflowPreviewForm, self).__init__(*args, **kwargs) + self.fields['preview'].initial = instance diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index 55bf9112ad..0fca918779 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -37,6 +37,25 @@ link_setup_workflow_list = Link( permissions=(permission_workflow_view,), icon='fa fa-sitemap', text=_('Workflows'), view='document_states:setup_workflow_list' ) +link_setup_workflow_state_action_delete = Link( + args='resolved_object.pk', permissions=(permission_workflow_edit,), + tags='dangerous', text=_('Delete'), + view='document_states:setup_workflow_state_action_delete', +) +link_setup_workflow_state_action_edit = Link( + args='resolved_object.pk', permissions=(permission_workflow_edit,), + text=_('Edit'), view='document_states:setup_workflow_state_action_edit', +) +link_setup_workflow_state_action_list = Link( + args='resolved_object.pk', permissions=(permission_workflow_edit,), + text=_('Actions'), + view='document_states:setup_workflow_state_action_list', +) +link_setup_workflow_state_action_selection = Link( + args='resolved_object.pk', permissions=(permission_workflow_edit,), + text=_('Create action'), + view='document_states:setup_workflow_state_action_selection', +) link_setup_workflow_state_create = Link( permissions=(permission_workflow_edit,), text=_('Create state'), view='document_states:setup_workflow_state_create', args='object.pk' @@ -110,28 +129,7 @@ link_workflow_instance_transition_events = Link( text=_('Transition events'), view='document_states:setup_workflow_instance_transition_events' ) - -### - -link_setup_workflow_state_action_list = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Actions'), - view='document_states:setup_workflow_state_action_list', -) - -link_setup_workflow_state_action_selection = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Create action'), - view='document_states:setup_workflow_state_action_selection', -) - - -link_setup_workflow_state_action_delete = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - tags='dangerous', text=_('Delete'), - view='document_states:setup_workflow_state_action_delete', -) -link_setup_workflow_state_action_edit = Link( - args='resolved_object.pk', permissions=(permission_workflow_edit,), - text=_('Edit'), view='document_states:setup_workflow_state_action_edit', +link_workflow_preview = Link( + args='resolved_object.pk', permissions=(permission_workflow_view,), + text=_('Preview'), view='document_states:workflow_preview' ) diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index b5b5ffc2df..a79a55c3ba 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals import json import logging +from graphviz import Digraph + from django.conf import settings from django.core.exceptions import PermissionDenied, ValidationError from django.db import IntegrityError, models @@ -78,6 +80,48 @@ class Workflow(models.Model): 'Workflow %s launched for document %s', self, document ) + def render(self): + diagram = Digraph( + name='finite_state_machine', graph_attr={ + 'rankdir': 'LR', 'size': '8,5' + }, format='png' + ) + + state_cache = {} + transition_cache = [] + + for state in self.states.all(): + state_cache['s{}'.format(state.pk)] = { + 'name': 's{}'.format(state.pk), + 'label': state.label, + 'initial': state.initial, + 'connections': {'origin': 0, 'destination': 0} + } + + for transition in self.transitions.all(): + transition_cache.append( + { + 'tail_name': 's{}'.format(transition.origin_state.pk), + 'head_name': 's{}'.format(transition.destination_state.pk), + 'label': transition.label + } + ) + state_cache['s{}'.format(transition.origin_state.pk)]['connections']['origin'] = state_cache['s{}'.format(transition.origin_state.pk)]['connections']['origin'] + 1 + state_cache['s{}'.format(transition.destination_state.pk)]['connections']['destination'] += 1 + + for key, value in state_cache.items(): + kwargs = { + 'name': value['name'], + 'label': value['label'], + 'shape': 'doublecircle' if value['connections']['origin'] == 0 or value['connections']['destination'] == 0 or value['initial'] else 'circle', + } + diagram.node(**kwargs) + + for transition in transition_cache: + diagram.edge(**transition) + + return diagram.pipe() + class Meta: ordering = ('label',) verbose_name = _('Workflow') diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index 5f45b797e4..8ed87c69e7 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -22,8 +22,8 @@ from .views import ( SetupWorkflowTransitionEditView, SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows, WorkflowDocumentListView, WorkflowInstanceDetailView, - WorkflowInstanceTransitionView, WorkflowListView, - WorkflowStateDocumentListView, WorkflowStateListView, + WorkflowImageView, WorkflowInstanceTransitionView, WorkflowListView, + WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView, ) urlpatterns = [ @@ -165,6 +165,16 @@ urlpatterns = [ WorkflowStateListView.as_view(), name='workflow_state_list' ), + url( + r'^(?P\d+)/image/$', + WorkflowImageView.as_view(), + name='workflow_image' + ), + url( + r'^(?P\d+)/preview/$', + WorkflowPreviewView.as_view(), + name='workflow_preview' + ), url( r'^state/(?P\d+)/documents/$', WorkflowStateDocumentListView.as_view(), diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index 2d43dfa82b..7ab5f270ed 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages +from django.core.files.base import ContentFile from django.db.utils import IntegrityError from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -10,9 +11,9 @@ from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList from common.views import ( AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView, - SingleObjectDeleteView, SingleObjectDynamicFormCreateView, - SingleObjectDynamicFormEditView, SingleObjectEditView, - SingleObjectListView + SingleObjectDeleteView, SingleObjectDetailView, + SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView, + SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView ) from documents.models import Document from documents.views import DocumentListView @@ -22,8 +23,8 @@ from events.models import EventType from .classes import WorkflowAction from .forms import ( WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm, - WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm, - WorkflowTransitionTriggerEventRelationshipFormSet + WorkflowPreviewForm, WorkflowStateActionDynamicForm, WorkflowStateForm, + WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet ) from .models import ( Workflow, WorkflowInstance, WorkflowState, WorkflowStateAction, @@ -733,3 +734,28 @@ class ToolLaunchAllWorkflows(ConfirmView): messages.success( self.request, _('Workflow launch queued successfully.') ) + + +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 + + def get_extra_context(self): + return { + 'hide_labels': True, + 'title': _('Preview of: %s') % self.get_object() + } diff --git a/mayan/apps/document_states/widgets.py b/mayan/apps/document_states/widgets.py index 98b594631d..4488acd66e 100644 --- a/mayan/apps/document_states/widgets.py +++ b/mayan/apps/document_states/widgets.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals -from django.utils.html import format_html_join +from django import forms +from django.urls import reverse +from django.utils.html import format_html_join, mark_safe def widget_transition_events(transition): @@ -11,3 +13,23 @@ def widget_transition_events(transition): ) for transition_trigger in transition.trigger_events.all() ) ) + + +def widget_workflow_diagram(workflow): + return mark_safe( + ''.format( + reverse('document_states:workflow_image', args=(workflow.pk,)) + ) + ) + + +class WorkflowImageWidget(forms.widgets.Widget): + def render(self, name, value, attrs=None): + final_attrs = self.build_attrs(attrs) + + if value: + output = [] + output.append(widget_workflow_diagram(value)) + return mark_safe(''.join(output)) + else: + return ''