Add support for generating diagrams from workflows.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-08-08 02:47:20 -04:00
parent 28a1edbc72
commit 8fea65e3f1
7 changed files with 147 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<pk>\d+)/image/$',
WorkflowImageView.as_view(),
name='workflow_image'
),
url(
r'^(?P<pk>\d+)/preview/$',
WorkflowPreviewView.as_view(),
name='workflow_preview'
),
url(
r'^state/(?P<pk>\d+)/documents/$',
WorkflowStateDocumentListView.as_view(),

View File

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

View File

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