Add workflow instance transitioning support, add workflow instance detail view
This commit is contained in:
@@ -9,13 +9,18 @@ from documents.models import Document
|
||||
from navigation.api import register_links, register_model_list_columns
|
||||
from project_setup.api import register_setup
|
||||
|
||||
from .models import Workflow, WorkflowInstance, WorkflowState, WorkflowTransition
|
||||
from .links import (link_setup_workflow_create, link_setup_workflow_delete,
|
||||
link_setup_workflow_edit, link_setup_workflow_list,
|
||||
link_setup_workflow_states, link_setup_workflow_states_create,
|
||||
link_setup_workflow_transitions, link_setup_workflow_transitions_create,
|
||||
link_setup_workflow_document_types,
|
||||
link_document_workflow_list)
|
||||
from .models import (
|
||||
Workflow, WorkflowInstance, WorkflowInstanceLogEntry, WorkflowState,
|
||||
WorkflowTransition
|
||||
)
|
||||
from .links import (
|
||||
link_document_workflow_instance_list, link_setup_workflow_create,
|
||||
link_setup_workflow_delete, link_setup_workflow_edit,
|
||||
link_setup_workflow_list, link_setup_workflow_states,
|
||||
link_setup_workflow_states_create, link_setup_workflow_transitions,
|
||||
link_setup_workflow_transitions_create, link_setup_workflow_document_types,
|
||||
link_workflow_instance_detail, link_workflow_instance_transition
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, dispatch_uid='launch_workflow', sender=Document)
|
||||
@@ -49,6 +54,10 @@ register_model_list_columns(WorkflowInstance, [
|
||||
'name': _('Last transition'),
|
||||
'attribute': 'get_last_transition'
|
||||
},
|
||||
{
|
||||
'name': _('Date and time'),
|
||||
'attribute': encapsulate(lambda workflow: getattr(workflow.get_last_log_entry(), 'datetime', _('None')))
|
||||
},
|
||||
])
|
||||
|
||||
register_model_list_columns(WorkflowTransition, [
|
||||
@@ -62,7 +71,19 @@ register_model_list_columns(WorkflowTransition, [
|
||||
},
|
||||
])
|
||||
|
||||
register_links([Document], [link_document_workflow_list], menu_name='form_header')
|
||||
register_model_list_columns(WorkflowInstanceLogEntry, [
|
||||
{
|
||||
'name': _('Date and time'),
|
||||
'attribute': 'datetime'
|
||||
},
|
||||
{
|
||||
'name': _('Transition'),
|
||||
'attribute': 'transition'
|
||||
},
|
||||
])
|
||||
|
||||
register_links([Document], [link_document_workflow_instance_list], menu_name='form_header')
|
||||
register_links([WorkflowInstance], [link_workflow_instance_detail, link_workflow_instance_transition])
|
||||
register_links([Workflow, 'document_states:setup_workflow_create', 'document_states:setup_workflow_list'], [link_setup_workflow_list, link_setup_workflow_create], menu_name='secondary_menu')
|
||||
register_links([Workflow], [link_setup_workflow_states, link_setup_workflow_transitions, link_setup_workflow_document_types, link_setup_workflow_edit, link_setup_workflow_delete])
|
||||
register_links([Workflow], [link_setup_workflow_states_create, link_setup_workflow_transitions_create], menu_name='sidebar')
|
||||
|
||||
@@ -3,7 +3,9 @@ from __future__ import unicode_literals
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .models import WorkflowState, WorkflowTransition
|
||||
from common.forms import DetailForm
|
||||
|
||||
from .models import WorkflowState, WorkflowInstance, WorkflowTransition
|
||||
|
||||
|
||||
class WorkflowStateForm(forms.ModelForm):
|
||||
@@ -13,7 +15,27 @@ class WorkflowStateForm(forms.ModelForm):
|
||||
|
||||
|
||||
class WorkflowTransitionForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
workflow = kwargs.pop('workflow')
|
||||
super(WorkflowTransitionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['origin_state'].queryset = self.fields['origin_state'].queryset.filter(workflow=workflow)
|
||||
self.fields['destination_state'].queryset = self.fields['destination_state'].queryset.filter(workflow=workflow)
|
||||
|
||||
class Meta:
|
||||
# TODO: restrict states to the ones of this workflow
|
||||
fields = ('label', 'origin_state', 'destination_state')
|
||||
model = WorkflowTransition
|
||||
|
||||
|
||||
class WorkflowInstanceTransitionForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
workflow = kwargs.pop('workflow')
|
||||
super(WorkflowInstanceTransitionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['transition'].choices = workflow.get_transition_choices().values_list('pk', 'label')
|
||||
|
||||
transition = forms.ChoiceField(label=_('Transition'))
|
||||
|
||||
|
||||
class WorkflowInstanceDetailForm(DetailForm):
|
||||
class Meta:
|
||||
model = WorkflowInstance
|
||||
fields = ('workflow',)
|
||||
|
||||
@@ -15,4 +15,6 @@ link_setup_workflow_transitions_create = {'text': _('Create transition'), 'view'
|
||||
|
||||
link_setup_workflow_document_types = {'text': _('Document types'), 'view': 'document_states:setup_workflow_document_types', 'args': 'object.pk', 'famfam': 'layout'}
|
||||
|
||||
link_document_workflow_list = {'text': _('Workflows'), 'view': 'document_states:document_workflow_list', 'args': 'object.pk', 'famfam': 'table'}
|
||||
link_document_workflow_instance_list = {'text': _('Workflows'), 'view': 'document_states:document_workflow_instance_list', 'args': 'object.pk', 'famfam': 'table'}
|
||||
link_workflow_instance_detail = {'text': _('Detail'), 'view': 'document_states:workflow_instance_detail', 'args': 'workflow_instance.pk', 'famfam': 'table'}
|
||||
link_workflow_instance_transition = {'text': _('Transition'), 'view': 'document_states:workflow_instance_transition', 'args': 'workflow_instance.pk', 'famfam': 'table_lightning'}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -61,8 +62,8 @@ class WorkflowTransition(models.Model):
|
||||
workflow = models.ForeignKey(Workflow, related_name='transitions', verbose_name=_('Workflow'))
|
||||
label = models.CharField(max_length=255, verbose_name=_('Label'))
|
||||
|
||||
origin_state = models.ForeignKey(WorkflowState, related_name='origins', verbose_name=_('Origin state'))
|
||||
destination_state = models.ForeignKey(WorkflowState, related_name='destinations', verbose_name=_('Destination state'))
|
||||
origin_state = models.ForeignKey(WorkflowState, related_name='origin_transitions', verbose_name=_('Origin state'))
|
||||
destination_state = models.ForeignKey(WorkflowState, related_name='destination_transitions', verbose_name=_('Destination state'))
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
@@ -78,9 +79,12 @@ class WorkflowInstance(models.Model):
|
||||
workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow'))
|
||||
document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('document_states:workflow_instance_detail', args=[str(self.pk)])
|
||||
|
||||
def do_transition(self, transition):
|
||||
try:
|
||||
if transition in self.get_current_state().origins:
|
||||
if transition in self.get_current_state().origin_transitions.all():
|
||||
self.log_entries.create(transition=transition)
|
||||
except AttributeError:
|
||||
# No initial state has been set for this workflow
|
||||
@@ -94,12 +98,21 @@ class WorkflowInstance(models.Model):
|
||||
|
||||
def get_last_transition(self):
|
||||
try:
|
||||
return self.log_entries.order_by('datetime').last().transition
|
||||
return self.get_last_log_entry().transition
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_last_log_entry(self):
|
||||
try:
|
||||
return self.log_entries.order_by('datetime').last()
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_transition_choices(self):
|
||||
return self.get_current_state().origin_transitions.all()
|
||||
|
||||
def __str__(self):
|
||||
return self.workflow
|
||||
return unicode(self.workflow)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('document', 'workflow')
|
||||
@@ -114,7 +127,7 @@ class WorkflowInstanceLogEntry(models.Model):
|
||||
transition = models.ForeignKey(WorkflowTransition, verbose_name=_('Transition'))
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
return unicode(self.transition)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Workflow instance log entry')
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from .views import (SetupWorkflowCreateView, SetupWorkflowDeleteView,
|
||||
SetupWorkflowEditView, SetupWorkflowListView,
|
||||
SetupWorkflowStateListView, SetupWorkflowStateCreateView,
|
||||
SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView,
|
||||
DocumentWorkflowListView)
|
||||
from .views import (
|
||||
SetupWorkflowCreateView, SetupWorkflowDeleteView, SetupWorkflowEditView,
|
||||
SetupWorkflowListView, SetupWorkflowStateListView,
|
||||
SetupWorkflowStateCreateView, SetupWorkflowTransitionListView,
|
||||
SetupWorkflowTransitionCreateView, DocumentWorkflowInstanceListView,
|
||||
WorkflowInstanceDetailView, WorkflowInstanceTransitionView
|
||||
)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^setup/all/$', SetupWorkflowListView.as_view(), name='setup_workflow_list'),
|
||||
@@ -17,7 +19,9 @@ urlpatterns = patterns('',
|
||||
url(r'^setup/(?P<pk>\d+)/transitions/$', SetupWorkflowTransitionListView.as_view(), name='setup_workflow_transitions'),
|
||||
url(r'^setup/(?P<pk>\d+)/transitions/create/$', SetupWorkflowTransitionCreateView.as_view(), name='setup_workflow_transitions_create'),
|
||||
|
||||
url(r'^document/(?P<pk>\d+)/workflows/$', DocumentWorkflowListView.as_view(), name='document_workflow_list'),
|
||||
url(r'^document/(?P<pk>\d+)/workflows/$', DocumentWorkflowInstanceListView.as_view(), name='document_workflow_instance_list'),
|
||||
url(r'^document/workflows/(?P<pk>\d+)/$', WorkflowInstanceDetailView.as_view(), name='workflow_instance_detail'),
|
||||
url(r'^document/workflows/(?P<pk>\d+)/transition/$', WorkflowInstanceTransitionView.as_view(), name='workflow_instance_transition'),
|
||||
)
|
||||
|
||||
urlpatterns += patterns('document_states.views',
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.shortcuts import render_to_response, get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
||||
from django.views.generic import FormView
|
||||
|
||||
from acls.models import AccessEntry
|
||||
from common.utils import encapsulate, generate_choices_w_labels
|
||||
@@ -20,21 +21,26 @@ from common.widgets import two_state_template
|
||||
from documents.models import Document
|
||||
from permissions.models import Permission
|
||||
|
||||
from .forms import WorkflowStateForm, WorkflowTransitionForm
|
||||
from .models import Workflow
|
||||
from .permissions import (PERMISSION_WORKFLOW_CREATE, PERMISSION_WORKFLOW_DELETE,
|
||||
PERMISSION_WORKFLOW_EDIT, PERMISSION_WORKFLOW_VIEW,
|
||||
PERMISSION_DOCUMENT_WORKFLOW_VIEW)
|
||||
from .forms import (
|
||||
WorkflowInstanceDetailForm, WorkflowInstanceTransitionForm,
|
||||
WorkflowStateForm, WorkflowTransitionForm
|
||||
)
|
||||
from .models import Workflow, WorkflowInstance
|
||||
from .permissions import (
|
||||
PERMISSION_WORKFLOW_CREATE, PERMISSION_WORKFLOW_DELETE,
|
||||
PERMISSION_WORKFLOW_EDIT, PERMISSION_WORKFLOW_VIEW,
|
||||
PERMISSION_DOCUMENT_WORKFLOW_VIEW, PERMISSION_DOCUMENT_WORKFLOW_TRANSITION
|
||||
)
|
||||
|
||||
|
||||
class DocumentWorkflowListView(SingleObjectListView):
|
||||
class DocumentWorkflowInstanceListView(SingleObjectListView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_WORKFLOW_VIEW])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_DOCUMENT_WORKFLOW_VIEW, request.user, self.get_workflow())
|
||||
AccessEntry.objects.check_access(PERMISSION_DOCUMENT_WORKFLOW_VIEW, request.user, self.get_document())
|
||||
|
||||
return super(DocumentWorkflowListView, self).dispatch(request, *args, **kwargs)
|
||||
return super(DocumentWorkflowInstanceListView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_document(self):
|
||||
return get_object_or_404(Document, pk=self.kwargs['pk'])
|
||||
@@ -43,18 +49,123 @@ class DocumentWorkflowListView(SingleObjectListView):
|
||||
return self.get_document().workflows.all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DocumentWorkflowListView, self).get_context_data(**kwargs)
|
||||
context = super(DocumentWorkflowInstanceListView, self).get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
'hide_link': True,
|
||||
'object': self.get_document(),
|
||||
'title': _('Workflows of document: %s') % self.get_document()
|
||||
'title': _('Workflows of document: %s') % self.get_document(),
|
||||
'list_object_variable_name': 'workflow_instance',
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WorkflowInstanceDetailView(SingleObjectListView):
|
||||
template_name = 'main/generic_multi_subtemplates.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_WORKFLOW_VIEW])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_DOCUMENT_WORKFLOW_VIEW, request.user, self.get_workflow_instance().document)
|
||||
|
||||
return super(WorkflowInstanceDetailView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_workflow_instance(self):
|
||||
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_workflow_instance().log_entries.all()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
form = WorkflowInstanceDetailForm(instance=self.get_workflow_instance(), extra_fields=[
|
||||
{'label': _('Current state'), 'field': 'get_current_state'},
|
||||
{'label': _('Last transition'), 'field': 'get_last_transition'},
|
||||
]
|
||||
)
|
||||
|
||||
context = {
|
||||
'object': self.get_workflow_instance().document,
|
||||
'workflow_instance': self.get_workflow_instance(),
|
||||
'navigation_object_list': [
|
||||
{'object': 'object', 'name': _('Index')},
|
||||
{'object': 'workflow_instance', 'name': _('Node')}
|
||||
],
|
||||
'title': _('Detail of workflow: %(workflow)s - %(document)s') % {
|
||||
'workflow': self.get_workflow_instance(), 'document': self.get_workflow_instance().document
|
||||
},
|
||||
'subtemplates_list': [
|
||||
{
|
||||
'name': 'main/generic_detail_subtemplate.html',
|
||||
'context': {
|
||||
'form': form,
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'main/generic_list_subtemplate.html',
|
||||
'context': {
|
||||
'object_list': self.get_queryset(),
|
||||
'title': _('Log entries'),
|
||||
'hide_object': True,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class WorkflowInstanceTransitionView(FormView):
|
||||
form_class = WorkflowInstanceTransitionForm
|
||||
template_name = 'main/generic_form.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
Permission.objects.check_permissions(request.user, [PERMISSION_DOCUMENT_WORKFLOW_TRANSITION])
|
||||
except PermissionDenied:
|
||||
AccessEntry.objects.check_access(PERMISSION_DOCUMENT_WORKFLOW_TRANSITION, request.user, self.get_workflow_instance().document)
|
||||
|
||||
return super(WorkflowInstanceTransitionView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
transition = self.get_workflow_instance().workflow.transitions.get(pk=form.cleaned_data['transition'])
|
||||
self.get_workflow_instance().do_transition(transition)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(WorkflowInstanceTransitionView, self).get_form_kwargs()
|
||||
kwargs['workflow'] = self.get_workflow_instance()
|
||||
return kwargs
|
||||
|
||||
def get_workflow_instance(self):
|
||||
return get_object_or_404(WorkflowInstance, pk=self.kwargs['pk'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(WorkflowInstanceTransitionView, self).get_context_data(**kwargs)
|
||||
|
||||
context.update(
|
||||
{
|
||||
'object': self.get_workflow_instance().document,
|
||||
'workflow_instance': self.get_workflow_instance(),
|
||||
'navigation_object_list': [
|
||||
{'object': 'object', 'name': _('Index')},
|
||||
{'object': 'workflow_instance', 'name': _('Node')}
|
||||
],
|
||||
'title': _('Do transition for workflow: %s') % self.get_workflow_instance(),
|
||||
'submit_label': _('Submit'),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_workflow_instance().get_absolute_url()
|
||||
|
||||
|
||||
# Setup
|
||||
|
||||
class SetupWorkflowListView(SingleObjectListView):
|
||||
extra_context = {
|
||||
'title': _('Workflows'),
|
||||
@@ -200,6 +311,11 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
|
||||
)
|
||||
return context
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(SetupWorkflowTransitionCreateView, self).get_form_kwargs()
|
||||
kwargs['workflow'] = self.get_workflow()
|
||||
return kwargs
|
||||
|
||||
def get_workflow(self):
|
||||
return get_object_or_404(Workflow, pk=self.kwargs['pk'])
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends 'main/base.html' %}
|
||||
|
||||
{% load subtemplates_tags %}
|
||||
|
||||
{% block title %} :: {% include 'main/calculate_form_title.html' %}{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% for subtemplate in sidebar_subtemplates_list %}
|
||||
{% if subtemplate.form %}
|
||||
{% render_subtemplate subtemplate.name subtemplate.context as rendered_subtemplate %}
|
||||
<div class="generic_subform">
|
||||
{{ rendered_subtemplate }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_subtemplate subtemplate.name subtemplate.context as rendered_subtemplate %}
|
||||
{{ rendered_subtemplate }}
|
||||
{% endif %}
|
||||
{% if subtemplate.grid_clear or not subtemplate.grid %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if main_title %}
|
||||
<div class="content">
|
||||
<h2 class="title">{{ main_title }}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="pure-g">
|
||||
|
||||
{% for subtemplate in subtemplates_list %}
|
||||
|
||||
{% if subtemplate.grid %}
|
||||
<div class="pure-u-{{ subtemplate.grid }}-24">
|
||||
{% else %}
|
||||
<div class="pure-u-1">
|
||||
{% endif %}
|
||||
{% if subtemplate.form %}
|
||||
{% render_subtemplate subtemplate.name subtemplate.context as rendered_subtemplate %}
|
||||
<div class="generic_subform">
|
||||
{{ rendered_subtemplate }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% render_subtemplate subtemplate.name subtemplate.context as rendered_subtemplate %}
|
||||
{{ rendered_subtemplate }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if subtemplate.grid_clear or not subtemplate.grid %}
|
||||
<div class="clear"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user