Finish workflow context implementation

Improve workflow instance detail view.
Add workflow transition field widget support.
Fix workflow transition field required support.
Update tests.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-07-01 09:53:23 -04:00
parent 303e34299a
commit 572690e2bc
11 changed files with 174 additions and 29 deletions

View File

@@ -27,6 +27,7 @@ from .dependencies import * # NOQA
from .handlers import ( from .handlers import (
handler_index_document, handler_launch_workflow, handler_trigger_transition handler_index_document, handler_launch_workflow, handler_trigger_transition
) )
from .html_widgets import widget_transition_events, WorkflowLogExtraDataWidget
from .links import ( from .links import (
link_document_workflow_instance_list, link_setup_document_type_workflows, link_document_workflow_instance_list, link_setup_document_type_workflows,
link_setup_workflow_document_types, link_setup_workflow_create, link_setup_workflow_document_types, link_setup_workflow_create,
@@ -54,7 +55,6 @@ from .permissions import (
permission_workflow_delete, permission_workflow_edit, permission_workflow_delete, permission_workflow_edit,
permission_workflow_transition, permission_workflow_view permission_workflow_transition, permission_workflow_view
) )
from .widgets import widget_transition_events
class DocumentStatesApp(MayanAppConfig): class DocumentStatesApp(MayanAppConfig):
@@ -206,22 +206,31 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Date and time'), source=WorkflowInstanceLogEntry, label=_('Date and time'),
attribute='datetime' attribute='datetime', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, attribute='user' source=WorkflowInstanceLogEntry, attribute='user', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, source=WorkflowInstanceLogEntry,
attribute='transition' attribute='transition__origin_state', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, source=WorkflowInstanceLogEntry,
attribute='comment' attribute='transition', is_sortable=True
) )
SourceColumn( SourceColumn(
source=WorkflowInstanceLogEntry, source=WorkflowInstanceLogEntry,
attribute='extra_data' attribute='transition__destination_state', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
attribute='comment', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
attribute='get_extra_data', label=_('Additional details'),
widget=WorkflowLogExtraDataWidget
) )
SourceColumn( SourceColumn(
@@ -281,8 +290,16 @@ class DocumentStatesApp(MayanAppConfig):
source=WorkflowTransitionField source=WorkflowTransitionField
) )
SourceColumn( SourceColumn(
attribute='required', is_sortable=True, source=WorkflowTransitionField, attribute='required', is_sortable=True,
widget=TwoStateWidget source=WorkflowTransitionField, widget=TwoStateWidget
)
SourceColumn(
attribute='get_widget_display', label=_('Widget'),
is_sortable=False, source=WorkflowTransitionField
)
SourceColumn(
attribute='widget_kwargs', is_sortable=True,
source=WorkflowTransitionField
) )
SourceColumn( SourceColumn(

View File

@@ -0,0 +1,39 @@
from __future__ import unicode_literals
from django import forms
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html_join, mark_safe
def widget_transition_events(transition):
return format_html_join(
sep='\n', format_string='<div class="">{}</div>', args_generator=(
(
transition_trigger.event_type.label,
) for transition_trigger in transition.trigger_events.all()
)
)
def widget_workflow_diagram(workflow):
return mark_safe(
'<img class="img-responsive" src="{}" style="margin:auto;">'.format(
reverse(
viewname='document_states:workflow_image', kwargs={
'pk': workflow.pk
}
)
)
)
class WorkflowLogExtraDataWidget(object):
template_name = 'document_states/extra_data.html'
def render(self, name=None, value=None):
return render_to_string(
template_name=self.template_name, context={
'value': value
}
)

View File

@@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon from mayan.apps.appearance.classes import Icon
from mayan.apps.documents.icons import icon_document_type from mayan.apps.documents.icons import icon_document_type
icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap') icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap')
icon_document_type_workflow_list = icon_workflow icon_document_type_workflow_list = icon_workflow

View File

@@ -2,14 +2,6 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
WORKFLOW_ACTION_ON_ENTRY = 1
WORKFLOW_ACTION_ON_EXIT = 2
WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)
FIELD_TYPE_CHOICE_CHAR = 1 FIELD_TYPE_CHOICE_CHAR = 1
FIELD_TYPE_CHOICE_INTEGER = 2 FIELD_TYPE_CHOICE_INTEGER = 2
FIELD_TYPE_CHOICES = ( FIELD_TYPE_CHOICES = (
@@ -21,3 +13,20 @@ FIELD_TYPE_MAPPING = {
FIELD_TYPE_CHOICE_CHAR: 'django.forms.CharField', FIELD_TYPE_CHOICE_CHAR: 'django.forms.CharField',
FIELD_TYPE_CHOICE_INTEGER: 'django.forms.IntegerField', FIELD_TYPE_CHOICE_INTEGER: 'django.forms.IntegerField',
} }
WIDGET_CLASS_TEXTAREA = 1
WIDGET_CLASS_CHOICES = (
(WIDGET_CLASS_TEXTAREA, _('Text area')),
)
WIDGET_CLASS_MAPPING = {
WIDGET_CLASS_TEXTAREA: 'django.forms.widgets.Textarea',
}
WORKFLOW_ACTION_ON_ENTRY = 1
WORKFLOW_ACTION_ON_EXIT = 2
WORKFLOW_ACTION_WHEN_CHOICES = (
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
)

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 13:11
from __future__ import unicode_literals
from django.db import migrations, models
import mayan.apps.common.validators
class Migration(migrations.Migration):
dependencies = [
('document_states', '0014_auto_20190701_0454'),
]
operations = [
migrations.AddField(
model_name='workflowtransitionfield',
name='widget',
field=models.PositiveIntegerField(blank=True, choices=[(1, 'Text area')], help_text='An optional class to change the default presentation of the field.', null=True, verbose_name='Widget class'),
),
migrations.AddField(
model_name='workflowtransitionfield',
name='widget_kwargs',
field=models.TextField(blank=True, help_text='A group of keyword arguments to customize the widget. Use YAML format.', validators=[mayan.apps.common.validators.YAMLValidator()], verbose_name='Widget keyword arguments'),
),
migrations.AlterField(
model_name='workflowinstance',
name='context',
field=models.TextField(blank=True, verbose_name='Context'),
),
]

View File

@@ -4,6 +4,11 @@ import json
import logging import logging
from graphviz import Digraph from graphviz import Digraph
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
@@ -15,7 +20,7 @@ from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.validators import validate_internal_name from mayan.apps.common.validators import YAMLValidator, validate_internal_name
from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.events.models import StoredEventType from mayan.apps.events.models import StoredEventType
@@ -23,7 +28,7 @@ from mayan.apps.events.models import StoredEventType
from .error_logs import error_log_state_actions from .error_logs import error_log_state_actions
from .events import event_workflow_created, event_workflow_edited from .events import event_workflow_created, event_workflow_edited
from .literals import ( from .literals import (
FIELD_TYPE_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES, FIELD_TYPE_CHOICES, WIDGET_CLASS_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES,
WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT
) )
from .managers import WorkflowManager from .managers import WorkflowManager
@@ -393,6 +398,18 @@ class WorkflowTransitionField(models.Model):
'Whether this fields needs to be filled out or not to proceed.' 'Whether this fields needs to be filled out or not to proceed.'
), verbose_name=_('Required') ), verbose_name=_('Required')
) )
widget = models.PositiveIntegerField(
blank=True, choices=WIDGET_CLASS_CHOICES, help_text=_(
'An optional class to change the default presentation of the field.'
), null=True, verbose_name=_('Widget class')
)
widget_kwargs = models.TextField(
blank=True, help_text=_(
'A group of keyword arguments to customize the widget. '
'Use YAML format.'
), validators=[YAMLValidator()],
verbose_name=_('Widget keyword arguments')
)
class Meta: class Meta:
unique_together = ('transition', 'name') unique_together = ('transition', 'name')
@@ -402,6 +419,9 @@ class WorkflowTransitionField(models.Model):
def __str__(self): def __str__(self):
return self.label return self.label
def get_widget_kwargs(self):
return yaml.load(stream=self.widget_kwargs, Loader=SafeLoader)
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowTransitionTriggerEvent(models.Model): class WorkflowTransitionTriggerEvent(models.Model):
@@ -595,6 +615,13 @@ class WorkflowInstanceLogEntry(models.Model):
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user): if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
raise ValidationError(_('Not a valid transition choice.')) raise ValidationError(_('Not a valid transition choice.'))
def get_extra_data(self):
result = {}
for key, value in self.loads().items():
result[self.transition.fields.get(name=key).label] = value
return result
def loads(self): def loads(self):
""" """
Deserialize the context data. Deserialize the context data.

View File

@@ -0,0 +1,7 @@
{% if value %}
<ul>
{% for key, value in value.items %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>
{% endif %}

View File

@@ -152,9 +152,10 @@ class WorkflowTransitionViewTestMixin(object):
def _request_test_workflow_transition(self): def _request_test_workflow_transition(self):
return self.post( return self.post(
viewname='document_states:workflow_instance_transition', viewname='document_states:workflow_instance_transition_execute',
kwargs={'pk': self.test_workflow_instance.pk}, data={ kwargs={
'transition': self.test_workflow_transition.pk, 'workflow_instance_pk': self.test_workflow_instance.pk,
'workflow_transition_pk': self.test_workflow_transition.pk,
} }
) )

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..literals import FIELD_TYPE_CHOICE_CHAR
from ..models import WorkflowTransition from ..models import WorkflowTransition
from ..permissions import ( from ..permissions import (
permission_workflow_edit, permission_workflow_view, permission_workflow_edit, permission_workflow_view,
@@ -19,6 +20,7 @@ from .mixins import (
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field' TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field' TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test' TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
class WorkflowTransitionViewTestCase( class WorkflowTransitionViewTestCase(
@@ -164,7 +166,7 @@ class WorkflowTransitionDocumentViewTestCase(
permission. permission.
""" """
response = self._request_test_workflow_transition() response = self._request_test_workflow_transition()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 404)
# Workflow should remain in the same initial state # Workflow should remain in the same initial state
self.assertEqual( self.assertEqual(
@@ -249,6 +251,7 @@ class WorkflowTransitionFieldViewTestCase(
def _create_test_workflow_transition_field(self): def _create_test_workflow_transition_field(self):
self.test_workflow_transition_field = self.test_workflow_transition.fields.create( self.test_workflow_transition_field = self.test_workflow_transition.fields.create(
field_type=TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
name=TEST_WORKFLOW_TRANSITION_FIELD_NAME, name=TEST_WORKFLOW_TRANSITION_FIELD_NAME,
label=TEST_WORKFLOW_TRANSITION_FIELD_LABEL, label=TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
help_text=TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT help_text=TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT
@@ -289,6 +292,7 @@ class WorkflowTransitionFieldViewTestCase(
viewname='document_states:setup_workflow_transition_field_create', viewname='document_states:setup_workflow_transition_field_create',
kwargs={'pk': self.test_workflow_transition.pk}, kwargs={'pk': self.test_workflow_transition.pk},
data={ data={
'field_type': TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
'name': TEST_WORKFLOW_TRANSITION_FIELD_NAME, 'name': TEST_WORKFLOW_TRANSITION_FIELD_NAME,
'label': TEST_WORKFLOW_TRANSITION_FIELD_LABEL, 'label': TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
'help_text': TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT 'help_text': TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT

View File

@@ -16,7 +16,7 @@ from mayan.apps.documents.models import Document
from ..forms import WorkflowInstanceTransitionSelectForm from ..forms import WorkflowInstanceTransitionSelectForm
from ..icons import icon_workflow_instance_detail, icon_workflow_list from ..icons import icon_workflow_instance_detail, icon_workflow_list
from ..links import link_workflow_instance_transition from ..links import link_workflow_instance_transition
from ..literals import FIELD_TYPE_MAPPING from ..literals import FIELD_TYPE_MAPPING, WIDGET_CLASS_MAPPING
from ..models import WorkflowInstance from ..models import WorkflowInstance
from ..permissions import permission_workflow_view from ..permissions import permission_workflow_view
@@ -165,9 +165,15 @@ class WorkflowInstanceTransitionExecuteView(FormView):
for field in self.get_workflow_transition().fields.all(): for field in self.get_workflow_transition().fields.all():
schema['fields'][field.name] = { schema['fields'][field.name] = {
'class': FIELD_TYPE_MAPPING[field.field_type],
'help_text': field.help_text,
'label': field.label, 'label': field.label,
'class': FIELD_TYPE_MAPPING[field.field_type], 'kwargs': { 'required': field.required,
} }
if field.widget:
schema['widgets'][field.name] = {
'class': WIDGET_CLASS_MAPPING[field.widget],
'kwargs': field.get_widget_kwargs()
} }
return {'schema': schema} return {'schema': schema}

View File

@@ -739,8 +739,10 @@ class SetupWorkflowTransitionTriggerEventListView(ExternalObjectMixin, FormView)
class SetupWorkflowTransitionFieldCreateView(ExternalObjectMixin, SingleObjectCreateView): class SetupWorkflowTransitionFieldCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = WorkflowTransition external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit external_object_permission = permission_workflow_edit
fields = ('name', 'label', 'field_type', 'help_text', 'required') fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
def get_extra_context(self): def get_extra_context(self):
return { return {
'navigation_object_list': ('transition', 'workflow'), 'navigation_object_list': ('transition', 'workflow'),
@@ -789,7 +791,10 @@ class SetupWorkflowTransitionFieldDeleteView(SingleObjectDeleteView):
class SetupWorkflowTransitionFieldEditView(SingleObjectEditView): class SetupWorkflowTransitionFieldEditView(SingleObjectEditView):
fields = ('name', 'label', 'field_type', 'help_text', 'required',) fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
model = WorkflowTransitionField model = WorkflowTransitionField
object_permission = permission_workflow_edit object_permission = permission_workflow_edit