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_setup_workflow_transition_delete, link_setup_workflow_transition_edit,
link_tool_launch_all_workflows, link_workflow_instance_detail, link_tool_launch_all_workflows, link_workflow_instance_detail,
link_workflow_instance_transition, link_workflow_document_list, link_workflow_instance_transition, link_workflow_document_list,
link_workflow_list, link_workflow_state_document_list, link_workflow_list, link_workflow_preview,
link_workflow_state_list, link_workflow_instance_transition_events link_workflow_state_document_list, link_workflow_state_list,
link_workflow_instance_transition_events
) )
from .permissions import permission_workflow_transition from .permissions import permission_workflow_transition
from .queues import * # NOQA from .queues import * # NOQA
@@ -220,7 +221,8 @@ class DocumentStatesApp(MayanAppConfig):
links=( links=(
link_setup_workflow_states, link_setup_workflow_transitions, link_setup_workflow_states, link_setup_workflow_transitions,
link_setup_workflow_document_types, link_setup_workflow_edit, 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,) ), sources=(Workflow,)
) )
menu_object.bind_links( menu_object.bind_links(

View File

@@ -12,6 +12,7 @@ from .classes import WorkflowAction
from .models import ( from .models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
) )
from .widgets import WorkflowImageWidget
class WorkflowActionSelectionForm(forms.Form): class WorkflowActionSelectionForm(forms.Form):
@@ -157,3 +158,12 @@ class WorkflowInstanceTransitionForm(forms.Form):
comment = forms.CharField( comment = forms.CharField(
label=_('Comment'), required=False, widget=forms.widgets.Textarea() 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', permissions=(permission_workflow_view,), icon='fa fa-sitemap',
text=_('Workflows'), view='document_states:setup_workflow_list' 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( link_setup_workflow_state_create = Link(
permissions=(permission_workflow_edit,), text=_('Create state'), permissions=(permission_workflow_edit,), text=_('Create state'),
view='document_states:setup_workflow_state_create', args='object.pk' view='document_states:setup_workflow_state_create', args='object.pk'
@@ -110,28 +129,7 @@ link_workflow_instance_transition_events = Link(
text=_('Transition events'), text=_('Transition events'),
view='document_states:setup_workflow_instance_transition_events' view='document_states:setup_workflow_instance_transition_events'
) )
link_workflow_preview = Link(
### args='resolved_object.pk', permissions=(permission_workflow_view,),
text=_('Preview'), view='document_states:workflow_preview'
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',
) )

View File

@@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals
import json import json
import logging import logging
from graphviz import Digraph
from django.conf import settings from django.conf import settings
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
@@ -78,6 +80,48 @@ class Workflow(models.Model):
'Workflow %s launched for document %s', self, document '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: class Meta:
ordering = ('label',) ordering = ('label',)
verbose_name = _('Workflow') verbose_name = _('Workflow')

View File

@@ -22,8 +22,8 @@ from .views import (
SetupWorkflowTransitionEditView, SetupWorkflowTransitionEditView,
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows, SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView, WorkflowDocumentListView, WorkflowInstanceDetailView,
WorkflowInstanceTransitionView, WorkflowListView, WorkflowImageView, WorkflowInstanceTransitionView, WorkflowListView,
WorkflowStateDocumentListView, WorkflowStateListView, WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
) )
urlpatterns = [ urlpatterns = [
@@ -165,6 +165,16 @@ urlpatterns = [
WorkflowStateListView.as_view(), WorkflowStateListView.as_view(),
name='workflow_state_list' 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( url(
r'^state/(?P<pk>\d+)/documents/$', r'^state/(?P<pk>\d+)/documents/$',
WorkflowStateDocumentListView.as_view(), WorkflowStateDocumentListView.as_view(),

View File

@@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib import messages from django.contrib import messages
from django.core.files.base import ContentFile
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 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 acls.models import AccessControlList
from common.views import ( from common.views import (
AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView, AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectDynamicFormCreateView, SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectDynamicFormCreateView, SingleObjectDynamicFormEditView,
SingleObjectListView SingleObjectDownloadView, SingleObjectEditView, SingleObjectListView
) )
from documents.models import Document from documents.models import Document
from documents.views import DocumentListView from documents.views import DocumentListView
@@ -22,8 +23,8 @@ from events.models import EventType
from .classes import WorkflowAction from .classes import WorkflowAction
from .forms import ( from .forms import (
WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm, WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm,
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm, WorkflowPreviewForm, WorkflowStateActionDynamicForm, WorkflowStateForm,
WorkflowTransitionTriggerEventRelationshipFormSet WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet
) )
from .models import ( from .models import (
Workflow, WorkflowInstance, WorkflowState, WorkflowStateAction, Workflow, WorkflowInstance, WorkflowState, WorkflowStateAction,
@@ -733,3 +734,28 @@ class ToolLaunchAllWorkflows(ConfirmView):
messages.success( messages.success(
self.request, _('Workflow launch queued successfully.') 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 __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): def widget_transition_events(transition):
@@ -11,3 +13,23 @@ def widget_transition_events(transition):
) for transition_trigger in transition.trigger_events.all() ) 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 ''