Add support for generating diagrams from workflows.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
Reference in New Issue
Block a user