From 28a1edbc72aa47947dcc31b8e6ff7d1826f9c1cf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 7 Aug 2017 05:23:02 -0400 Subject: [PATCH] Initial commit to support workflow actions. Signed-off-by: Roberto Rosario --- mayan/apps/document_states/apps.py | 45 +++++- mayan/apps/document_states/classes.py | 58 +++++++ mayan/apps/document_states/forms.py | 56 ++++++- mayan/apps/document_states/links.py | 25 +++ mayan/apps/document_states/literals.py | 11 ++ .../migrations/0009_auto_20170807_0612.py | 37 +++++ mayan/apps/document_states/models.py | 78 ++++++++- mayan/apps/document_states/urls.py | 44 ++++- mayan/apps/document_states/views.py | 152 +++++++++++++++++- mayan/apps/tags/models.py | 6 + mayan/apps/tags/views.py | 4 +- mayan/apps/tags/workflow_actions.py | 62 +++++++ 12 files changed, 560 insertions(+), 18 deletions(-) create mode 100644 mayan/apps/document_states/literals.py create mode 100644 mayan/apps/document_states/migrations/0009_auto_20170807_0612.py create mode 100644 mayan/apps/tags/workflow_actions.py diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index c72166a036..043e51bf40 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -18,7 +18,7 @@ from mayan.celery import app from navigation import SourceColumn from rest_api.classes import APIEndPoint -from .classes import DocumentStateHelper +from .classes import DocumentStateHelper, WorkflowAction from .handlers import ( 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_setup_workflow_create, link_setup_workflow_delete, link_setup_workflow_edit, link_setup_workflow_list, - link_setup_workflow_states, 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_states, link_setup_workflow_state_action_delete, + link_setup_workflow_state_action_edit, + 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_tool_launch_all_workflows, link_workflow_instance_detail, link_workflow_instance_transition, link_workflow_document_list, @@ -67,9 +71,12 @@ class DocumentStatesApp(MayanAppConfig): WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry') WorkflowRuntimeProxy = self.get_model('WorkflowRuntimeProxy') WorkflowState = self.get_model('WorkflowState') + WorkflowStateAction = self.get_model('WorkflowStateAction') WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy') WorkflowTransition = self.get_model('WorkflowTransition') + WorkflowAction.initialize() + ModelAttribute( Document, 'workflow.< workflow internal name >.get_current_state', label=_('Current state of a workflow'), description=_( @@ -157,6 +164,22 @@ class DocumentStatesApp(MayanAppConfig): 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( source=WorkflowTransition, label=_('Origin state'), attribute='origin_state' @@ -203,6 +226,7 @@ class DocumentStatesApp(MayanAppConfig): menu_object.bind_links( links=( link_setup_workflow_state_edit, + link_setup_workflow_state_action_list, link_setup_workflow_state_delete ), sources=(WorkflowState,) ) @@ -229,6 +253,13 @@ class DocumentStatesApp(MayanAppConfig): link_workflow_state_document_list, ), 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( links=(link_setup_workflow_list, link_setup_workflow_create), sources=( @@ -242,6 +273,12 @@ class DocumentStatesApp(MayanAppConfig): WorkflowRuntimeProxy, ) ) + menu_secondary.bind_links( + links=(link_setup_workflow_state_action_selection,), + sources=( + WorkflowState, + ) + ) menu_setup.bind_links(links=(link_setup_workflow_list,)) menu_sidebar.bind_links( links=( diff --git a/mayan/apps/document_states/classes.py b/mayan/apps/document_states/classes.py index cba92b76e0..cdf1cb37e1 100644 --- a/mayan/apps/document_states/classes.py +++ b/mayan/apps/document_states/classes.py @@ -1,7 +1,17 @@ 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 +__all__ = ('WorkflowAction',) +logger = logging.getLogger(__name__) + class DocumentStateHelper(PropertyHelper): @staticmethod @@ -11,3 +21,51 @@ class DocumentStateHelper(PropertyHelper): def get_result(self, 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', {}) + } diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index 776fbe60d6..1117bd1e81 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -1,10 +1,30 @@ from __future__ import absolute_import, unicode_literals +import json + from django import forms from django.forms.formsets import formset_factory 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): @@ -13,6 +33,40 @@ class WorkflowForm(forms.ModelForm): 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 Meta: fields = ('initial', 'label', 'completion') diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index d9564255ca..55bf9112ad 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -110,3 +110,28 @@ link_workflow_instance_transition_events = Link( text=_('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', +) diff --git a/mayan/apps/document_states/literals.py b/mayan/apps/document_states/literals.py new file mode 100644 index 0000000000..79fc51f828 --- /dev/null +++ b/mayan/apps/document_states/literals.py @@ -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')), +) diff --git a/mayan/apps/document_states/migrations/0009_auto_20170807_0612.py b/mayan/apps/document_states/migrations/0009_auto_20170807_0612.py new file mode 100644 index 0000000000..e58aeea9b9 --- /dev/null +++ b/mayan/apps/document_states/migrations/0009_auto_20170807_0612.py @@ -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')]), + ), + ] diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index aec7620a5d..b5b5ffc2df 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import json import logging 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.urls import reverse 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 acls.models import AccessControlList @@ -16,6 +18,10 @@ from documents.models import Document, DocumentType from events.models import EventType from permissions import Permission +from .literals import ( + WORKFLOW_ACTION_WHEN_CHOICES, WORKFLOW_ACTION_ON_ENTRY, + WORKFLOW_ACTION_ON_EXIT +) from .managers import WorkflowManager from .permissions import permission_workflow_transition @@ -123,6 +129,14 @@ class WorkflowState(models.Model): self.workflow.states.all().update(initial=False) 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): latest_entries = WorkflowInstanceLogEntry.objects.annotate( max_datetime=Max( @@ -149,6 +163,58 @@ class WorkflowState(models.Model): ).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 class WorkflowTransition(models.Model): workflow = models.ForeignKey( @@ -156,7 +222,6 @@ class WorkflowTransition(models.Model): verbose_name=_('Workflow') ) label = models.CharField(max_length=255, verbose_name=_('Label')) - origin_state = models.ForeignKey( WorkflowState, on_delete=models.CASCADE, 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): 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 Meta: diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index a83ad4b754..5f45b797e4 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -13,10 +13,13 @@ from .views import ( DocumentWorkflowInstanceListView, SetupWorkflowCreateView, SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, SetupWorkflowEditView, SetupWorkflowListView, - SetupWorkflowStateCreateView, SetupWorkflowStateDeleteView, - SetupWorkflowStateEditView, SetupWorkflowStateListView, - SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView, - SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView, + SetupWorkflowStateActionCreateView, SetupWorkflowStateActionDeleteView, + SetupWorkflowStateActionEditView, SetupWorkflowStateActionListView, + SetupWorkflowStateActionSelectionView, SetupWorkflowStateCreateView, + SetupWorkflowStateDeleteView, SetupWorkflowStateEditView, + SetupWorkflowStateListView, SetupWorkflowTransitionListView, + SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView, + SetupWorkflowTransitionEditView, SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows, WorkflowDocumentListView, WorkflowInstanceDetailView, WorkflowInstanceTransitionView, WorkflowListView, @@ -98,6 +101,39 @@ urlpatterns = [ SetupWorkflowStateEditView.as_view(), name='setup_workflow_state_edit' ), + url( + r'^setup/workflow/state/(?P\d+)/actions/$', + SetupWorkflowStateActionListView.as_view(), + name='setup_workflow_state_action_list' + ), + url( + r'^setup/workflow/state/(?P\d+)/actions/$', + SetupWorkflowStateActionListView.as_view(), + name='setup_workflow_state_action_list' + ), + url( + r'^setup/workflow/state/(?P\d+)/actions/selection/$', + SetupWorkflowStateActionSelectionView.as_view(), + name='setup_workflow_state_action_selection' + ), + url( + r'^setup/workflow/state/(?P\d+)/actions/(?P[a-zA-Z0-9_.]+)/create/$', + SetupWorkflowStateActionCreateView.as_view(), + name='setup_workflow_state_action_create' + ), + + url( + r'^setup/workflow/state/actions/(?P\d+)/delete/$', + SetupWorkflowStateActionDeleteView.as_view(), + name='setup_workflow_state_action_delete' + ), + url( + r'^setup/workflow/state/actions/(?P\d+)/edit/$', + SetupWorkflowStateActionEditView.as_view(), + name='setup_workflow_state_action_edit' + ), + + url( r'^setup/workflow/transitions/(?P\d+)/delete/$', SetupWorkflowTransitionDeleteView.as_view(), diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index b3d9da9b34..2d43dfa82b 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages 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.urls import reverse, reverse_lazy 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 common.views import ( AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView, - SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView + SingleObjectDeleteView, SingleObjectDynamicFormCreateView, + SingleObjectDynamicFormEditView, SingleObjectEditView, + SingleObjectListView ) from documents.models import Document from documents.views import DocumentListView from events.classes import Event from events.models import EventType +from .classes import WorkflowAction from .forms import ( - WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm, - WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet + WorkflowActionSelectionForm, WorkflowForm, WorkflowInstanceTransitionForm, + WorkflowStateActionDynamicForm, WorkflowStateForm, WorkflowTransitionForm, + WorkflowTransitionTriggerEventRelationshipFormSet ) from .models import ( - Workflow, WorkflowInstance, WorkflowState, WorkflowTransition, - WorkflowRuntimeProxy, WorkflowStateRuntimeProxy, + Workflow, WorkflowInstance, WorkflowState, WorkflowStateAction, + WorkflowTransition, WorkflowRuntimeProxy, WorkflowStateRuntimeProxy, ) from .permissions import ( permission_workflow_create, permission_workflow_delete, @@ -223,6 +227,142 @@ class SetupWorkflowStateListView(SingleObjectListView): 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): form_class = WorkflowStateForm view_permission = permission_workflow_edit diff --git a/mayan/apps/tags/models.py b/mayan/apps/tags/models.py index 7572d9448c..67534c0aad 100644 --- a/mayan/apps/tags/models.py +++ b/mayan/apps/tags/models.py @@ -33,6 +33,9 @@ class Tag(models.Model): verbose_name = _('Tag') verbose_name_plural = _('Tags') + def attach_to(self, document): + self.documents.add(document) + def get_document_count(self, user): queryset = AccessControlList.objects.filter_by_access( permission_document_view, user, queryset=self.documents @@ -40,6 +43,9 @@ class Tag(models.Model): return queryset.count() + def remove_from(self, document): + self.documents.remove(document) + class DocumentTag(Tag): class Meta: diff --git a/mayan/apps/tags/views.py b/mayan/apps/tags/views.py index b7b9b101d0..fded552a91 100644 --- a/mayan/apps/tags/views.py +++ b/mayan/apps/tags/views.py @@ -94,7 +94,7 @@ class TagAttachActionView(MultipleObjectFormActionView): } ) else: - tag.documents.add(instance) + tag.attach_to(instance) messages.success( self.request, _( @@ -299,7 +299,7 @@ class TagRemoveActionView(MultipleObjectFormActionView): } ) else: - tag.documents.remove(instance) + tag.remove_from(instance) messages.success( self.request, _( diff --git a/mayan/apps/tags/workflow_actions.py b/mayan/apps/tags/workflow_actions.py new file mode 100644 index 0000000000..af10688056 --- /dev/null +++ b/mayan/apps/tags/workflow_actions.py @@ -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 + )