diff --git a/mayan/apps/document_states/__init__.py b/mayan/apps/document_states/__init__.py index cb2205565b..6c598a0ee9 100644 --- a/mayan/apps/document_states/__init__.py +++ b/mayan/apps/document_states/__init__.py @@ -1,16 +1,28 @@ 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 common.utils import encapsulate +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, WorkflowState, WorkflowTransition +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_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) @@ -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, [ { '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], [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') diff --git a/mayan/apps/document_states/admin.py b/mayan/apps/document_states/admin.py index b223625e10..5c5a39cc7e 100644 --- a/mayan/apps/document_states/admin.py +++ b/mayan/apps/document_states/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Workflow, WorkflowState, WorkflowTransition +from .models import Workflow, WorkflowInstance, WorkflowState, WorkflowTransition class WorkflowStateInline(admin.TabularInline): @@ -15,4 +15,9 @@ class WorkflowAdmin(admin.ModelAdmin): inlines = [WorkflowStateInline, WorkflowTransitionInline] +class WorkflowInstanceAdmin(admin.ModelAdmin): + list_display = ('workflow', 'document') + + admin.site.register(Workflow, WorkflowAdmin) +admin.site.register(WorkflowInstance, WorkflowInstanceAdmin) diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index bf0f94fe7e..fd9b87bcfe 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -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_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'} diff --git a/mayan/apps/document_states/managers.py b/mayan/apps/document_states/managers.py new file mode 100644 index 0000000000..ea9ad19962 --- /dev/null +++ b/mayan/apps/document_states/managers.py @@ -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) diff --git a/mayan/apps/document_states/migrations/0007_auto__del_documenttypeworkflow__del_unique_documenttypeworkflow_docume.py b/mayan/apps/document_states/migrations/0007_auto__del_documenttypeworkflow__del_unique_documenttypeworkflow_docume.py new file mode 100644 index 0000000000..9c16d0fe44 --- /dev/null +++ b/mayan/apps/document_states/migrations/0007_auto__del_documenttypeworkflow__del_unique_documenttypeworkflow_docume.py @@ -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'] \ No newline at end of file diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 253fdfe95c..d5ba3fc94e 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -6,20 +6,31 @@ from django.utils.translation import ugettext as _ from documents.models import Document, DocumentType +from .managers import WorkflowManager + @python_2_unicode_compatible class Workflow(models.Model): 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): 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): try: return self.states.get(initial=True) except self.states.model.DoesNotExist: return None + def launch_for(self, document): + self.instances.create(document=document) + class Meta: verbose_name = _('Workflow') verbose_name_plural = _('Workflows') @@ -62,24 +73,10 @@ class WorkflowTransition(models.Model): 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 class WorkflowInstance(models.Model): - workflow = models.ForeignKey(Workflow, verbose_name=_('Workflow')) - document = models.ForeignKey(Document, verbose_name=_('Document')) + workflow = models.ForeignKey(Workflow, related_name='instances', verbose_name=_('Workflow')) + document = models.ForeignKey(Document, related_name='workflows', verbose_name=_('Document')) def do_transition(self, transition): try: @@ -91,12 +88,18 @@ class WorkflowInstance(models.Model): def get_current_state(self): try: - return self.log_entries.order_by('datetime').last().transition.destination_state + return self.get_last_transition().destination_state except AttributeError: 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): - return self.label + return self.workflow class Meta: unique_together = ('document', 'workflow') diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index df71150610..b49b2982cb 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -3,7 +3,8 @@ from django.conf.urls import patterns, url from .views import (SetupWorkflowCreateView, SetupWorkflowDeleteView, SetupWorkflowEditView, SetupWorkflowListView, SetupWorkflowStateListView, SetupWorkflowStateCreateView, - SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView) + SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView, + DocumentWorkflowListView) urlpatterns = patterns('', url(r'^setup/all/$', SetupWorkflowListView.as_view(), name='setup_workflow_list'), @@ -15,4 +16,12 @@ urlpatterns = patterns('', url(r'^setup/(?P\d+)/transitions/$', SetupWorkflowTransitionListView.as_view(), name='setup_workflow_transitions'), url(r'^setup/(?P\d+)/transitions/create/$', SetupWorkflowTransitionCreateView.as_view(), name='setup_workflow_transitions_create'), + + url(r'^document/(?P\d+)/workflows/$', DocumentWorkflowListView.as_view(), name='document_workflow_list'), ) + +urlpatterns += patterns('document_states.views', + url(r'^setup/(?P\d+)/document_types/$', 'setup_workflow_document_types', name='setup_workflow_document_types'), +) + + diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index 9a6f2aad6a..7e03e05433 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -12,13 +12,47 @@ from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _, ungettext from acls.models import AccessEntry +from common.utils import encapsulate, generate_choices_w_labels 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 .forms import WorkflowStateForm, WorkflowTransitionForm 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): @@ -185,3 +219,25 @@ class SetupWorkflowTransitionCreateView(SingleObjectCreateView): return super(SetupWorkflowTransitionCreateView, self).form_invalid(form) else: 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, + } + )