Add view for associating a workflow to a document type, add support to auto launch a workflow instance when a document is created, allow associating a many workflows to a document, add view to display a document's workflows instances

This commit is contained in:
Roberto Rosario
2015-01-13 06:02:42 -04:00
parent 8ff3747a15
commit b2453ee01e
8 changed files with 231 additions and 25 deletions

View File

@@ -1,16 +1,28 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.utils import encapsulate from common.utils import encapsulate
from documents.models import Document
from navigation.api import register_links, register_model_list_columns from navigation.api import register_links, register_model_list_columns
from project_setup.api import register_setup from project_setup.api import register_setup
from .models import Workflow, WorkflowState, WorkflowTransition from .models import Workflow, WorkflowInstance, WorkflowState, WorkflowTransition
from .links import (link_setup_workflow_create, link_setup_workflow_delete, from .links import (link_setup_workflow_create, link_setup_workflow_delete,
link_setup_workflow_edit, link_setup_workflow_list, link_setup_workflow_edit, link_setup_workflow_list,
link_setup_workflow_states, link_setup_workflow_states_create, link_setup_workflow_states, link_setup_workflow_states_create,
link_setup_workflow_transitions, link_setup_workflow_transitions_create) link_setup_workflow_transitions, link_setup_workflow_transitions_create,
link_setup_workflow_document_types,
link_document_workflow_list)
@receiver(post_save, dispatch_uid='launch_workflow', sender=Document)
def launch_workflow(sender, instance, created, **kwargs):
if created:
Workflow.objects.launch_for(instance)
register_setup(link_setup_workflow_list) register_setup(link_setup_workflow_list)
@@ -28,6 +40,17 @@ register_model_list_columns(WorkflowState, [
}, },
]) ])
register_model_list_columns(WorkflowInstance, [
{
'name': _('Current state'),
'attribute': 'get_current_state'
},
{
'name': _('Last transition'),
'attribute': 'get_last_transition'
},
])
register_model_list_columns(WorkflowTransition, [ register_model_list_columns(WorkflowTransition, [
{ {
'name': _('Origin state'), 'name': _('Origin state'),
@@ -39,6 +62,7 @@ register_model_list_columns(WorkflowTransition, [
}, },
]) ])
register_links([Document], [link_document_workflow_list], menu_name='form_header')
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, '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_edit, link_setup_workflow_delete]) 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') register_links([Workflow], [link_setup_workflow_states_create, link_setup_workflow_transitions_create], menu_name='sidebar')

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Workflow, WorkflowState, WorkflowTransition from .models import Workflow, WorkflowInstance, WorkflowState, WorkflowTransition
class WorkflowStateInline(admin.TabularInline): class WorkflowStateInline(admin.TabularInline):
@@ -15,4 +15,9 @@ class WorkflowAdmin(admin.ModelAdmin):
inlines = [WorkflowStateInline, WorkflowTransitionInline] inlines = [WorkflowStateInline, WorkflowTransitionInline]
class WorkflowInstanceAdmin(admin.ModelAdmin):
list_display = ('workflow', 'document')
admin.site.register(Workflow, WorkflowAdmin) admin.site.register(Workflow, WorkflowAdmin)
admin.site.register(WorkflowInstance, WorkflowInstanceAdmin)

View File

@@ -12,3 +12,7 @@ link_setup_workflow_states_create = {'text': _('Create state'), 'view': 'documen
link_setup_workflow_transitions = {'text': _('Transitions'), 'view': 'document_states:setup_workflow_transitions', 'args': 'object.pk', 'famfam': 'lightning'} link_setup_workflow_transitions = {'text': _('Transitions'), 'view': 'document_states:setup_workflow_transitions', 'args': 'object.pk', 'famfam': 'lightning'}
link_setup_workflow_transitions_create = {'text': _('Create transition'), 'view': 'document_states:setup_workflow_transitions_create', 'args': 'object.pk', 'famfam': 'lightning_add'} link_setup_workflow_transitions_create = {'text': _('Create transition'), 'view': 'document_states:setup_workflow_transitions_create', 'args': 'object.pk', 'famfam': 'lightning_add'}
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'}

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
from django.db import models
class WorkflowManager(models.Manager):
def launch_for(self, document):
for workflow in document.document_type.workflows.all():
workflow.launch_for(document)

View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'DocumentTypeWorkflow', fields ['document_type', 'workflow']
db.delete_unique(u'document_states_documenttypeworkflow', ['document_type_id', 'workflow_id'])
# Deleting model 'DocumentTypeWorkflow'
db.delete_table(u'document_states_documenttypeworkflow')
# Adding M2M table for field document_types on 'Workflow'
m2m_table_name = db.shorten_name(u'document_states_workflow_document_types')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('workflow', models.ForeignKey(orm[u'document_states.workflow'], null=False)),
('documenttype', models.ForeignKey(orm[u'documents.documenttype'], null=False))
))
db.create_unique(m2m_table_name, ['workflow_id', 'documenttype_id'])
def backwards(self, orm):
# Adding model 'DocumentTypeWorkflow'
db.create_table(u'document_states_documenttypeworkflow', (
('document_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['documents.DocumentType'])),
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('workflow', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['document_states.Workflow'])),
))
db.send_create_signal(u'document_states', ['DocumentTypeWorkflow'])
# Adding unique constraint on 'DocumentTypeWorkflow', fields ['document_type', 'workflow']
db.create_unique(u'document_states_documenttypeworkflow', ['document_type_id', 'workflow_id'])
# Removing M2M table for field document_types on 'Workflow'
db.delete_table(db.shorten_name(u'document_states_workflow_document_types'))
models = {
u'document_states.workflow': {
'Meta': {'object_name': 'Workflow'},
'document_types': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['documents.DocumentType']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
},
u'document_states.workflowinstance': {
'Meta': {'unique_together': "((u'document', u'workflow'),)", 'object_name': 'WorkflowInstance'},
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['documents.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['document_states.Workflow']"})
},
u'document_states.workflowinstancelogentry': {
'Meta': {'object_name': 'WorkflowInstanceLogEntry'},
'datetime': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'transition': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['document_states.WorkflowTransition']"}),
'workflow_instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'log_entries'", 'to': u"orm['document_states.WorkflowInstance']"})
},
u'document_states.workflowstate': {
'Meta': {'unique_together': "((u'workflow', u'label'),)", 'object_name': 'WorkflowState'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'initial': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'workflow': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'states'", 'to': u"orm['document_states.Workflow']"})
},
u'document_states.workflowtransition': {
'Meta': {'unique_together': "((u'workflow', u'label', u'origin_state', u'destination_state'),)", 'object_name': 'WorkflowTransition'},
'destination_state': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'destinations'", 'to': u"orm['document_states.WorkflowState']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'origin_state': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'origins'", 'to': u"orm['document_states.WorkflowState']"}),
'workflow': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'transitions'", 'to': u"orm['document_states.Workflow']"})
},
u'documents.document': {
'Meta': {'ordering': "['-date_added']", 'object_name': 'Document'},
'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'document_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'documents'", 'to': u"orm['documents.DocumentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'default': "u'Uninitialized document'", 'max_length': '255', 'db_index': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "u'eng'", 'max_length': '8'}),
'uuid': ('django.db.models.fields.CharField', [], {'default': "u'fbd029e5-0f29-4863-b72c-e8cd1373b9f3'", 'max_length': '48'})
},
u'documents.documenttype': {
'Meta': {'ordering': "['name']", 'object_name': 'DocumentType'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'ocr': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
}
}
complete_apps = ['document_states']

View File

@@ -6,20 +6,31 @@ from django.utils.translation import ugettext as _
from documents.models import Document, DocumentType from documents.models import Document, DocumentType
from .managers import WorkflowManager
@python_2_unicode_compatible @python_2_unicode_compatible
class Workflow(models.Model): class Workflow(models.Model):
label = models.CharField(max_length=255, unique=True, verbose_name=_('Label')) label = models.CharField(max_length=255, unique=True, verbose_name=_('Label'))
document_types = models.ManyToManyField(DocumentType, related_name='workflows', verbose_name=_('Document types'))
objects = WorkflowManager()
def __str__(self): def __str__(self):
return self.label return self.label
def get_document_types_not_in_workflow(self):
return DocumentType.objects.exclude(pk__in=self.document_types.all())
def get_initial_state(self): def get_initial_state(self):
try: try:
return self.states.get(initial=True) return self.states.get(initial=True)
except self.states.model.DoesNotExist: except self.states.model.DoesNotExist:
return None return None
def launch_for(self, document):
self.instances.create(document=document)
class Meta: class Meta:
verbose_name = _('Workflow') verbose_name = _('Workflow')
verbose_name_plural = _('Workflows') verbose_name_plural = _('Workflows')
@@ -62,24 +73,10 @@ class WorkflowTransition(models.Model):
verbose_name_plural = _('Workflow transitions') verbose_name_plural = _('Workflow transitions')
@python_2_unicode_compatible
class DocumentTypeWorkflow(models.Model):
document_type = models.ForeignKey(DocumentType, verbose_name=_('Document type'))
workflow = models.ForeignKey(Workflow, verbose_name=_('Workflow'))
def __str__(self):
return self.label
class Meta:
unique_together = ('document_type', 'workflow')
verbose_name = _('Document type workflow')
verbose_name_plural = _('Document type workflow')
@python_2_unicode_compatible @python_2_unicode_compatible
class WorkflowInstance(models.Model): class WorkflowInstance(models.Model):
workflow = models.ForeignKey(Workflow, verbose_name=_('Workflow')) workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow'))
document = models.ForeignKey(Document, verbose_name=_('Document')) document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document'))
def do_transition(self, transition): def do_transition(self, transition):
try: try:
@@ -91,12 +88,18 @@ class WorkflowInstance(models.Model):
def get_current_state(self): def get_current_state(self):
try: try:
return self.log_entries.order_by('datetime').last().transition.destination_state return self.get_last_transition().destination_state
except AttributeError: except AttributeError:
return self.workflow.get_initial_state() return self.workflow.get_initial_state()
def get_last_transition(self):
try:
return self.log_entries.order_by('datetime').last().transition
except AttributeError:
return None
def __str__(self): def __str__(self):
return self.label return self.workflow
class Meta: class Meta:
unique_together = ('document', 'workflow') unique_together = ('document', 'workflow')

View File

@@ -3,7 +3,8 @@ from django.conf.urls import patterns, url
from .views import (SetupWorkflowCreateView, SetupWorkflowDeleteView, from .views import (SetupWorkflowCreateView, SetupWorkflowDeleteView,
SetupWorkflowEditView, SetupWorkflowListView, SetupWorkflowEditView, SetupWorkflowListView,
SetupWorkflowStateListView, SetupWorkflowStateCreateView, SetupWorkflowStateListView, SetupWorkflowStateCreateView,
SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView) SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView,
DocumentWorkflowListView)
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^setup/all/$', SetupWorkflowListView.as_view(), name='setup_workflow_list'), url(r'^setup/all/$', SetupWorkflowListView.as_view(), name='setup_workflow_list'),
@@ -15,4 +16,12 @@ urlpatterns = patterns('',
url(r'^setup/(?P<pk>\d+)/transitions/$', SetupWorkflowTransitionListView.as_view(), name='setup_workflow_transitions'), 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'^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'),
) )
urlpatterns += patterns('document_states.views',
url(r'^setup/(?P<pk>\d+)/document_types/$', 'setup_workflow_document_types', name='setup_workflow_document_types'),
)

View File

@@ -12,13 +12,47 @@ from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _, ungettext from django.utils.translation import ugettext_lazy as _, ungettext
from acls.models import AccessEntry from acls.models import AccessEntry
from common.utils import encapsulate, generate_choices_w_labels
from common.views import (SingleObjectCreateView, SingleObjectDeleteView, from common.views import (SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectEditView, SingleObjectListView) SingleObjectEditView, SingleObjectListView,
assign_remove)
from common.widgets import two_state_template
from documents.models import Document
from permissions.models import Permission from permissions.models import Permission
from .forms import WorkflowStateForm, WorkflowTransitionForm from .forms import WorkflowStateForm, WorkflowTransitionForm
from .models import Workflow from .models import Workflow
from .permissions import PERMISSION_WORKFLOW_CREATE, PERMISSION_WORKFLOW_DELETE, PERMISSION_WORKFLOW_EDIT, PERMISSION_WORKFLOW_VIEW from .permissions import (PERMISSION_WORKFLOW_CREATE, PERMISSION_WORKFLOW_DELETE,
PERMISSION_WORKFLOW_EDIT, PERMISSION_WORKFLOW_VIEW,
PERMISSION_DOCUMENT_WORKFLOW_VIEW)
class DocumentWorkflowListView(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())
return super(DocumentWorkflowListView, self).dispatch(request, *args, **kwargs)
def get_document(self):
return get_object_or_404(Document, pk=self.kwargs['pk'])
def get_queryset(self):
return self.get_document().workflows.all()
def get_context_data(self, **kwargs):
context = super(DocumentWorkflowListView, self).get_context_data(**kwargs)
context.update(
{
'hide_link': True,
'object': self.get_document(),
'title': _('Workflows of document: %s') % self.get_document()
}
)
return context
class SetupWorkflowListView(SingleObjectListView): class SetupWorkflowListView(SingleObjectListView):
@@ -185,3 +219,25 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView):
return super(SetupWorkflowTransitionCreateView, self).form_invalid(form) return super(SetupWorkflowTransitionCreateView, self).form_invalid(form)
else: else:
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
def setup_workflow_document_types(request, pk):
workflow = get_object_or_404(Workflow, pk=pk)
try:
Permission.objects.check_permissions(request.user, [PERMISSION_WORKFLOW_EDIT])
except PermissionDenied:
AccessEntry.objects.check_access(PERMISSION_WORKFLOW_EDIT, request.user, workflow)
return assign_remove(
request,
left_list=lambda: generate_choices_w_labels(workflow.get_document_types_not_in_workflow(), display_object_type=False),
right_list=lambda: generate_choices_w_labels(workflow.document_types.all(), display_object_type=False),
add_method=lambda x: workflow.document_types.add(x),
remove_method=lambda x: workflow.document_types.remove(x),
decode_content_type=True,
extra_context={
'main_title': _(u'Document types assigned the workflow: %s') % workflow,
'object': workflow,
}
)