Add support for workflow transition triggers.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-08-03 04:01:11 -04:00
parent 4560009927
commit 81f481fadf
14 changed files with 359 additions and 22 deletions

View File

@@ -19,7 +19,9 @@ from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .classes import DocumentStateHelper
from .handlers import handler_index_document, launch_workflow
from .handlers import (
handler_index_document, handler_trigger_transition, launch_workflow
)
from .links import (
link_document_workflow_instance_list, link_setup_workflow_document_types,
link_setup_workflow_create, link_setup_workflow_delete,
@@ -31,10 +33,11 @@ from .links import (
link_tool_launch_all_workflows, link_workflow_instance_detail,
link_workflow_instance_transition, link_workflow_document_list,
link_workflow_list, link_workflow_state_document_list,
link_workflow_state_list
link_workflow_state_list, link_workflow_instance_transition_events
)
from .permissions import permission_workflow_transition
from .queues import * # NOQA
from .widgets import widget_transition_events
class DocumentStatesApp(MayanAppConfig):
@@ -48,6 +51,9 @@ class DocumentStatesApp(MayanAppConfig):
APIEndPoint(app=self, version_string='1')
Action = apps.get_model(
app_label='actstream', model_name='Action'
)
Document = apps.get_model(
app_label='documents', model_name='Document'
)
@@ -159,6 +165,12 @@ class DocumentStatesApp(MayanAppConfig):
source=WorkflowTransition, label=_('Destination state'),
attribute='destination_state'
)
SourceColumn(
source=WorkflowTransition, label=_('Triggers'),
func=lambda context: widget_transition_events(
transition=context['object']
)
)
app.conf.CELERY_QUEUES.extend(
(
@@ -196,7 +208,8 @@ class DocumentStatesApp(MayanAppConfig):
)
menu_object.bind_links(
links=(
link_setup_workflow_transition_edit, link_acl_list,
link_setup_workflow_transition_edit,
link_workflow_instance_transition_events, link_acl_list,
link_setup_workflow_transition_delete
), sources=(WorkflowTransition,)
)
@@ -249,3 +262,8 @@ class DocumentStatesApp(MayanAppConfig):
dispatch_uid='handler_index_document_save',
sender=WorkflowInstanceLogEntry
)
post_save.connect(
handler_trigger_transition,
dispatch_uid='document_states_handler_trigger_transition',
sender=Action
)

View File

@@ -1,6 +1,7 @@
from __future__ import absolute_import, unicode_literals
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
@@ -39,6 +40,54 @@ class WorkflowTransitionForm(forms.ModelForm):
model = WorkflowTransition
class WorkflowTransitionTriggerEventRelationshipForm(forms.Form):
label = forms.CharField(
label=_('Label'), required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
relationship = forms.ChoiceField(
label=_('Enabled'),
widget=forms.RadioSelect(), choices=(
('no', _('No')),
('yes', _('Yes')),
)
)
def __init__(self, *args, **kwargs):
super(WorkflowTransitionTriggerEventRelationshipForm, self).__init__(
*args, **kwargs
)
self.fields['label'].initial = self.initial['event_type'].label
relationship = self.initial['transition'].trigger_events.filter(
event_type=self.initial['event_type'],
)
if relationship.exists():
self.fields['relationship'].initial = 'yes'
else:
self.fields['relationship'].initial = 'no'
def save(self):
relationship = self.initial['transition'].trigger_events.filter(
event_type=self.initial['event_type'],
)
if self.cleaned_data['relationship'] == 'no':
relationship.delete()
elif self.cleaned_data['relationship'] == 'yes':
if not relationship.exists():
self.initial['transition'].trigger_events.create(
event_type=self.initial['event_type'],
)
WorkflowTransitionTriggerEventRelationshipFormSet = formset_factory(
WorkflowTransitionTriggerEventRelationshipForm, extra=0
)
class WorkflowInstanceTransitionForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')

View File

@@ -1,8 +1,36 @@
from __future__ import unicode_literals
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from document_indexing.tasks import task_index_document
from events.classes import Event
def handler_index_document(sender, **kwargs):
task_index_document.apply_async(
kwargs=dict(
document_id=kwargs['instance'].workflow_instance.document.pk
)
)
def handler_trigger_transition(sender, **kwargs):
action = kwargs['instance']
WorkflowInstance = apps.get_model(
app_label='document_states', model_name='WorkflowInstance'
)
WorkflowTransition = apps.get_model(
app_label='document_states', model_name='WorkflowTransition'
)
for transition in WorkflowTransition.objects.filter(trigger_events__event_type__name=kwargs['instance'].verb):
for workflow_instance in WorkflowInstance.objects.filter(workflow__transitions=transition, document=action.target):
workflow_instance.do_transition(
comment=_('Event trigger: %s') % Event.get(name=action.verb).label,
transition=transition
)
def launch_workflow(sender, instance, created, **kwargs):
@@ -12,11 +40,3 @@ def launch_workflow(sender, instance, created, **kwargs):
if created:
Workflow.objects.launch_for(instance)
def handler_index_document(sender, **kwargs):
task_index_document.apply_async(
kwargs=dict(
document_id=kwargs['instance'].workflow_instance.document.pk
)
)

View File

@@ -105,3 +105,8 @@ link_workflow_state_list = Link(
text=_('States'), view='document_states:workflow_state_list',
args='resolved_object.pk'
)
link_workflow_instance_transition_events = Link(
args='resolved_object.pk', permissions=(permission_workflow_edit,),
text=_('Transition events'),
view='document_states:setup_workflow_instance_transition_events'
)

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-08-03 06:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0001_initial'),
('document_states', '0004_workflow_internal_name'),
]
operations = [
migrations.CreateModel(
name='WorkflowTransitionTriggerEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='events.EventType', verbose_name='Event type')),
],
options={
'verbose_name': 'Workflow transition trigger event',
'verbose_name_plural': 'Workflow transitions trigger events',
},
),
migrations.AddField(
model_name='workflowtransition',
name='trigger_time_period',
field=models.PositiveIntegerField(blank=True, help_text='Amount of time after which this transition will trigger on its own.', null=True, verbose_name='Trigger time period'),
),
migrations.AddField(
model_name='workflowtransition',
name='trigger_time_unit',
field=models.CharField(blank=True, choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes')], max_length=8, null=True, verbose_name='Trigger time unit'),
),
migrations.AddField(
model_name='workflowtransitiontriggerevent',
name='transition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='document_states.WorkflowTransition', verbose_name='Transition'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-08-03 06:51
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('document_states', '0005_auto_20170803_0638'),
]
operations = [
migrations.RenameField(
model_name='workflowtransitiontriggerevent',
old_name='stored_event_type',
new_name='event_type',
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-08-03 07:28
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('document_states', '0006_auto_20170803_0651'),
]
operations = [
migrations.AlterField(
model_name='workflowinstancelogentry',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterField(
model_name='workflowtransitiontriggerevent',
name='event_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.EventType', verbose_name='Event type'),
),
migrations.AlterField(
model_name='workflowtransitiontriggerevent',
name='transition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='document_states.WorkflowTransition', verbose_name='Transition'),
),
]

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-08-03 07:52
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('document_states', '0007_auto_20170803_0728'),
]
operations = [
migrations.RemoveField(
model_name='workflowtransition',
name='trigger_time_period',
),
migrations.RemoveField(
model_name='workflowtransition',
name='trigger_time_unit',
),
]

View File

@@ -13,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from common.validators import validate_internal_name
from documents.models import Document, DocumentType
from events.models import EventType
from permissions import Permission
from .managers import WorkflowManager
@@ -178,6 +179,24 @@ class WorkflowTransition(models.Model):
verbose_name_plural = _('Workflow transitions')
@python_2_unicode_compatible
class WorkflowTransitionTriggerEvent(models.Model):
transition = models.ForeignKey(
WorkflowTransition, on_delete=models.CASCADE,
related_name='trigger_events', verbose_name=_('Transition')
)
event_type = models.ForeignKey(
EventType, on_delete=models.CASCADE, verbose_name=_('Event type')
)
class Meta:
verbose_name = _('Workflow transition trigger event')
verbose_name_plural = _('Workflow transitions trigger events')
def __str__(self):
return force_text(self.transition)
@python_2_unicode_compatible
class WorkflowInstance(models.Model):
workflow = models.ForeignKey(
@@ -197,7 +216,7 @@ class WorkflowInstance(models.Model):
'document_states:workflow_instance_detail', args=(str(self.pk),)
)
def do_transition(self, transition, user, comment=None):
def do_transition(self, transition, user=None, comment=None):
try:
if transition in self.get_current_state().origin_transitions.all():
self.log_entries.create(
@@ -306,8 +325,8 @@ class WorkflowInstanceLogEntry(models.Model):
verbose_name=_('Transition')
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
verbose_name=_('User')
settings.AUTH_USER_MODEL, blank=True, null=True,
on_delete=models.CASCADE, verbose_name=_('User')
)
comment = models.TextField(blank=True, verbose_name=_('Comment'))

View File

@@ -17,9 +17,10 @@ from .views import (
SetupWorkflowStateEditView, SetupWorkflowStateListView,
SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView,
SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView,
ToolLaunchAllWorkflows, WorkflowDocumentListView,
WorkflowInstanceDetailView, WorkflowInstanceTransitionView,
WorkflowListView, WorkflowStateDocumentListView, WorkflowStateListView
SetupWorkflowTransitionTriggerEventListView, ToolLaunchAllWorkflows,
WorkflowDocumentListView, WorkflowInstanceDetailView,
WorkflowInstanceTransitionView, WorkflowListView,
WorkflowStateDocumentListView, WorkflowStateListView,
)
urlpatterns = [
@@ -37,7 +38,6 @@ urlpatterns = [
WorkflowInstanceTransitionView.as_view(),
name='workflow_instance_transition'
),
url(
r'^setup/all/$', SetupWorkflowListView.as_view(),
name='setup_workflow_list'
@@ -83,6 +83,11 @@ urlpatterns = [
SetupWorkflowTransitionCreateView.as_view(),
name='setup_workflow_transition_create'
),
url(
r'^setup/(?P<pk>\d+)/transitions/events/$',
SetupWorkflowTransitionTriggerEventListView.as_view(),
name='setup_workflow_instance_transition_events'
),
url(
r'^setup/workflow/state/(?P<pk>\d+)/delete/$',
SetupWorkflowStateDeleteView.as_view(),

View File

@@ -14,14 +14,16 @@ from common.views import (
)
from documents.models import Document
from documents.views import DocumentListView
from events.classes import Event
from events.models import EventType
from .forms import (
WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm,
WorkflowTransitionForm
WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet
)
from .models import (
Workflow, WorkflowInstance, WorkflowState, WorkflowTransition,
WorkflowRuntimeProxy, WorkflowStateRuntimeProxy
WorkflowRuntimeProxy, WorkflowStateRuntimeProxy,
)
from .permissions import (
permission_workflow_create, permission_workflow_delete,
@@ -507,6 +509,79 @@ class WorkflowStateListView(SingleObjectListView):
return get_object_or_404(WorkflowRuntimeProxy, pk=self.kwargs['pk'])
class SetupWorkflowTransitionTriggerEventListView(FormView):
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
submodel = EventType
def dispatch(self, *args, **kwargs):
AccessControlList.objects.check_access(
permissions=permission_workflow_edit,
user=self.request.user, obj=self.get_object().workflow
)
Event.refresh()
return super(
SetupWorkflowTransitionTriggerEventListView, self
).dispatch(*args, **kwargs)
def form_valid(self, form):
try:
for instance in form:
instance.save()
except Exception as exception:
messages.error(
self.request,
_(
'Error updating workflow transition trigger events; %s'
) % exception
)
else:
messages.success(
self.request, _(
'Workflow transition trigger events updated successfully'
)
)
return super(
SetupWorkflowTransitionTriggerEventListView, self
).form_valid(form=form)
def get_object(self):
return get_object_or_404(WorkflowTransition, pk=self.kwargs['pk'])
def get_extra_context(self):
return {
'form_display_mode_table': True,
'navigation_object_list': ('object', 'workflow'),
'object': self.get_object(),
'title': _(
'Workflow transition trigger events for: %s'
) % self.get_object(),
'workflow': self.get_object().workflow,
}
def get_initial(self):
obj = self.get_object()
initial = []
# Return the queryset by name from the sorted list of the class
event_type_ids = [event_type.name for event_type in Event.all()]
event_type_queryset = EventType.objects.filter(name__in=event_type_ids)
for event_type in event_type_queryset:
initial.append({
'transition': obj,
'event_type': event_type,
})
return initial
def get_post_action_redirect(self):
return reverse(
'document_states:setup_workflow_transitions',
args=(self.get_object().workflow.pk,)
)
class ToolLaunchAllWorkflows(ConfirmView):
extra_context = {
'title': _('Launch all workflows?')

View File

@@ -0,0 +1,13 @@
from __future__ import unicode_literals
from django.utils.html import format_html_join
def widget_transition_events(transition):
return format_html_join(
sep='\n', format_string='<div class="">{}</div>', args_generator=(
(
transition_trigger.event_type.label,
) for transition_trigger in transition.trigger_events.all()
)
)

View File

@@ -12,7 +12,7 @@ class Event(object):
@classmethod
def all(cls):
return cls._registry.values()
return Event.sort(event_type_list=cls._registry.values())
@classmethod
def get(cls, name):
@@ -30,6 +30,17 @@ class Event(object):
except KeyError as exception:
return force_text(exception)
@classmethod
def refresh(cls):
for event_type in cls.all():
event_type.get_type()
@staticmethod
def sort(event_type_list):
return sorted(
event_type_list, key=lambda x: x.label
)
def __init__(self, name, label):
self.name = name
self.label = label

View File

@@ -21,4 +21,8 @@ class EventType(models.Model):
return self.get_class().label
def get_class(self):
return Event.get_label(self.name)
return Event.get(name=self.name)
@property
def label(self):
return self.get_class().label