Initial commit to support workflow actions.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
@@ -18,7 +18,7 @@ from mayan.celery import app
|
|||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .classes import DocumentStateHelper
|
from .classes import DocumentStateHelper, WorkflowAction
|
||||||
from .handlers import (
|
from .handlers import (
|
||||||
handler_index_document, handler_trigger_transition, launch_workflow
|
handler_index_document, handler_trigger_transition, launch_workflow
|
||||||
)
|
)
|
||||||
@@ -26,9 +26,13 @@ from .links import (
|
|||||||
link_document_workflow_instance_list, link_setup_workflow_document_types,
|
link_document_workflow_instance_list, link_setup_workflow_document_types,
|
||||||
link_setup_workflow_create, link_setup_workflow_delete,
|
link_setup_workflow_create, link_setup_workflow_delete,
|
||||||
link_setup_workflow_edit, link_setup_workflow_list,
|
link_setup_workflow_edit, link_setup_workflow_list,
|
||||||
link_setup_workflow_states, link_setup_workflow_state_create,
|
link_setup_workflow_states, link_setup_workflow_state_action_delete,
|
||||||
link_setup_workflow_state_delete, link_setup_workflow_state_edit,
|
link_setup_workflow_state_action_edit,
|
||||||
link_setup_workflow_transitions, link_setup_workflow_transition_create,
|
link_setup_workflow_state_action_list,
|
||||||
|
link_setup_workflow_state_action_selection,
|
||||||
|
link_setup_workflow_state_create, link_setup_workflow_state_delete,
|
||||||
|
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_delete, link_setup_workflow_transition_edit,
|
||||||
link_tool_launch_all_workflows, link_workflow_instance_detail,
|
link_tool_launch_all_workflows, link_workflow_instance_detail,
|
||||||
link_workflow_instance_transition, link_workflow_document_list,
|
link_workflow_instance_transition, link_workflow_document_list,
|
||||||
@@ -67,9 +71,12 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry')
|
WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry')
|
||||||
WorkflowRuntimeProxy = self.get_model('WorkflowRuntimeProxy')
|
WorkflowRuntimeProxy = self.get_model('WorkflowRuntimeProxy')
|
||||||
WorkflowState = self.get_model('WorkflowState')
|
WorkflowState = self.get_model('WorkflowState')
|
||||||
|
WorkflowStateAction = self.get_model('WorkflowStateAction')
|
||||||
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
|
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
|
||||||
WorkflowTransition = self.get_model('WorkflowTransition')
|
WorkflowTransition = self.get_model('WorkflowTransition')
|
||||||
|
|
||||||
|
WorkflowAction.initialize()
|
||||||
|
|
||||||
ModelAttribute(
|
ModelAttribute(
|
||||||
Document, 'workflow.< workflow internal name >.get_current_state',
|
Document, 'workflow.< workflow internal name >.get_current_state',
|
||||||
label=_('Current state of a workflow'), description=_(
|
label=_('Current state of a workflow'), description=_(
|
||||||
@@ -157,6 +164,22 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
source=WorkflowState, label=_('Completion'), attribute='completion'
|
source=WorkflowState, label=_('Completion'), attribute='completion'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=WorkflowStateAction, label=_('Label'), attribute='label'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=WorkflowStateAction, label=_('Enabled?'),
|
||||||
|
func=lambda context: two_state_template(context['object'].enabled)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=WorkflowStateAction, label=_('When?'),
|
||||||
|
attribute='get_when_display'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=WorkflowStateAction, label=_('Action type'),
|
||||||
|
attribute='get_class_label'
|
||||||
|
)
|
||||||
|
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=WorkflowTransition, label=_('Origin state'),
|
source=WorkflowTransition, label=_('Origin state'),
|
||||||
attribute='origin_state'
|
attribute='origin_state'
|
||||||
@@ -203,6 +226,7 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_setup_workflow_state_edit,
|
link_setup_workflow_state_edit,
|
||||||
|
link_setup_workflow_state_action_list,
|
||||||
link_setup_workflow_state_delete
|
link_setup_workflow_state_delete
|
||||||
), sources=(WorkflowState,)
|
), sources=(WorkflowState,)
|
||||||
)
|
)
|
||||||
@@ -229,6 +253,13 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
link_workflow_state_document_list,
|
link_workflow_state_document_list,
|
||||||
), sources=(WorkflowStateRuntimeProxy,)
|
), sources=(WorkflowStateRuntimeProxy,)
|
||||||
)
|
)
|
||||||
|
menu_object.bind_links(
|
||||||
|
links=(
|
||||||
|
link_setup_workflow_state_action_edit,
|
||||||
|
link_setup_workflow_state_action_delete,
|
||||||
|
), sources=(WorkflowStateAction,)
|
||||||
|
)
|
||||||
|
|
||||||
menu_secondary.bind_links(
|
menu_secondary.bind_links(
|
||||||
links=(link_setup_workflow_list, link_setup_workflow_create),
|
links=(link_setup_workflow_list, link_setup_workflow_create),
|
||||||
sources=(
|
sources=(
|
||||||
@@ -242,6 +273,12 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
WorkflowRuntimeProxy,
|
WorkflowRuntimeProxy,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
menu_secondary.bind_links(
|
||||||
|
links=(link_setup_workflow_state_action_selection,),
|
||||||
|
sources=(
|
||||||
|
WorkflowState,
|
||||||
|
)
|
||||||
|
)
|
||||||
menu_setup.bind_links(links=(link_setup_workflow_list,))
|
menu_setup.bind_links(links=(link_setup_workflow_list,))
|
||||||
menu_sidebar.bind_links(
|
menu_sidebar.bind_links(
|
||||||
links=(
|
links=(
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
from common.classes import PropertyHelper
|
from common.classes import PropertyHelper
|
||||||
|
|
||||||
|
__all__ = ('WorkflowAction',)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DocumentStateHelper(PropertyHelper):
|
class DocumentStateHelper(PropertyHelper):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -11,3 +21,51 @@ class DocumentStateHelper(PropertyHelper):
|
|||||||
|
|
||||||
def get_result(self, name):
|
def get_result(self, name):
|
||||||
return self.instance.workflows.get(workflow__internal_name=name)
|
return self.instance.workflows.get(workflow__internal_name=name)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowActionMetaclass(type):
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
new_class = super(WorkflowActionMetaclass, mcs).__new__(
|
||||||
|
mcs, name, bases, attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
if not new_class.__module__ == __name__:
|
||||||
|
mcs._registry[
|
||||||
|
'{}.{}'.format(new_class.__module__, name)
|
||||||
|
] = new_class
|
||||||
|
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowActionBase(object):
|
||||||
|
fields = ()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAction(six.with_metaclass(WorkflowActionMetaclass, WorkflowActionBase)):
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls._registry[name]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls):
|
||||||
|
return cls._registry
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize():
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
try:
|
||||||
|
import_module('{}.workflow_actions'.format(app.name))
|
||||||
|
except ImportError as exception:
|
||||||
|
if force_text(exception) != 'No module named workflow_actions':
|
||||||
|
logger.error(
|
||||||
|
'Error importing %s workflow_actions.py file; %s',
|
||||||
|
app.name, exception
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_form_schema(self, request=None):
|
||||||
|
return {
|
||||||
|
'fields': self.fields or (),
|
||||||
|
'widgets': getattr(self, 'widgets', {})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.formsets import formset_factory
|
from django.forms.formsets import formset_factory
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .models import Workflow, WorkflowState, WorkflowTransition
|
from common.forms import DynamicModelForm
|
||||||
|
|
||||||
|
from .classes import WorkflowAction
|
||||||
|
from .models import (
|
||||||
|
Workflow, WorkflowState, WorkflowStateAction, WorkflowTransition
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowActionSelectionForm(forms.Form):
|
||||||
|
klass = forms.ChoiceField(choices=(), label=_('Action'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WorkflowActionSelectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['klass'].choices = [
|
||||||
|
(
|
||||||
|
key, klass.label
|
||||||
|
) for key, klass in WorkflowAction.get_all().items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WorkflowForm(forms.ModelForm):
|
class WorkflowForm(forms.ModelForm):
|
||||||
@@ -13,6 +33,40 @@ class WorkflowForm(forms.ModelForm):
|
|||||||
model = Workflow
|
model = Workflow
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowStateActionDynamicForm(DynamicModelForm):
|
||||||
|
class Meta:
|
||||||
|
fields = ('label', 'when', 'enabled', 'action_data')
|
||||||
|
model = WorkflowStateAction
|
||||||
|
widgets = {'action_data': forms.widgets.HiddenInput}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
result = super(
|
||||||
|
WorkflowStateActionDynamicForm, self
|
||||||
|
).__init__(*args, **kwargs)
|
||||||
|
if self.instance.action_data:
|
||||||
|
for key, value in json.loads(self.instance.action_data).items():
|
||||||
|
self.fields[key].initial = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
data = super(WorkflowStateActionDynamicForm, self).clean()
|
||||||
|
|
||||||
|
# Consolidate the dynamic fields into a single JSON field called
|
||||||
|
# 'action_data'.
|
||||||
|
action_data = {}
|
||||||
|
|
||||||
|
for field in self.schema['fields']:
|
||||||
|
action_data[field['name']] = data.pop(
|
||||||
|
field['name'], field.get('default', None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flatten the queryset to a list of ids
|
||||||
|
action_data['tags'] = list(action_data['tags'].values_list('id', flat=True))
|
||||||
|
data['action_data'] = json.dumps(action_data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class WorkflowStateForm(forms.ModelForm):
|
class WorkflowStateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('initial', 'label', 'completion')
|
fields = ('initial', 'label', 'completion')
|
||||||
|
|||||||
@@ -110,3 +110,28 @@ link_workflow_instance_transition_events = Link(
|
|||||||
text=_('Transition events'),
|
text=_('Transition events'),
|
||||||
view='document_states:setup_workflow_instance_transition_events'
|
view='document_states:setup_workflow_instance_transition_events'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
link_setup_workflow_state_action_list = Link(
|
||||||
|
args='resolved_object.pk', permissions=(permission_workflow_edit,),
|
||||||
|
text=_('Actions'),
|
||||||
|
view='document_states:setup_workflow_state_action_list',
|
||||||
|
)
|
||||||
|
|
||||||
|
link_setup_workflow_state_action_selection = Link(
|
||||||
|
args='resolved_object.pk', permissions=(permission_workflow_edit,),
|
||||||
|
text=_('Create action'),
|
||||||
|
view='document_states:setup_workflow_state_action_selection',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
link_setup_workflow_state_action_delete = Link(
|
||||||
|
args='resolved_object.pk', permissions=(permission_workflow_edit,),
|
||||||
|
tags='dangerous', text=_('Delete'),
|
||||||
|
view='document_states:setup_workflow_state_action_delete',
|
||||||
|
)
|
||||||
|
link_setup_workflow_state_action_edit = Link(
|
||||||
|
args='resolved_object.pk', permissions=(permission_workflow_edit,),
|
||||||
|
text=_('Edit'), view='document_states:setup_workflow_state_action_edit',
|
||||||
|
)
|
||||||
|
|||||||
11
mayan/apps/document_states/literals.py
Normal file
11
mayan/apps/document_states/literals.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
WORKFLOW_ACTION_ON_ENTRY = 1
|
||||||
|
WORKFLOW_ACTION_ON_EXIT = 2
|
||||||
|
|
||||||
|
WORKFLOW_ACTION_WHEN_CHOICES = (
|
||||||
|
(WORKFLOW_ACTION_ON_ENTRY, _('On entry')),
|
||||||
|
(WORKFLOW_ACTION_ON_EXIT, _('On exit')),
|
||||||
|
)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-08-07 06:12
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('document_states', '0008_auto_20170803_0752'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WorkflowStateAction',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('label', models.CharField(max_length=255, verbose_name='Label')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
|
||||||
|
('when', models.PositiveIntegerField(choices=[(1, 'On entry'), (2, 'On exit')], default=1, help_text='At which moment of the state this action will execute', verbose_name='When')),
|
||||||
|
('action_path', models.CharField(help_text='The dotted Python path to the workflow action class to execute.', max_length=128, verbose_name='Entry action path')),
|
||||||
|
('action_data', models.TextField(blank=True, verbose_name='Entry action data')),
|
||||||
|
('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='actions', to='document_states.WorkflowState', verbose_name='Workflow state')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('label',),
|
||||||
|
'verbose_name': 'Workflow state action',
|
||||||
|
'verbose_name_plural': 'Workflow state actions',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='workflowstateaction',
|
||||||
|
unique_together=set([('state', 'label')]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -8,6 +9,7 @@ from django.db import IntegrityError, models
|
|||||||
from django.db.models import F, Max, Q
|
from django.db.models import F, Max, Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
@@ -16,6 +18,10 @@ from documents.models import Document, DocumentType
|
|||||||
from events.models import EventType
|
from events.models import EventType
|
||||||
from permissions import Permission
|
from permissions import Permission
|
||||||
|
|
||||||
|
from .literals import (
|
||||||
|
WORKFLOW_ACTION_WHEN_CHOICES, WORKFLOW_ACTION_ON_ENTRY,
|
||||||
|
WORKFLOW_ACTION_ON_EXIT
|
||||||
|
)
|
||||||
from .managers import WorkflowManager
|
from .managers import WorkflowManager
|
||||||
from .permissions import permission_workflow_transition
|
from .permissions import permission_workflow_transition
|
||||||
|
|
||||||
@@ -123,6 +129,14 @@ class WorkflowState(models.Model):
|
|||||||
self.workflow.states.all().update(initial=False)
|
self.workflow.states.all().update(initial=False)
|
||||||
return super(WorkflowState, self).save(*args, **kwargs)
|
return super(WorkflowState, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entry_actions(self):
|
||||||
|
return self.actions.filter(when=WORKFLOW_ACTION_ON_ENTRY)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exit_actions(self):
|
||||||
|
return self.actions.filter(when=WORKFLOW_ACTION_ON_EXIT)
|
||||||
|
|
||||||
def get_documents(self):
|
def get_documents(self):
|
||||||
latest_entries = WorkflowInstanceLogEntry.objects.annotate(
|
latest_entries = WorkflowInstanceLogEntry.objects.annotate(
|
||||||
max_datetime=Max(
|
max_datetime=Max(
|
||||||
@@ -149,6 +163,58 @@ class WorkflowState(models.Model):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class WorkflowStateAction(models.Model):
|
||||||
|
state = models.ForeignKey(
|
||||||
|
WorkflowState, on_delete=models.CASCADE,
|
||||||
|
related_name='actions', verbose_name=_('Workflow state')
|
||||||
|
)
|
||||||
|
label = models.CharField(max_length=255, verbose_name=_('Label'))
|
||||||
|
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
|
||||||
|
when = models.PositiveIntegerField(
|
||||||
|
choices=WORKFLOW_ACTION_WHEN_CHOICES,
|
||||||
|
default=WORKFLOW_ACTION_ON_ENTRY, help_text=_(
|
||||||
|
'At which moment of the state this action will execute'
|
||||||
|
), verbose_name=_('When')
|
||||||
|
)
|
||||||
|
action_path = models.CharField(
|
||||||
|
max_length=128, help_text=_(
|
||||||
|
'The dotted Python path to the workflow action class to execute.'
|
||||||
|
), verbose_name=_('Entry action path')
|
||||||
|
)
|
||||||
|
action_data = models.TextField(
|
||||||
|
blank=True, verbose_name=_('Entry action data')
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.label
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('label',)
|
||||||
|
unique_together = ('state', 'label')
|
||||||
|
verbose_name = _('Workflow state action')
|
||||||
|
verbose_name_plural = _('Workflow state actions')
|
||||||
|
|
||||||
|
def dumps(self, data):
|
||||||
|
self.action_data = json.dumps(data)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
self.get_class_instance().execute(context=context)
|
||||||
|
|
||||||
|
def get_class(self):
|
||||||
|
return import_string(self.action_path)
|
||||||
|
|
||||||
|
def get_class_label(self):
|
||||||
|
return self.get_class().label
|
||||||
|
|
||||||
|
def get_class_instance(self):
|
||||||
|
return self.get_class()(**self.loads())
|
||||||
|
|
||||||
|
def loads(self):
|
||||||
|
return json.loads(self.action_data)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class WorkflowTransition(models.Model):
|
class WorkflowTransition(models.Model):
|
||||||
workflow = models.ForeignKey(
|
workflow = models.ForeignKey(
|
||||||
@@ -156,7 +222,6 @@ class WorkflowTransition(models.Model):
|
|||||||
verbose_name=_('Workflow')
|
verbose_name=_('Workflow')
|
||||||
)
|
)
|
||||||
label = models.CharField(max_length=255, verbose_name=_('Label'))
|
label = models.CharField(max_length=255, verbose_name=_('Label'))
|
||||||
|
|
||||||
origin_state = models.ForeignKey(
|
origin_state = models.ForeignKey(
|
||||||
WorkflowState, on_delete=models.CASCADE,
|
WorkflowState, on_delete=models.CASCADE,
|
||||||
related_name='origin_transitions', verbose_name=_('Origin state')
|
related_name='origin_transitions', verbose_name=_('Origin state')
|
||||||
@@ -341,6 +406,17 @@ class WorkflowInstanceLogEntry(models.Model):
|
|||||||
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
|
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
|
||||||
raise ValidationError(_('Not a valid transition choice.'))
|
raise ValidationError(_('Not a valid transition choice.'))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
result = super(WorkflowInstanceLogEntry, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
for action in self.transition.origin_state.exit_actions.all():
|
||||||
|
action.execute(context={'entry_log': self})
|
||||||
|
|
||||||
|
for action in self.transition.destination_state.entry_actions.all():
|
||||||
|
action.execute(context={'entry_log': self})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRuntimeProxy(Workflow):
|
class WorkflowRuntimeProxy(Workflow):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ from .views import (
|
|||||||
DocumentWorkflowInstanceListView, SetupWorkflowCreateView,
|
DocumentWorkflowInstanceListView, SetupWorkflowCreateView,
|
||||||
SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView,
|
SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView,
|
||||||
SetupWorkflowEditView, SetupWorkflowListView,
|
SetupWorkflowEditView, SetupWorkflowListView,
|
||||||
SetupWorkflowStateCreateView, SetupWorkflowStateDeleteView,
|
SetupWorkflowStateActionCreateView, SetupWorkflowStateActionDeleteView,
|
||||||
SetupWorkflowStateEditView, SetupWorkflowStateListView,
|
SetupWorkflowStateActionEditView, SetupWorkflowStateActionListView,
|
||||||
SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView,
|
SetupWorkflowStateActionSelectionView, SetupWorkflowStateCreateView,
|
||||||
SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView,
|
SetupWorkflowStateDeleteView, SetupWorkflowStateEditView,
|
||||||
|
SetupWorkflowStateListView, SetupWorkflowTransitionListView,
|
||||||
|
SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView,
|
||||||
|
SetupWorkflowTransitionEditView,
|
||||||
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
|
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
|
||||||
WorkflowDocumentListView, WorkflowInstanceDetailView,
|
WorkflowDocumentListView, WorkflowInstanceDetailView,
|
||||||
WorkflowInstanceTransitionView, WorkflowListView,
|
WorkflowInstanceTransitionView, WorkflowListView,
|
||||||
@@ -98,6 +101,39 @@ urlpatterns = [
|
|||||||
SetupWorkflowStateEditView.as_view(),
|
SetupWorkflowStateEditView.as_view(),
|
||||||
name='setup_workflow_state_edit'
|
name='setup_workflow_state_edit'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/(?P<pk>\d+)/actions/$',
|
||||||
|
SetupWorkflowStateActionListView.as_view(),
|
||||||
|
name='setup_workflow_state_action_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/(?P<pk>\d+)/actions/$',
|
||||||
|
SetupWorkflowStateActionListView.as_view(),
|
||||||
|
name='setup_workflow_state_action_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/(?P<pk>\d+)/actions/selection/$',
|
||||||
|
SetupWorkflowStateActionSelectionView.as_view(),
|
||||||
|
name='setup_workflow_state_action_selection'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/(?P<pk>\d+)/actions/(?P<class_path>[a-zA-Z0-9_.]+)/create/$',
|
||||||
|
SetupWorkflowStateActionCreateView.as_view(),
|
||||||
|
name='setup_workflow_state_action_create'
|
||||||
|
),
|
||||||
|
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/actions/(?P<pk>\d+)/delete/$',
|
||||||
|
SetupWorkflowStateActionDeleteView.as_view(),
|
||||||
|
name='setup_workflow_state_action_delete'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^setup/workflow/state/actions/(?P<pk>\d+)/edit/$',
|
||||||
|
SetupWorkflowStateActionEditView.as_view(),
|
||||||
|
name='setup_workflow_state_action_edit'
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
url(
|
url(
|
||||||
r'^setup/workflow/transitions/(?P<pk>\d+)/delete/$',
|
r'^setup/workflow/transitions/(?P<pk>\d+)/delete/$',
|
||||||
SetupWorkflowTransitionDeleteView.as_view(),
|
SetupWorkflowTransitionDeleteView.as_view(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -10,20 +10,24 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from common.views import (
|
from common.views import (
|
||||||
AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView,
|
AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView,
|
||||||
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
|
SingleObjectDeleteView, SingleObjectDynamicFormCreateView,
|
||||||
|
SingleObjectDynamicFormEditView, SingleObjectEditView,
|
||||||
|
SingleObjectListView
|
||||||
)
|
)
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.views import DocumentListView
|
from documents.views import DocumentListView
|
||||||
from events.classes import Event
|
from events.classes import Event
|
||||||
from events.models import EventType
|
from events.models import EventType
|
||||||
|
|
||||||
|
from .classes import WorkflowAction
|
||||||
from .forms import (
|
from .forms import (
|
||||||
WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm,
|
WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm,
|
||||||
WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet
|
WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm,
|
||||||
|
WorkflowTransitionTriggerEventRelationshipFormSet
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
Workflow, WorkflowInstance, WorkflowState, WorkflowTransition,
|
Workflow, WorkflowInstance, WorkflowState, WorkflowStateAction,
|
||||||
WorkflowRuntimeProxy, WorkflowStateRuntimeProxy,
|
WorkflowTransition, WorkflowRuntimeProxy, WorkflowStateRuntimeProxy,
|
||||||
)
|
)
|
||||||
from .permissions import (
|
from .permissions import (
|
||||||
permission_workflow_create, permission_workflow_delete,
|
permission_workflow_create, permission_workflow_delete,
|
||||||
@@ -223,6 +227,142 @@ class SetupWorkflowStateListView(SingleObjectListView):
|
|||||||
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
|
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorkflowStateActionCreateView(SingleObjectDynamicFormCreateView):
|
||||||
|
form_class = WorkflowStateActionDynamicForm
|
||||||
|
object_permission = permission_workflow_edit
|
||||||
|
|
||||||
|
def get_class(self):
|
||||||
|
try:
|
||||||
|
return WorkflowAction.get(name=self.kwargs['class_path'])
|
||||||
|
except KeyError:
|
||||||
|
raise Http404(
|
||||||
|
'{} class not found'.format(self.kwargs['class_path'])
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'navigation_object_list': ('object', 'workflow'),
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _(
|
||||||
|
'Create a "%s" workflow action'
|
||||||
|
) % self.get_class().label,
|
||||||
|
'workflow': self.get_object().workflow
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_schema(self):
|
||||||
|
return self.get_class()().get_form_schema(request=self.request)
|
||||||
|
|
||||||
|
def get_instance_extra_data(self):
|
||||||
|
return {
|
||||||
|
'action_path': self.kwargs['class_path'],
|
||||||
|
'state': self.get_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return get_object_or_404(WorkflowState, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
def get_post_action_redirect(self):
|
||||||
|
return reverse(
|
||||||
|
'document_states:setup_workflow_state_action_list',
|
||||||
|
args=(self.get_object().pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorkflowStateActionDeleteView(SingleObjectDeleteView):
|
||||||
|
model = WorkflowStateAction
|
||||||
|
object_permission = permission_workflow_edit
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'navigation_object_list': (
|
||||||
|
'object', 'workflow_state', 'workflow'
|
||||||
|
),
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _('Delete workflow state action: %s') % self.get_object(),
|
||||||
|
'workflow': self.get_object().state.workflow,
|
||||||
|
'workflow_state': self.get_object().state,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_post_action_redirect(self):
|
||||||
|
return reverse(
|
||||||
|
'document_states:setup_workflow_state_action_list',
|
||||||
|
args=(self.get_object().state.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorkflowStateActionEditView(SingleObjectDynamicFormEditView):
|
||||||
|
form_class = WorkflowStateActionDynamicForm
|
||||||
|
model = WorkflowStateAction
|
||||||
|
object_permission = permission_workflow_edit
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'navigation_object_list': (
|
||||||
|
'object', 'workflow_state', 'workflow'
|
||||||
|
),
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _('Edit workflow state action: %s') % self.get_object(),
|
||||||
|
'workflow': self.get_object().state.workflow,
|
||||||
|
'workflow_state': self.get_object().state,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_schema(self):
|
||||||
|
return self.get_object().get_class_instance().get_form_schema(
|
||||||
|
request=self.request
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorkflowStateActionListView(SingleObjectListView):
|
||||||
|
object_permission = permission_workflow_edit
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'hide_object': True,
|
||||||
|
'navigation_object_list': ('object', 'workflow'),
|
||||||
|
'object': self.get_workflow_state(),
|
||||||
|
'title': _(
|
||||||
|
'Actions for workflow state: %s'
|
||||||
|
) % self.get_workflow_state(),
|
||||||
|
'workflow': self.get_workflow_state().workflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_schema(self):
|
||||||
|
return {'fields': self.get_class().fields}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_workflow_state().actions.all()
|
||||||
|
|
||||||
|
def get_workflow_state(self):
|
||||||
|
return get_object_or_404(WorkflowState, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWorkflowStateActionSelectionView(FormView):
|
||||||
|
form_class = WorkflowActionSelectionForm
|
||||||
|
#TODO: access check via workflow edit perm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
klass = form.cleaned_data['klass']
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse(
|
||||||
|
'document_states:setup_workflow_state_action_create',
|
||||||
|
args=(self.get_object().pk, klass,),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'navigation_object_list': (
|
||||||
|
'object', 'workflow'
|
||||||
|
),
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _('New workflow state action selection'),
|
||||||
|
'workflow': self.get_object().workflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return get_object_or_404(WorkflowState, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
|
||||||
class SetupWorkflowStateCreateView(SingleObjectCreateView):
|
class SetupWorkflowStateCreateView(SingleObjectCreateView):
|
||||||
form_class = WorkflowStateForm
|
form_class = WorkflowStateForm
|
||||||
view_permission = permission_workflow_edit
|
view_permission = permission_workflow_edit
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class Tag(models.Model):
|
|||||||
verbose_name = _('Tag')
|
verbose_name = _('Tag')
|
||||||
verbose_name_plural = _('Tags')
|
verbose_name_plural = _('Tags')
|
||||||
|
|
||||||
|
def attach_to(self, document):
|
||||||
|
self.documents.add(document)
|
||||||
|
|
||||||
def get_document_count(self, user):
|
def get_document_count(self, user):
|
||||||
queryset = AccessControlList.objects.filter_by_access(
|
queryset = AccessControlList.objects.filter_by_access(
|
||||||
permission_document_view, user, queryset=self.documents
|
permission_document_view, user, queryset=self.documents
|
||||||
@@ -40,6 +43,9 @@ class Tag(models.Model):
|
|||||||
|
|
||||||
return queryset.count()
|
return queryset.count()
|
||||||
|
|
||||||
|
def remove_from(self, document):
|
||||||
|
self.documents.remove(document)
|
||||||
|
|
||||||
|
|
||||||
class DocumentTag(Tag):
|
class DocumentTag(Tag):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class TagAttachActionView(MultipleObjectFormActionView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tag.documents.add(instance)
|
tag.attach_to(instance)
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
@@ -299,7 +299,7 @@ class TagRemoveActionView(MultipleObjectFormActionView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tag.documents.remove(instance)
|
tag.remove_from(instance)
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
|
|||||||
62
mayan/apps/tags/workflow_actions.py
Normal file
62
mayan/apps/tags/workflow_actions.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from acls.models import AccessControlList
|
||||||
|
from document_states.classes import WorkflowAction
|
||||||
|
from tags.models import Tag
|
||||||
|
from tags.permissions import permission_tag_view
|
||||||
|
|
||||||
|
__all__ = ('AttachTagAction',)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AttachTagAction(WorkflowAction):
|
||||||
|
fields = (
|
||||||
|
{
|
||||||
|
'name': 'tags', 'label': _('Tags'),
|
||||||
|
'class': 'django.forms.ModelMultipleChoiceField', 'kwargs': {
|
||||||
|
'help_text': _('Tags to attach to the document'),
|
||||||
|
'queryset': Tag.objects.none(), 'required': False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
label = _('Attach tag')
|
||||||
|
widgets = {
|
||||||
|
'tags': {
|
||||||
|
'class': 'tags.widgets.TagFormWidget', 'kwargs': {
|
||||||
|
'attrs': {'class': 'select2-tags'},
|
||||||
|
'queryset': Tag.objects.none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, tags=None):
|
||||||
|
if tags:
|
||||||
|
self.tags = Tag.objects.filter(pk__in=tags)
|
||||||
|
else:
|
||||||
|
self.tags = Tag.objects.none()
|
||||||
|
|
||||||
|
def get_form_schema(self, request):
|
||||||
|
user = request.user
|
||||||
|
logger.debug('user: %s', user)
|
||||||
|
|
||||||
|
queryset = AccessControlList.objects.filter_by_access(
|
||||||
|
permission_tag_view, user, queryset=Tag.objects.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields[0]['kwargs']['queryset'] = queryset
|
||||||
|
self.widgets['tags']['kwargs']['queryset'] = queryset
|
||||||
|
|
||||||
|
return {
|
||||||
|
'fields': self.fields,
|
||||||
|
'widgets': self.widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for tag in self.tags:
|
||||||
|
tag.attach_to(
|
||||||
|
document=context['entry_log'].workflow_instance.document
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user