From c9fd8b02e3b805ac6720eb5b8455fb0ee2bbf959 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 1 Jul 2019 01:12:02 -0400 Subject: [PATCH] Add field type selection Signed-off-by: Roberto Rosario --- mayan/apps/document_states/apps.py | 17 ++- mayan/apps/document_states/forms.py | 1 + mayan/apps/document_states/icons.py | 32 +++-- mayan/apps/document_states/literals.py | 12 ++ ...630_1331.py => 0014_auto_20190701_0454.py} | 16 ++- mayan/apps/document_states/models.py | 39 +++--- .../tests/test_workflow_transition_views.py | 124 ++++++++++++++++++ .../views/workflow_instance_views.py | 8 +- .../document_states/views/workflow_views.py | 42 +++--- 9 files changed, 234 insertions(+), 57 deletions(-) rename mayan/apps/document_states/migrations/{0014_auto_20190630_1331.py => 0014_auto_20190701_0454.py} (55%) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index 65186bb408..2c2888bade 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -157,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', @@ -206,16 +209,20 @@ class DocumentStatesApp(MayanAppConfig): attribute='datetime' ) SourceColumn( - source=WorkflowInstanceLogEntry, label=_('User'), attribute='user' + source=WorkflowInstanceLogEntry, attribute='user' ) SourceColumn( - source=WorkflowInstanceLogEntry, label=_('Transition'), + source=WorkflowInstanceLogEntry, attribute='transition' ) SourceColumn( - source=WorkflowInstanceLogEntry, label=_('Comment'), + source=WorkflowInstanceLogEntry, attribute='comment' ) + SourceColumn( + source=WorkflowInstanceLogEntry, + attribute='extra_data' + ) SourceColumn( attribute='label', is_sortable=True, source=WorkflowState @@ -269,6 +276,10 @@ class DocumentStatesApp(MayanAppConfig): 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 diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index dfb2e6fd65..1e10ea9a14 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -175,6 +175,7 @@ class WorkflowInstanceTransitionSelectForm(forms.Form): ].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() ) diff --git a/mayan/apps/document_states/icons.py b/mayan/apps/document_states/icons.py index db6c5b5925..5c54da1aee 100644 --- a/mayan/apps/document_states/icons.py +++ b/mayan/apps/document_states/icons.py @@ -26,7 +26,9 @@ 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,8 +60,12 @@ 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' @@ -72,19 +78,27 @@ 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='code') -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 = 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='code', + driver_name='fontawesome-dual', primary_symbol='table', secondary_symbol='plus' ) -icon_workflow_transition_field_list = Icon(driver_name='fontawesome', symbol='code') +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/literals.py b/mayan/apps/document_states/literals.py index 79fc51f828..e18064ae0e 100644 --- a/mayan/apps/document_states/literals.py +++ b/mayan/apps/document_states/literals.py @@ -9,3 +9,15 @@ WORKFLOW_ACTION_WHEN_CHOICES = ( (WORKFLOW_ACTION_ON_ENTRY, _('On entry')), (WORKFLOW_ACTION_ON_EXIT, _('On exit')), ) + +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', +} diff --git a/mayan/apps/document_states/migrations/0014_auto_20190630_1331.py b/mayan/apps/document_states/migrations/0014_auto_20190701_0454.py similarity index 55% rename from mayan/apps/document_states/migrations/0014_auto_20190630_1331.py rename to mayan/apps/document_states/migrations/0014_auto_20190701_0454.py index 8f4e567f9a..6ede77fcd0 100644 --- a/mayan/apps/document_states/migrations/0014_auto_20190630_1331.py +++ b/mayan/apps/document_states/migrations/0014_auto_20190701_0454.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.20 on 2019-06-30 13:31 +# Generated by Django 1.11.20 on 2019-07-01 04:54 from __future__ import unicode_literals from django.db import migrations, models @@ -17,10 +17,11 @@ class Migration(migrations.Migration): name='WorkflowTransitionField', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Internal name')), - ('label', models.CharField(max_length=128, verbose_name='Label')), - ('help_text', models.TextField(blank=True, verbose_name='Help text')), - ('required', models.BooleanField(default=False, verbose_name='Required')), + ('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={ @@ -33,6 +34,11 @@ class Migration(migrations.Migration): 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/models.py b/mayan/apps/document_states/models.py index 7a4c6647bd..60e83adc06 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -23,8 +23,8 @@ 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, WORKFLOW_ACTION_WHEN_CHOICES, + WORKFLOW_ACTION_ON_ENTRY, WORKFLOW_ACTION_ON_EXIT ) from .managers import WorkflowManager from .permissions import permission_workflow_transition @@ -369,6 +369,9 @@ class WorkflowTransitionField(models.Model): 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 ' @@ -390,7 +393,6 @@ class WorkflowTransitionField(models.Model): 'Whether this fields needs to be filled out or not to proceed.' ), verbose_name=_('Required') ) - #TODO: widget, widget kwargs class Meta: unique_together = ('transition', 'name') @@ -431,7 +433,7 @@ class WorkflowInstance(models.Model): verbose_name=_('Document') ) context = models.TextField( - blank=True, verbose_name=_('Backend data') + blank=True, verbose_name=_('Context') ) class Meta: @@ -447,25 +449,25 @@ class WorkflowInstance(models.Model): with transaction.atomic(): try: if transition in self.get_current_state().origin_transitions.all(): - self.log_entries.create( - comment=comment or '', transition=transition, user=user - ) if extra_data: - data = self.loads() - data.update(extra_data) - self.dumps(data=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 - # TODO: execute transition event target = document, - # action_object = self - - def dumps(self, data): + def dumps(self, context): """ Serialize the context data. """ - self.context = json.dumps(data) + self.context = json.dumps(context) self.save() def get_absolute_url(self): @@ -579,6 +581,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',) @@ -592,6 +595,12 @@ 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 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) 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..4a7e66f837 100644 --- a/mayan/apps/document_states/tests/test_workflow_transition_views.py +++ b/mayan/apps/document_states/tests/test_workflow_transition_views.py @@ -16,6 +16,10 @@ from .mixins import ( WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin ) +TEST_WORKFLOW_TRANSITION_FIELD_NAME = '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' + class WorkflowTransitionViewTestCase( WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin, @@ -232,3 +236,123 @@ 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( + 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={ + '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/views/workflow_instance_views.py b/mayan/apps/document_states/views/workflow_instance_views.py index f857049829..0ff1e8493d 100644 --- a/mayan/apps/document_states/views/workflow_instance_views.py +++ b/mayan/apps/document_states/views/workflow_instance_views.py @@ -4,7 +4,7 @@ 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, reverse_lazy +from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList @@ -16,6 +16,7 @@ from mayan.apps.documents.models import Document 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 from ..models import WorkflowInstance from ..permissions import permission_workflow_view @@ -165,7 +166,7 @@ class WorkflowInstanceTransitionExecuteView(FormView): for field in self.get_workflow_transition().fields.all(): schema['fields'][field.name] = { 'label': field.label, - 'class': 'django.forms.CharField', 'kwargs': { + 'class': FIELD_TYPE_MAPPING[field.field_type], 'kwargs': { } } @@ -219,6 +220,3 @@ class WorkflowInstanceTransitionSelectView(ExternalObjectMixin, FormView): 'user': self.request.user, 'workflow_instance': self.external_object } - - #def get_workflow_instance(self): - # return get_object_or_404(klass=WorkflowInstance, pk=self.kwargs['pk']) diff --git a/mayan/apps/document_states/views/workflow_views.py b/mayan/apps/document_states/views/workflow_views.py index b28970e932..34d845458a 100644 --- a/mayan/apps/document_states/views/workflow_views.py +++ b/mayan/apps/document_states/views/workflow_views.py @@ -31,12 +31,13 @@ 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, @@ -738,8 +739,7 @@ class SetupWorkflowTransitionTriggerEventListView(ExternalObjectMixin, FormView) class SetupWorkflowTransitionFieldCreateView(ExternalObjectMixin, SingleObjectCreateView): external_object_class = WorkflowTransition external_object_permission = permission_workflow_edit - fields = ('name', 'label', 'help_text', 'required') - #object_permission = permission_workflow_edit + fields = ('name', 'label', 'field_type', 'help_text', 'required') def get_extra_context(self): return { @@ -789,7 +789,7 @@ class SetupWorkflowTransitionFieldDeleteView(SingleObjectDeleteView): class SetupWorkflowTransitionFieldEditView(SingleObjectEditView): - fields = ('name', 'label', 'help_text', 'required',) + fields = ('name', 'label', 'field_type', 'help_text', 'required',) model = WorkflowTransitionField object_permission = permission_workflow_edit @@ -819,21 +819,23 @@ class SetupWorkflowTransitionFieldListView(ExternalObjectMixin, SingleObjectList return { 'hide_object': True, 'navigation_object_list': ('object', 'workflow'), - #'no_results_icon': icon_workflow_transition_action, - #'no_results_main_link': link_setup_workflow_transition_action_selection.resolve( - # context=RequestContext( - # request=self.request, dict_={ - # 'object': self.get_workflow_transition() - # } - # ) - #), - #'no_results_text': _( - # 'Workflow state actions are macros that get executed when ' - # 'documents enters or leaves the state in which they reside.' - #), - #'no_results_title': _( - # 'There are no actions for this workflow state' - #), + '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'