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 rest_api.classes import APIEndPoint
from .classes import DocumentStateHelper 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 ( 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,
@@ -31,10 +33,11 @@ from .links import (
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,
link_workflow_list, link_workflow_state_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 .permissions import permission_workflow_transition
from .queues import * # NOQA from .queues import * # NOQA
from .widgets import widget_transition_events
class DocumentStatesApp(MayanAppConfig): class DocumentStatesApp(MayanAppConfig):
@@ -48,6 +51,9 @@ class DocumentStatesApp(MayanAppConfig):
APIEndPoint(app=self, version_string='1') APIEndPoint(app=self, version_string='1')
Action = apps.get_model(
app_label='actstream', model_name='Action'
)
Document = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
) )
@@ -159,6 +165,12 @@ class DocumentStatesApp(MayanAppConfig):
source=WorkflowTransition, label=_('Destination state'), source=WorkflowTransition, label=_('Destination state'),
attribute='destination_state' attribute='destination_state'
) )
SourceColumn(
source=WorkflowTransition, label=_('Triggers'),
func=lambda context: widget_transition_events(
transition=context['object']
)
)
app.conf.CELERY_QUEUES.extend( app.conf.CELERY_QUEUES.extend(
( (
@@ -196,7 +208,8 @@ class DocumentStatesApp(MayanAppConfig):
) )
menu_object.bind_links( menu_object.bind_links(
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 link_setup_workflow_transition_delete
), sources=(WorkflowTransition,) ), sources=(WorkflowTransition,)
) )
@@ -249,3 +262,8 @@ class DocumentStatesApp(MayanAppConfig):
dispatch_uid='handler_index_document_save', dispatch_uid='handler_index_document_save',
sender=WorkflowInstanceLogEntry 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 __future__ import absolute_import, unicode_literals
from django import forms from django import forms
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 .models import Workflow, WorkflowState, WorkflowTransition
@@ -39,6 +40,54 @@ class WorkflowTransitionForm(forms.ModelForm):
model = WorkflowTransition 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): class WorkflowInstanceTransitionForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user = kwargs.pop('user') user = kwargs.pop('user')

View File

@@ -1,8 +1,36 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from document_indexing.tasks import task_index_document 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): def launch_workflow(sender, instance, created, **kwargs):
@@ -12,11 +40,3 @@ def launch_workflow(sender, instance, created, **kwargs):
if created: if created:
Workflow.objects.launch_for(instance) 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', text=_('States'), view='document_states:workflow_state_list',
args='resolved_object.pk' 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 acls.models import AccessControlList
from common.validators import validate_internal_name from common.validators import validate_internal_name
from documents.models import Document, DocumentType from documents.models import Document, DocumentType
from events.models import EventType
from permissions import Permission from permissions import Permission
from .managers import WorkflowManager from .managers import WorkflowManager
@@ -178,6 +179,24 @@ class WorkflowTransition(models.Model):
verbose_name_plural = _('Workflow transitions') 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 @python_2_unicode_compatible
class WorkflowInstance(models.Model): class WorkflowInstance(models.Model):
workflow = models.ForeignKey( workflow = models.ForeignKey(
@@ -197,7 +216,7 @@ class WorkflowInstance(models.Model):
'document_states:workflow_instance_detail', args=(str(self.pk),) '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: try:
if transition in self.get_current_state().origin_transitions.all(): if transition in self.get_current_state().origin_transitions.all():
self.log_entries.create( self.log_entries.create(
@@ -306,8 +325,8 @@ class WorkflowInstanceLogEntry(models.Model):
verbose_name=_('Transition') verbose_name=_('Transition')
) )
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, settings.AUTH_USER_MODEL, blank=True, null=True,
verbose_name=_('User') on_delete=models.CASCADE, verbose_name=_('User')
) )
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))

View File

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

View File

@@ -14,14 +14,16 @@ from common.views import (
) )
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.models import EventType
from .forms import ( from .forms import (
WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm, WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm,
WorkflowTransitionForm WorkflowTransitionForm, WorkflowTransitionTriggerEventRelationshipFormSet
) )
from .models import ( from .models import (
Workflow, WorkflowInstance, WorkflowState, WorkflowTransition, Workflow, WorkflowInstance, WorkflowState, WorkflowTransition,
WorkflowRuntimeProxy, WorkflowStateRuntimeProxy WorkflowRuntimeProxy, WorkflowStateRuntimeProxy,
) )
from .permissions import ( from .permissions import (
permission_workflow_create, permission_workflow_delete, permission_workflow_create, permission_workflow_delete,
@@ -507,6 +509,79 @@ class WorkflowStateListView(SingleObjectListView):
return get_object_or_404(WorkflowRuntimeProxy, pk=self.kwargs['pk']) 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): class ToolLaunchAllWorkflows(ConfirmView):
extra_context = { extra_context = {
'title': _('Launch all workflows?') '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 @classmethod
def all(cls): def all(cls):
return cls._registry.values() return Event.sort(event_type_list=cls._registry.values())
@classmethod @classmethod
def get(cls, name): def get(cls, name):
@@ -30,6 +30,17 @@ class Event(object):
except KeyError as exception: except KeyError as exception:
return force_text(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): def __init__(self, name, label):
self.name = name self.name = name
self.label = label self.label = label

View File

@@ -21,4 +21,8 @@ class EventType(models.Model):
return self.get_class().label return self.get_class().label
def get_class(self): 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