Add workflow instance transitioning support, add workflow instance detail view

This commit is contained in:
Roberto Rosario
2015-01-17 01:07:20 -04:00
parent eefbd5758a
commit 644ce2839e
7 changed files with 265 additions and 33 deletions

View File

@@ -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')

View File

@@ -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',)

View File

@@ -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'}

View File

@@ -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')

View File

@@ -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',

View File

@@ -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'])

View File

@@ -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 %}