Backport workflow context and field support

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-07-06 04:13:26 -04:00
parent 941356ed69
commit 4c212f6ea4
19 changed files with 842 additions and 99 deletions

View File

@@ -8,6 +8,8 @@
- Backport workflow preview refactor. GitLab issue #532.
- Add support for source column inheritance.
- Add support for source column exclusion.
- Backport workflow context support.
- Backport workflow transitions field support.
3.2.5 (2019-07-05)
==================

View File

@@ -20,6 +20,8 @@ Changes
- Backport workflow preview refactor. GitLab issue #532.
- Add support for source column inheritance.
- Add support for source column exclusion.
- Backport workflow context support.
- Backport workflow transitions field support.
Removals
--------

View File

@@ -27,6 +27,7 @@ from .dependencies import * # NOQA
from .handlers import (
handler_index_document, handler_launch_workflow, handler_trigger_transition
)
from .html_widgets import WorkflowLogExtraDataWidget, widget_transition_events
from .links import (
link_document_workflow_instance_list, link_setup_document_type_workflows,
link_setup_workflow_document_types, link_setup_workflow_create,
@@ -40,6 +41,10 @@ from .links import (
link_setup_workflow_state_edit, link_setup_workflow_transitions,
link_setup_workflow_transition_create,
link_setup_workflow_transition_delete, link_setup_workflow_transition_edit,
link_setup_workflow_transition_field_create,
link_setup_workflow_transition_field_delete,
link_setup_workflow_transition_field_edit,
link_setup_workflow_transition_field_list,
link_tool_launch_all_workflows, link_workflow_instance_detail,
link_workflow_instance_transition, link_workflow_runtime_proxy_document_list,
link_workflow_runtime_proxy_list, link_workflow_preview,
@@ -50,7 +55,6 @@ from .permissions import (
permission_workflow_delete, permission_workflow_edit,
permission_workflow_transition, permission_workflow_view
)
from .widgets import widget_transition_events
class DocumentStatesApp(MayanAppConfig):
@@ -86,6 +90,7 @@ class DocumentStatesApp(MayanAppConfig):
WorkflowStateAction = self.get_model('WorkflowStateAction')
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
WorkflowTransition = self.get_model('WorkflowTransition')
WorkflowTransitionField = self.get_model('WorkflowTransitionField')
WorkflowTransitionTriggerEvent = self.get_model(
'WorkflowTransitionTriggerEvent'
)
@@ -152,6 +157,9 @@ class DocumentStatesApp(MayanAppConfig):
ModelPermission.register_inheritance(
model=WorkflowTransition, related='workflow',
)
ModelPermission.register_inheritance(
model=WorkflowTransitionField, related='transition',
)
ModelPermission.register_inheritance(
model=WorkflowTransitionTriggerEvent,
related='transition__workflow',
@@ -160,9 +168,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn(
attribute='label', is_sortable=True, source=Workflow
)
SourceColumn(
column_workflow_internal_name = SourceColumn(
attribute='internal_name', is_sortable=True, source=Workflow
)
column_workflow_internal_name.add_exclude(source=WorkflowRuntimeProxy)
SourceColumn(
attribute='get_initial_state', empty_value=_('None'),
source=Workflow
@@ -203,12 +212,25 @@ class DocumentStatesApp(MayanAppConfig):
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Transition'),
attribute='transition'
source=WorkflowInstanceLogEntry,
attribute='transition__origin_state', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Comment'),
attribute='comment'
source=WorkflowInstanceLogEntry,
attribute='transition', is_sortable=True
)
SourceColumn(
source=WorkflowInstanceLogEntry,
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(
@@ -256,6 +278,43 @@ class DocumentStatesApp(MayanAppConfig):
)
)
SourceColumn(
attribute='name', is_identifier=True, is_sortable=True,
source=WorkflowTransitionField
)
SourceColumn(
attribute='label', is_sortable=True, source=WorkflowTransitionField
)
SourceColumn(
attribute='get_field_type_display', label=_('Type'),
source=WorkflowTransitionField
)
SourceColumn(
attribute='required', is_sortable=True,
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(
source=WorkflowRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
SourceColumn(
source=WorkflowStateRuntimeProxy, label=_('Documents'),
func=lambda context: context['object'].get_document_count(
user=context['request'].user
), order=99
)
menu_facet.bind_links(
links=(link_document_workflow_instance_list,), sources=(Document,)
)
@@ -291,10 +350,17 @@ class DocumentStatesApp(MayanAppConfig):
menu_object.bind_links(
links=(
link_setup_workflow_transition_edit,
link_workflow_transition_events, link_acl_list,
link_workflow_transition_events,
link_setup_workflow_transition_field_list, link_acl_list,
link_setup_workflow_transition_delete
), sources=(WorkflowTransition,)
)
menu_object.bind_links(
links=(
link_setup_workflow_transition_field_delete,
link_setup_workflow_transition_field_edit
), sources=(WorkflowTransitionField,)
)
menu_object.bind_links(
links=(
link_workflow_instance_detail,
@@ -328,6 +394,12 @@ class DocumentStatesApp(MayanAppConfig):
'document_states:setup_workflow_list'
)
)
menu_secondary.bind_links(
links=(link_setup_workflow_transition_field_create,),
sources=(
WorkflowTransition,
)
)
menu_secondary.bind_links(
links=(link_workflow_runtime_proxy_list,),
sources=(

View File

@@ -165,26 +165,19 @@ WorkflowTransitionTriggerEventRelationshipFormSet = formset_factory(
)
class WorkflowInstanceTransitionForm(forms.Form):
class WorkflowInstanceTransitionSelectForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
workflow_instance = kwargs.pop('workflow_instance')
super(WorkflowInstanceTransitionForm, self).__init__(*args, **kwargs)
super(WorkflowInstanceTransitionSelectForm, self).__init__(*args, **kwargs)
self.fields[
'transition'
].queryset = workflow_instance.get_transition_choices(_user=user)
transition = forms.ModelChoiceField(
help_text=_('Select a transition to execute in the next step.'),
label=_('Transition'), queryset=WorkflowTransition.objects.none()
)
comment = forms.CharField(
help_text=_('Optional comment to attach to the transition.'),
label=_('Comment'), required=False, widget=forms.widgets.Textarea(
attrs={
'rows': 3
}
)
)
class WorkflowPreviewForm(forms.Form):

View File

@@ -0,0 +1,27 @@
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()
)
)
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.documents.icons import icon_document_type
icon_workflow = Icon(driver_name='fontawesome', symbol='sitemap')
icon_document_type_workflow_list = icon_workflow
@@ -25,8 +24,9 @@ icon_workflow_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_workflow_list = Icon(driver_name='fontawesome', symbol='sitemap')
icon_workflow_preview = Icon(driver_name='fontawesome', symbol='eye')
icon_workflow_instance_detail = Icon(driver_name='fontawesome', symbol='sitemap')
icon_workflow_instance_detail = Icon(
driver_name='fontawesome', symbol='sitemap'
)
icon_workflow_instance_transition = Icon(
driver_name='fontawesome', symbol='arrows-alt-h'
)
@@ -58,13 +58,19 @@ icon_workflow_state_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_state_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_workflow_state_action = Icon(driver_name='fontawesome', symbol='code')
icon_workflow_state_action_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_state_action_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_workflow_state_action_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_state_action_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_state_action_selection = Icon(
driver_name='fontawesome-dual', primary_symbol='code',
secondary_symbol='plus'
)
icon_workflow_state_action_list = Icon(driver_name='fontawesome', symbol='code')
icon_workflow_state_action_list = Icon(
driver_name='fontawesome', symbol='code'
)
icon_workflow_transition = Icon(
driver_name='fontawesome', symbol='arrows-alt-h'
)
@@ -72,10 +78,28 @@ icon_workflow_transition_create = Icon(
driver_name='fontawesome-dual', primary_symbol='arrows-alt-h',
secondary_symbol='plus'
)
icon_workflow_transition_delete = Icon(driver_name='fontawesome', symbol='times')
icon_workflow_transition_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_transition_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_transition_field = Icon(driver_name='fontawesome', symbol='table')
icon_workflow_transition_field_delete = Icon(
driver_name='fontawesome', symbol='times'
)
icon_workflow_transition_field_edit = Icon(
driver_name='fontawesome', symbol='pencil-alt'
)
icon_workflow_transition_field_create = Icon(
driver_name='fontawesome-dual', primary_symbol='table',
secondary_symbol='plus'
)
icon_workflow_transition_field_list = Icon(
driver_name='fontawesome', symbol='table'
)
icon_workflow_transition_triggers = Icon(
driver_name='fontawesome', symbol='bolt'
)

View File

@@ -129,6 +129,35 @@ link_workflow_transition_events = Link(
text=_('Transition triggers'),
view='document_states:setup_workflow_transition_events'
)
# Workflow transition fields
link_setup_workflow_transition_field_create = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field',
permissions=(permission_workflow_edit,), text=_('Create field'),
view='document_states:setup_workflow_transition_field_create',
)
link_setup_workflow_transition_field_delete = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_delete',
permissions=(permission_workflow_edit,),
tags='dangerous', text=_('Delete'),
view='document_states:setup_workflow_transition_field_delete',
)
link_setup_workflow_transition_field_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',
permissions=(permission_workflow_edit,),
text=_('Edit'), view='document_states:setup_workflow_transition_field_edit',
)
link_setup_workflow_transition_field_list = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_list',
permissions=(permission_workflow_edit,),
text=_('Fields'),
view='document_states:setup_workflow_transition_field_list',
)
link_workflow_preview = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_preview',
@@ -159,7 +188,7 @@ link_workflow_instance_transition = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_instance_transition',
text=_('Transition'),
view='document_states:workflow_instance_transition',
view='document_states:workflow_instance_transition_selection',
)
# Runtime proxies

View File

@@ -2,6 +2,27 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
FIELD_TYPE_CHOICE_CHAR = 1
FIELD_TYPE_CHOICE_INTEGER = 2
FIELD_TYPE_CHOICES = (
(FIELD_TYPE_CHOICE_CHAR, _('Character')),
(FIELD_TYPE_CHOICE_INTEGER, _('Number (Integer)')),
)
FIELD_TYPE_MAPPING = {
FIELD_TYPE_CHOICE_CHAR: 'django.forms.CharField',
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

View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-01 04:54
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('document_states', '0013_auto_20190423_0810'),
]
operations = [
migrations.CreateModel(
name='WorkflowTransitionField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('field_type', models.PositiveIntegerField(choices=[(1, 'Character'), (2, 'Number (Integer)')], verbose_name='Type')),
('name', models.CharField(help_text='The name that will be used to identify this field in other parts of the workflow system.', max_length=128, verbose_name='Internal name')),
('label', models.CharField(help_text='The field name that will be shown on the user interface.', max_length=128, verbose_name='Label')),
('help_text', models.TextField(blank=True, help_text='An optional message that will help users better understand the purpose of the field and data to provide.', verbose_name='Help text')),
('required', models.BooleanField(default=False, help_text='Whether this fields needs to be filled out or not to proceed.', verbose_name='Required')),
('transition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='document_states.WorkflowTransition', verbose_name='Transition')),
],
options={
'verbose_name': 'Workflow transition trigger event',
'verbose_name_plural': 'Workflow transitions trigger events',
},
),
migrations.AddField(
model_name='workflowinstance',
name='context',
field=models.TextField(blank=True, verbose_name='Backend data'),
),
migrations.AddField(
model_name='workflowinstancelogentry',
name='extra_data',
field=models.TextField(blank=True, verbose_name='Extra data'),
),
migrations.AlterUniqueTogether(
name='workflowtransitionfield',
unique_together=set([('transition', 'name')]),
),
]

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

@@ -6,6 +6,11 @@ import logging
from furl import furl
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.core import serializers
@@ -19,15 +24,16 @@ from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
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.permissions import permission_document_view
from mayan.apps.events.models import StoredEventType
from .error_logs import error_log_state_actions
from .events import event_workflow_created, event_workflow_edited
from .literals import (
WORKFLOW_ACTION_WHEN_CHOICES, WORKFLOW_ACTION_ON_ENTRY,
WORKFLOW_ACTION_ON_EXIT
FIELD_TYPE_CHOICES, WIDGET_CLASS_CHOICES, WORKFLOW_ACTION_WHEN_CHOICES,
WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT
)
from .managers import WorkflowManager
from .permissions import permission_workflow_transition
@@ -407,6 +413,61 @@ class WorkflowTransition(models.Model):
return self.label
@python_2_unicode_compatible
class WorkflowTransitionField(models.Model):
transition = models.ForeignKey(
on_delete=models.CASCADE, related_name='fields',
to=WorkflowTransition, verbose_name=_('Transition')
)
field_type = models.PositiveIntegerField(
choices=FIELD_TYPE_CHOICES, verbose_name=_('Type')
)
name = models.CharField(
help_text=_(
'The name that will be used to identify this field in other parts '
'of the workflow system.'
), max_length=128, verbose_name=_('Internal name')
)
label = models.CharField(
help_text=_(
'The field name that will be shown on the user interface.'
), max_length=128, verbose_name=_('Label'))
help_text = models.TextField(
blank=True, help_text=_(
'An optional message that will help users better understand the '
'purpose of the field and data to provide.'
), verbose_name=_('Help text')
)
required = models.BooleanField(
default=False, help_text=_(
'Whether this fields needs to be filled out or not to proceed.'
), 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:
unique_together = ('transition', 'name')
verbose_name = _('Workflow transition trigger event')
verbose_name_plural = _('Workflow transitions trigger events')
def __str__(self):
return self.label
def get_widget_kwargs(self):
return yaml.load(stream=self.widget_kwargs, Loader=SafeLoader)
@python_2_unicode_compatible
class WorkflowTransitionTriggerEvent(models.Model):
transition = models.ForeignKey(
@@ -436,6 +497,9 @@ class WorkflowInstance(models.Model):
on_delete=models.CASCADE, related_name='workflows', to=Document,
verbose_name=_('Document')
)
context = models.TextField(
blank=True, verbose_name=_('Context')
)
class Meta:
ordering = ('workflow',)
@@ -446,15 +510,30 @@ class WorkflowInstance(models.Model):
def __str__(self):
return force_text(self.workflow)
def do_transition(self, transition, user=None, comment=None):
try:
if transition in self.get_current_state().origin_transitions.all():
self.log_entries.create(
comment=comment or '', transition=transition, user=user
)
except AttributeError:
# No initial state has been set for this workflow
pass
def do_transition(self, transition, extra_data=None, user=None, comment=None):
with transaction.atomic():
try:
if transition in self.get_current_state().origin_transitions.all():
if extra_data:
context = self.loads()
context.update(extra_data)
self.dumps(context=context)
self.log_entries.create(
comment=comment or '',
extra_data=json.dumps(extra_data or {}),
transition=transition, user=user
)
except AttributeError:
# No initial state has been set for this workflow
pass
def dumps(self, context):
"""
Serialize the context data.
"""
self.context = json.dumps(context)
self.save()
def get_absolute_url(self):
return reverse(
@@ -464,10 +543,12 @@ class WorkflowInstance(models.Model):
)
def get_context(self):
return {
context = {
'document': self.document, 'workflow': self.workflow,
'workflow_instance': self,
}
context['workflow_instance_context'] = self.loads()
return context
def get_current_state(self):
"""
@@ -533,6 +614,12 @@ class WorkflowInstance(models.Model):
"""
return WorkflowTransition.objects.none()
def loads(self):
"""
Deserialize the context data.
"""
return json.loads(self.context or '{}')
@python_2_unicode_compatible
class WorkflowInstanceLogEntry(models.Model):
@@ -559,6 +646,7 @@ class WorkflowInstanceLogEntry(models.Model):
to=settings.AUTH_USER_MODEL, verbose_name=_('User')
)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
extra_data = models.TextField(blank=True, verbose_name=_('Extra data'))
class Meta:
ordering = ('datetime',)
@@ -572,33 +660,47 @@ class WorkflowInstanceLogEntry(models.Model):
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
raise ValidationError(_('Not a valid transition choice.'))
def save(self, *args, **kwargs):
result = super(WorkflowInstanceLogEntry, self).save(*args, **kwargs)
context = self.workflow_instance.get_context()
context.update(
{
'entry_log': self
}
)
for action in self.transition.origin_state.exit_actions.filter(enabled=True):
context.update(
{
'action': action,
}
)
action.execute(context=context)
for action in self.transition.destination_state.entry_actions.filter(enabled=True):
context.update(
{
'action': action,
}
)
action.execute(context=context)
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):
"""
Deserialize the context data.
"""
return json.loads(self.extra_data or '{}')
def save(self, *args, **kwargs):
with transaction.atomic():
result = super(WorkflowInstanceLogEntry, self).save(*args, **kwargs)
context = self.workflow_instance.get_context()
context.update(
{
'entry_log': self
}
)
for action in self.transition.origin_state.exit_actions.filter(enabled=True):
context.update(
{
'action': action,
}
)
action.execute(context=context)
for action in self.transition.destination_state.entry_actions.filter(enabled=True):
context.update(
{
'action': action,
}
)
action.execute(context=context)
return result
class WorkflowRuntimeProxy(Workflow):
class Meta:
@@ -606,9 +708,30 @@ class WorkflowRuntimeProxy(Workflow):
verbose_name = _('Workflow runtime proxy')
verbose_name_plural = _('Workflow runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents executing this workflow.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.filter(workflows__workflow=self),
user=user
).count()
class WorkflowStateRuntimeProxy(WorkflowState):
class Meta:
proxy = True
verbose_name = _('Workflow state runtime proxy')
verbose_name_plural = _('Workflow state runtime proxies')
def get_document_count(self, user):
"""
Return the numeric count of documents at this workflow state.
The count is filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.get_documents(),
user=user
).count()

View File

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

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals
from ..literals import FIELD_TYPE_CHOICE_CHAR
TEST_INDEX_LABEL = 'test workflow index'
TEST_WORKFLOW_LABEL = 'test workflow label'
@@ -11,6 +13,10 @@ TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT = 'test workflow instance log entry com
TEST_WORKFLOW_STATE_LABEL = 'test state label'
TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited'
TEST_WORKFLOW_STATE_COMPLETION = 66
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
TEST_WORKFLOW_TRANSITION_LABEL = 'test transition label'
TEST_WORKFLOW_TRANSITION_LABEL_2 = 'test transition label 2'
TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited'
@@ -18,3 +24,4 @@ TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited'
TEST_INDEX_TEMPLATE_METADATA_EXPRESSION = '{{{{ document.workflow.{}.get_current_state }}}}'.format(
TEST_WORKFLOW_INTERNAL_NAME
)

View File

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

View File

@@ -10,7 +10,10 @@ from ..permissions import (
)
from .literals import (
TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT,
TEST_WORKFLOW_TRANSITION_FIELD_LABEL, TEST_WORKFLOW_TRANSITION_FIELD_NAME,
TEST_WORKFLOW_TRANSITION_FIELD_TYPE, TEST_WORKFLOW_TRANSITION_LABEL,
TEST_WORKFLOW_TRANSITION_LABEL_EDITED
)
from .mixins import (
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
@@ -160,7 +163,7 @@ class WorkflowTransitionDocumentViewTestCase(
permission.
"""
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
self.assertEqual(
@@ -232,3 +235,125 @@ class WorkflowTransitionEventViewTestCase(
response = self._request_test_workflow_transition_event_list_view()
self.assertEqual(response.status_code, 200)
class WorkflowTransitionFieldViewTestCase(
WorkflowTestMixin, WorkflowTransitionViewTestMixin, GenericViewTestCase
):
def setUp(self):
super(WorkflowTransitionFieldViewTestCase, self).setUp()
self._create_test_workflow()
self._create_test_workflow_states()
self._create_test_workflow_transition()
def _create_test_workflow_transition_field(self):
self.test_workflow_transition_field = self.test_workflow_transition.fields.create(
field_type=TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
name=TEST_WORKFLOW_TRANSITION_FIELD_NAME,
label=TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
help_text=TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT
)
def _request_test_workflow_transition_field_list_view(self):
return self.get(
viewname='document_states:setup_workflow_transition_field_list',
kwargs={'pk': self.test_workflow_transition.pk}
)
def test_workflow_transition_field_list_view_no_permission(self):
self._create_test_workflow_transition_field()
response = self._request_test_workflow_transition_field_list_view()
self.assertNotContains(
response=response,
text=self.test_workflow_transition_field.label,
status_code=404
)
def test_workflow_transition_field_list_view_with_access(self):
self._create_test_workflow_transition_field()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_test_workflow_transition_field_list_view()
self.assertContains(
response=response,
text=self.test_workflow_transition_field.label,
status_code=200
)
def _request_workflow_transition_field_create_view(self):
return self.post(
viewname='document_states:setup_workflow_transition_field_create',
kwargs={'pk': self.test_workflow_transition.pk},
data={
'field_type': TEST_WORKFLOW_TRANSITION_FIELD_TYPE,
'name': TEST_WORKFLOW_TRANSITION_FIELD_NAME,
'label': TEST_WORKFLOW_TRANSITION_FIELD_LABEL,
'help_text': TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT
}
)
def test_workflow_transition_field_create_view_no_permission(self):
workflow_transition_field_count = self.test_workflow_transition.fields.count()
response = self._request_workflow_transition_field_create_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count
)
def test_workflow_transition_field_create_view_with_access(self):
workflow_transition_field_count = self.test_workflow_transition.fields.count()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_workflow_transition_field_create_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count + 1
)
def _request_workflow_transition_field_delete_view(self):
return self.post(
viewname='document_states:setup_workflow_transition_field_delete',
kwargs={'pk': self.test_workflow_transition_field.pk},
)
def test_workflow_transition_field_delete_view_no_permission(self):
self._create_test_workflow_transition_field()
workflow_transition_field_count = self.test_workflow_transition.fields.count()
response = self._request_workflow_transition_field_delete_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count
)
def test_workflow_transition_field_delete_view_with_access(self):
self._create_test_workflow_transition_field()
workflow_transition_field_count = self.test_workflow_transition.fields.count()
self.grant_access(
obj=self.test_workflow, permission=permission_workflow_edit
)
response = self._request_workflow_transition_field_delete_view()
self.assertEqual(response.status_code, 302)
self.assertEqual(
self.test_workflow_transition.fields.count(),
workflow_transition_field_count - 1
)

View File

@@ -23,10 +23,15 @@ from .views import (
SetupWorkflowTransitionEditView,
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView,
WorkflowInstanceTransitionView, WorkflowListView,
WorkflowPreviewView, WorkflowStateDocumentListView, WorkflowStateListView,
WorkflowInstanceTransitionExecuteView, WorkflowInstanceTransitionSelectView,
WorkflowListView, WorkflowPreviewView, WorkflowStateDocumentListView,
WorkflowStateListView,
)
from .views.workflow_views import (
SetupDocumentTypeWorkflowsView, SetupWorkflowTransitionFieldCreateView,
SetupWorkflowTransitionFieldDeleteView,
SetupWorkflowTransitionFieldEditView, SetupWorkflowTransitionFieldListView
)
from .views.workflow_views import SetupDocumentTypeWorkflowsView
urlpatterns_workflows = [
url(
@@ -36,6 +41,29 @@ urlpatterns_workflows = [
),
]
urlpatterns_workflow_transition_fields = [
url(
regex=r'^setup/workflows/transitions/(?P<pk>\d+)/fields/create/$',
view=SetupWorkflowTransitionFieldCreateView.as_view(),
name='setup_workflow_transition_field_create'
),
url(
regex=r'^setup/workflows/transitions/(?P<pk>\d+)/fields/$',
view=SetupWorkflowTransitionFieldListView.as_view(),
name='setup_workflow_transition_field_list'
),
url(
regex=r'^setup/workflows/transitions/fields/(?P<pk>\d+)/delete/$',
view=SetupWorkflowTransitionFieldDeleteView.as_view(),
name='setup_workflow_transition_field_delete'
),
url(
regex=r'^setup/workflows/transitions/fields/(?P<pk>\d+)/edit/$',
view=SetupWorkflowTransitionFieldEditView.as_view(),
name='setup_workflow_transition_field_edit'
),
]
urlpatterns = [
url(
regex=r'^document/(?P<pk>\d+)/workflows/$',
@@ -48,9 +76,14 @@ urlpatterns = [
name='workflow_instance_detail'
),
url(
regex=r'^document/workflows/(?P<pk>\d+)/transition/$',
view=WorkflowInstanceTransitionView.as_view(),
name='workflow_instance_transition'
regex=r'^document/workflows/(?P<pk>\d+)/transitions/select/$',
view=WorkflowInstanceTransitionSelectView.as_view(),
name='workflow_instance_transition_selection'
),
url(
regex=r'^document/workflows/(?P<workflow_instance_pk>\d+)/transitions/(?P<workflow_transition_pk>\d+)/execute/$',
view=WorkflowInstanceTransitionExecuteView.as_view(),
name='workflow_instance_transition_execute'
),
url(
regex=r'^setup/all/$', view=SetupWorkflowListView.as_view(),
@@ -179,7 +212,9 @@ urlpatterns = [
name='workflow_state_document_list'
),
]
urlpatterns.extend(urlpatterns_workflows)
urlpatterns.extend(urlpatterns_workflow_transition_fields)
api_urls = [
url(

View File

@@ -4,21 +4,26 @@ from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.forms import DynamicForm
from mayan.apps.common.generics import FormView, SingleObjectListView
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.documents.models import Document
from ..forms import WorkflowInstanceTransitionForm
from ..forms import WorkflowInstanceTransitionSelectForm
from ..icons import icon_workflow_instance_detail, icon_workflow_list
from ..links import link_workflow_instance_transition
from ..literals import FIELD_TYPE_MAPPING, WIDGET_CLASS_MAPPING
from ..models import WorkflowInstance
from ..permissions import permission_workflow_view
__all__ = (
'DocumentWorkflowInstanceListView', 'WorkflowInstanceDetailView',
'WorkflowInstanceTransitionView'
'WorkflowInstanceTransitionSelectView',
'WorkflowInstanceTransitionExecuteView'
)
@@ -100,14 +105,17 @@ class WorkflowInstanceDetailView(SingleObjectListView):
return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk'])
class WorkflowInstanceTransitionView(FormView):
form_class = WorkflowInstanceTransitionForm
class WorkflowInstanceTransitionExecuteView(FormView):
form_class = DynamicForm
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
form_data = form.cleaned_data
comment = form_data.pop('comment')
self.get_workflow_instance().do_transition(
comment=form.cleaned_data['comment'],
transition=form.cleaned_data['transition'], user=self.request.user
comment=comment, extra_data=form_data,
transition=self.get_workflow_transition(), user=self.request.user,
)
messages.success(
self.request, _(
@@ -122,19 +130,99 @@ class WorkflowInstanceTransitionView(FormView):
'object': self.get_workflow_instance().document,
'submit_label': _('Submit'),
'title': _(
'Do transition for workflow: %s'
) % self.get_workflow_instance(),
'Execute transition "%(transition)s" for workflow: %(workflow)s'
) % {
'transition': self.get_workflow_transition(),
'workflow': self.get_workflow_instance(),
},
'workflow_instance': self.get_workflow_instance(),
}
def get_form_extra_kwargs(self):
return {
'user': self.request.user,
'workflow_instance': self.get_workflow_instance()
schema = {
'fields': {
'comment': {
'label': _('Comment'),
'class': 'django.forms.CharField', 'kwargs': {
'help_text': _(
'Optional comment to attach to the transition.'
),
'required': False,
}
}
},
'widgets': {
'comment': {
'class': 'django.forms.widgets.Textarea',
'kwargs': {
'attrs': {
'rows': 3
}
}
}
}
}
for field in self.get_workflow_transition().fields.all():
schema['fields'][field.name] = {
'class': FIELD_TYPE_MAPPING[field.field_type],
'help_text': field.help_text,
'label': field.label,
'required': field.required,
}
if field.widget:
schema['widgets'][field.name] = {
'class': WIDGET_CLASS_MAPPING[field.widget],
'kwargs': field.get_widget_kwargs()
}
return {'schema': schema}
def get_success_url(self):
return self.get_workflow_instance().get_absolute_url()
def get_workflow_instance(self):
return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk'])
return get_object_or_404(
klass=WorkflowInstance, pk=self.kwargs['workflow_instance_pk']
)
def get_workflow_transition(self):
return get_object_or_404(
klass=self.get_workflow_instance().get_transition_choices(
_user=self.request.user
), pk=self.kwargs['workflow_transition_pk']
)
class WorkflowInstanceTransitionSelectView(ExternalObjectMixin, FormView):
external_object_class = WorkflowInstance
form_class = WorkflowInstanceTransitionSelectForm
template_name = 'appearance/generic_form.html'
def form_valid(self, form):
return HttpResponseRedirect(
redirect_to=reverse(
viewname='document_states:workflow_instance_transition_execute',
kwargs={
'workflow_instance_pk': self.external_object.pk,
'workflow_transition_pk': form.cleaned_data['transition'].pk
}
)
)
def get_extra_context(self):
return {
'navigation_object_list': ('object', 'workflow_instance'),
'object': self.external_object.document,
'submit_label': _('Select'),
'title': _(
'Select transition for workflow: %s'
) % self.external_object,
'workflow_instance': self.external_object,
}
def get_form_extra_kwargs(self):
return {
'user': self.request.user,
'workflow_instance': self.external_object
}

View File

@@ -30,15 +30,17 @@ from ..forms import (
)
from ..icons import (
icon_workflow_list, icon_workflow_state, icon_workflow_state_action,
icon_workflow_transition
icon_workflow_transition, icon_workflow_transition_field
)
from ..links import (
link_setup_workflow_create, link_setup_workflow_state_create,
link_setup_workflow_state_action_selection,
link_setup_workflow_transition_create
link_setup_workflow_transition_create,
link_setup_workflow_transition_field_create,
)
from ..models import (
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition,
WorkflowTransitionField
)
from ..permissions import (
permission_workflow_create, permission_workflow_delete,
@@ -731,6 +733,124 @@ class SetupWorkflowTransitionTriggerEventListView(ExternalObjectMixin, FormView)
)
# Transition fields
class SetupWorkflowTransitionFieldCreateView(ExternalObjectMixin, SingleObjectCreateView):
external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit
fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
def get_extra_context(self):
return {
'navigation_object_list': ('transition', 'workflow'),
'transition': self.external_object,
'title': _(
'Create a field for workflow transition: %s'
) % self.external_object,
'workflow': self.external_object.workflow
}
def get_instance_extra_data(self):
return {
'transition': self.external_object,
}
def get_queryset(self):
return self.external_object.fields.all()
def get_post_action_redirect(self):
return reverse(
viewname='document_states:setup_workflow_transition_field_list',
kwargs={'pk': self.external_object.pk}
)
class SetupWorkflowTransitionFieldDeleteView(SingleObjectDeleteView):
model = WorkflowTransitionField
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_transition', 'workflow'
),
'object': self.object,
'title': _('Delete workflow transition field: %s') % self.object,
'workflow': self.object.transition.workflow,
'workflow_transition': self.object.transition,
}
def get_post_action_redirect(self):
return reverse(
viewname='document_states:setup_workflow_transition_field_list',
kwargs={'pk': self.object.transition.pk}
)
class SetupWorkflowTransitionFieldEditView(SingleObjectEditView):
fields = (
'name', 'label', 'field_type', 'help_text', 'required', 'widget',
'widget_kwargs'
)
model = WorkflowTransitionField
object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'navigation_object_list': (
'object', 'workflow_transition', 'workflow'
),
'object': self.object,
'title': _('Edit workflow transition field: %s') % self.object,
'workflow': self.object.transition.workflow,
'workflow_transition': self.object.transition,
}
def get_post_action_redirect(self):
return reverse(
viewname='document_states:setup_workflow_transition_field_list',
kwargs={'pk': self.object.transition.pk}
)
class SetupWorkflowTransitionFieldListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = WorkflowTransition
external_object_permission = permission_workflow_edit
def get_extra_context(self):
return {
'hide_object': True,
'navigation_object_list': ('object', 'workflow'),
'no_results_icon': icon_workflow_transition_field,
'no_results_main_link': link_setup_workflow_transition_field_create.resolve(
context=RequestContext(
request=self.request, dict_={
'object': self.external_object
}
)
),
'no_results_text': _(
'Workflow transition fields allow adding data to the '
'workflow\'s context. This additional context data can then '
'be used by other elements of the workflow system like the '
'workflow state actions.'
),
'no_results_title': _(
'There are no fields for this workflow transition'
),
'object': self.external_object,
'title': _(
'Fields for workflow transition: %s'
) % self.external_object,
'workflow': self.external_object.workflow,
}
def get_source_queryset(self):
return self.external_object.fields.all()
class ToolLaunchAllWorkflows(ConfirmView):
extra_context = {
'title': _('Launch all workflows?'),

View File

@@ -1,17 +1,6 @@
from __future__ import unicode_literals
from django import forms
from django.utils.html import format_html_join
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()
)
)
class WorkflowImageWidget(forms.widgets.Widget):