From 4c212f6ea4f667087cc6b6d886ec2d75868a15e7 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 6 Jul 2019 04:13:26 -0400 Subject: [PATCH] Backport workflow context and field support Signed-off-by: Roberto Rosario --- HISTORY.rst | 2 + docs/releases/3.3.rst | 2 + mayan/apps/document_states/apps.py | 86 +++++++- mayan/apps/document_states/forms.py | 13 +- mayan/apps/document_states/html_widgets.py | 27 +++ mayan/apps/document_states/icons.py | 38 +++- mayan/apps/document_states/links.py | 31 ++- mayan/apps/document_states/literals.py | 21 ++ .../migrations/0014_auto_20190701_0454.py | 46 ++++ .../migrations/0015_auto_20190701_1311.py | 31 +++ mayan/apps/document_states/models.py | 197 ++++++++++++++---- .../templates/document_states/extra_data.html | 8 + mayan/apps/document_states/tests/literals.py | 7 + mayan/apps/document_states/tests/mixins.py | 7 +- .../tests/test_workflow_transition_views.py | 129 +++++++++++- mayan/apps/document_states/urls.py | 47 ++++- .../views/workflow_instance_views.py | 112 ++++++++-- .../document_states/views/workflow_views.py | 126 ++++++++++- mayan/apps/document_states/widgets.py | 11 - 19 files changed, 842 insertions(+), 99 deletions(-) create mode 100644 mayan/apps/document_states/html_widgets.py create mode 100644 mayan/apps/document_states/migrations/0014_auto_20190701_0454.py create mode 100644 mayan/apps/document_states/migrations/0015_auto_20190701_1311.py create mode 100644 mayan/apps/document_states/templates/document_states/extra_data.html diff --git a/HISTORY.rst b/HISTORY.rst index 0fb21226a7..d14468108c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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) ================== diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst index 980763c0a0..6a66ffeb19 100644 --- a/docs/releases/3.3.rst +++ b/docs/releases/3.3.rst @@ -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 -------- diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index f49310b836..0e35c0d59c 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -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=( diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index 930dbf49e9..e2320e2b46 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -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): diff --git a/mayan/apps/document_states/html_widgets.py b/mayan/apps/document_states/html_widgets.py new file mode 100644 index 0000000000..98ad2d817f --- /dev/null +++ b/mayan/apps/document_states/html_widgets.py @@ -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='
{}
', 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 + } + ) diff --git a/mayan/apps/document_states/icons.py b/mayan/apps/document_states/icons.py index 8ae3b8e990..9fa519fac1 100644 --- a/mayan/apps/document_states/icons.py +++ b/mayan/apps/document_states/icons.py @@ -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' ) diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index 3d23efec95..1404ff2644 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -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 diff --git a/mayan/apps/document_states/literals.py b/mayan/apps/document_states/literals.py index 674bbeebef..ad6906fd3b 100644 --- a/mayan/apps/document_states/literals.py +++ b/mayan/apps/document_states/literals.py @@ -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 diff --git a/mayan/apps/document_states/migrations/0014_auto_20190701_0454.py b/mayan/apps/document_states/migrations/0014_auto_20190701_0454.py new file mode 100644 index 0000000000..6ede77fcd0 --- /dev/null +++ b/mayan/apps/document_states/migrations/0014_auto_20190701_0454.py @@ -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')]), + ), + ] diff --git a/mayan/apps/document_states/migrations/0015_auto_20190701_1311.py b/mayan/apps/document_states/migrations/0015_auto_20190701_1311.py new file mode 100644 index 0000000000..baef5fc886 --- /dev/null +++ b/mayan/apps/document_states/migrations/0015_auto_20190701_1311.py @@ -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'), + ), + ] diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index edfa431691..d3d4103814 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -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() diff --git a/mayan/apps/document_states/templates/document_states/extra_data.html b/mayan/apps/document_states/templates/document_states/extra_data.html new file mode 100644 index 0000000000..b996601769 --- /dev/null +++ b/mayan/apps/document_states/templates/document_states/extra_data.html @@ -0,0 +1,8 @@ +{% if value %} + +{% endif %} + diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index 212427d2d3..c17a870e9c 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -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 ) + diff --git a/mayan/apps/document_states/tests/mixins.py b/mayan/apps/document_states/tests/mixins.py index 76a1865ed4..f8ea985a1c 100644 --- a/mayan/apps/document_states/tests/mixins.py +++ b/mayan/apps/document_states/tests/mixins.py @@ -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, } ) diff --git a/mayan/apps/document_states/tests/test_workflow_transition_views.py b/mayan/apps/document_states/tests/test_workflow_transition_views.py index 1eb8dfb133..c298e5dde4 100644 --- a/mayan/apps/document_states/tests/test_workflow_transition_views.py +++ b/mayan/apps/document_states/tests/test_workflow_transition_views.py @@ -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 + ) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index f37af45452..ebc3e6acd9 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -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\d+)/fields/create/$', + view=SetupWorkflowTransitionFieldCreateView.as_view(), + name='setup_workflow_transition_field_create' + ), + url( + regex=r'^setup/workflows/transitions/(?P\d+)/fields/$', + view=SetupWorkflowTransitionFieldListView.as_view(), + name='setup_workflow_transition_field_list' + ), + url( + regex=r'^setup/workflows/transitions/fields/(?P\d+)/delete/$', + view=SetupWorkflowTransitionFieldDeleteView.as_view(), + name='setup_workflow_transition_field_delete' + ), + url( + regex=r'^setup/workflows/transitions/fields/(?P\d+)/edit/$', + view=SetupWorkflowTransitionFieldEditView.as_view(), + name='setup_workflow_transition_field_edit' + ), +] + urlpatterns = [ url( regex=r'^document/(?P\d+)/workflows/$', @@ -48,9 +76,14 @@ urlpatterns = [ name='workflow_instance_detail' ), url( - regex=r'^document/workflows/(?P\d+)/transition/$', - view=WorkflowInstanceTransitionView.as_view(), - name='workflow_instance_transition' + regex=r'^document/workflows/(?P\d+)/transitions/select/$', + view=WorkflowInstanceTransitionSelectView.as_view(), + name='workflow_instance_transition_selection' + ), + url( + regex=r'^document/workflows/(?P\d+)/transitions/(?P\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( diff --git a/mayan/apps/document_states/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index 66750fc468..bed57fdcbc 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -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 + } diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py index 4abab87386..008c83a108 100644 --- a/mayan/apps/document_states/views/workflow_views.py +++ b/mayan/apps/document_states/views/workflow_views.py @@ -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?'), diff --git a/mayan/apps/document_states/widgets.py b/mayan/apps/document_states/widgets.py index 92e761b7b9..a1ccdb57d7 100644 --- a/mayan/apps/document_states/widgets.py +++ b/mayan/apps/document_states/widgets.py @@ -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='
{}
', args_generator=( - ( - transition_trigger.event_type.label, - ) for transition_trigger in transition.trigger_events.all() - ) - ) class WorkflowImageWidget(forms.widgets.Widget):