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_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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 ''
|
||||||
|
|||||||
Reference in New Issue
Block a user