Add field type selection

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-07-01 01:12:02 -04:00
parent e1a63064dc
commit c9fd8b02e3
9 changed files with 234 additions and 57 deletions

View File

@@ -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

View File

@@ -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()
)

View File

@@ -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'

View File

@@ -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',
}

View File

@@ -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')]),

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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'])

View File

@@ -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'