diff --git a/docs/releases/2.2.rst b/docs/releases/2.2.rst index d7e6895f72..62d15f9213 100644 --- a/docs/releases/2.2.rst +++ b/docs/releases/2.2.rst @@ -120,6 +120,8 @@ Other changes controlled by the COMMON_TEST_FILE_HANDLES and COMMON_TEST_FILE_HANDLES settings. - Add tool to launch all workflows. GitLab issue #355 +- New workflow view that lists documents currently executing a workflow and + documents by their specific current workflow state. Removals -------- diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index e2d2b55aef..b4c1a01c21 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -7,8 +7,8 @@ from django.utils.translation import ugettext_lazy as _ from kombu import Exchange, Queue from common import ( - MayanAppConfig, menu_facet, menu_object, menu_secondary, menu_setup, - menu_sidebar, menu_tools + MayanAppConfig, menu_facet, menu_main, menu_object, menu_secondary, + menu_setup, menu_sidebar, menu_tools ) from common.widgets import two_state_template from mayan.celery import app @@ -25,7 +25,9 @@ from .links import ( link_setup_workflow_transitions, link_setup_workflow_transition_create, link_setup_workflow_transition_delete, link_setup_workflow_transition_edit, link_tool_launch_all_workflows, link_workflow_instance_detail, - link_workflow_instance_transition + link_workflow_instance_transition, link_workflow_document_list, + link_workflow_list, link_workflow_state_document_list, + link_workflow_state_list ) @@ -47,7 +49,9 @@ class DocumentStatesApp(MayanAppConfig): Workflow = self.get_model('Workflow') WorkflowInstance = self.get_model('WorkflowInstance') WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry') + WorkflowRuntimeProxy = self.get_model('WorkflowRuntimeProxy') WorkflowState = self.get_model('WorkflowState') + WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy') WorkflowTransition = self.get_model('WorkflowTransition') SourceColumn( @@ -135,6 +139,7 @@ class DocumentStatesApp(MayanAppConfig): menu_facet.bind_links( links=(link_document_workflow_instance_list,), sources=(Document,) ) + menu_main.bind_links(links=(link_workflow_list,), position=10) menu_object.bind_links( links=( link_setup_workflow_states, link_setup_workflow_transitions, @@ -160,6 +165,16 @@ class DocumentStatesApp(MayanAppConfig): link_workflow_instance_transition ), sources=(WorkflowInstance,) ) + menu_object.bind_links( + links=( + link_workflow_document_list, link_workflow_state_list, + ), sources=(WorkflowRuntimeProxy,) + ) + menu_object.bind_links( + links=( + link_workflow_state_document_list, + ), sources=(WorkflowStateRuntimeProxy,) + ) menu_secondary.bind_links( links=(link_setup_workflow_list, link_setup_workflow_create), sources=( @@ -167,6 +182,12 @@ class DocumentStatesApp(MayanAppConfig): 'document_states:setup_workflow_list' ) ) + menu_secondary.bind_links( + links=(link_workflow_list,), + sources=( + WorkflowRuntimeProxy, + ) + ) menu_setup.bind_links(links=(link_setup_workflow_list,)) menu_sidebar.bind_links( links=( diff --git a/mayan/apps/document_states/links.py b/mayan/apps/document_states/links.py index 4a3abbe7e1..fcef813012 100644 --- a/mayan/apps/document_states/links.py +++ b/mayan/apps/document_states/links.py @@ -86,3 +86,21 @@ link_workflow_instance_transition = Link( view='document_states:workflow_instance_transition', args='resolved_object.pk' ) +link_workflow_document_list = Link( + permissions=(permission_workflow_view,), text=_('Workflow documents'), + view='document_states:workflow_document_list', args='resolved_object.pk' +) +link_workflow_list = Link( + permissions=(permission_workflow_view,), icon='fa fa-sitemap', + text=_('Workflows'), view='document_states:workflow_list' +) +link_workflow_state_document_list = Link( + permissions=(permission_workflow_view,), + text=_('State documents'), view='document_states:workflow_state_document_list', + args='resolved_object.pk' +) +link_workflow_state_list = Link( + permissions=(permission_workflow_view,), + text=_('States'), view='document_states:workflow_state_list', + args='resolved_object.pk' +) diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 203b29574f..41fcd55907 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -56,6 +56,7 @@ class Workflow(models.Model): ) class Meta: + ordering = ('label',) verbose_name = _('Workflow') verbose_name_plural = _('Workflows') @@ -89,6 +90,7 @@ class WorkflowState(models.Model): return super(WorkflowState, self).save(*args, **kwargs) class Meta: + ordering = ('label',) unique_together = ('workflow', 'label') verbose_name = _('Workflow state') verbose_name_plural = _('Workflow states') @@ -114,6 +116,7 @@ class WorkflowTransition(models.Model): return self.label class Meta: + ordering = ('label',) unique_together = ( 'workflow', 'label', 'origin_state', 'destination_state' ) @@ -211,3 +214,17 @@ class WorkflowInstanceLogEntry(models.Model): def clean(self): if self.transition not in self.workflow_instance.get_transition_choices(): raise ValidationError(_('Not a valid transition choice.')) + + +class WorkflowRuntimeProxy(Workflow): + class Meta: + proxy = True + verbose_name = _('Workflow runtime proxy') + verbose_name_plural = _('Workflow runtime proxies') + + +class WorkflowStateRuntimeProxy(WorkflowState): + class Meta: + proxy = True + verbose_name = _('Workflow state runtime proxy') + verbose_name_plural = _('Workflow state runtime proxies') diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index 8b54a58748..e6217a66cf 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -18,7 +18,8 @@ from .views import ( SetupWorkflowTransitionListView, SetupWorkflowTransitionCreateView, SetupWorkflowTransitionDeleteView, SetupWorkflowTransitionEditView, ToolLaunchAllWorkflows, WorkflowDocumentListView, - WorkflowInstanceDetailView, WorkflowInstanceTransitionView + WorkflowInstanceDetailView, WorkflowInstanceTransitionView, + WorkflowListView, WorkflowStateDocumentListView, WorkflowStateListView ) urlpatterns = [ @@ -107,6 +108,27 @@ urlpatterns = [ ToolLaunchAllWorkflows.as_view(), name='tool_launch_all_workflows' ), + url( + r'all/$', + WorkflowListView.as_view(), + name='workflow_list' + ), + url( + r'^(?P\d+)/documents/$', + WorkflowDocumentListView.as_view(), + name='workflow_document_list' + ), + + url( + r'^(?P\d+)/states/$', + WorkflowStateListView.as_view(), + name='workflow_state_list' + ), + url( + r'^state/(?P\d+)/documents/$', + WorkflowStateDocumentListView.as_view(), + name='workflow_state_document_list' + ), ] api_urls = [ diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index d951a25298..d343eabd90 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages from django.core.urlresolvers import reverse, reverse_lazy +from django.db.models import F, Max from django.db.utils import IntegrityError from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -20,7 +21,10 @@ from .forms import ( WorkflowForm, WorkflowInstanceTransitionForm, WorkflowStateForm, WorkflowTransitionForm ) -from .models import Workflow, WorkflowInstance, WorkflowState, WorkflowTransition +from .models import ( + Workflow, WorkflowInstance, WorkflowInstanceLogEntry, WorkflowState, + WorkflowTransition, WorkflowRuntimeProxy, WorkflowStateRuntimeProxy +) from .permissions import ( permission_workflow_create, permission_workflow_delete, permission_workflow_edit, permission_workflow_transition, @@ -56,36 +60,10 @@ class DocumentWorkflowInstanceListView(SingleObjectListView): return self.get_document().workflows.all() -class WorkflowDocumentListView(DocumentListView): - def dispatch(self, request, *args, **kwargs): - self.workflow = get_object_or_404(Workflow, pk=self.kwargs['pk']) - - AccessControlList.objects.check_access( - permissions=permission_workflow_view, user=request.user, - obj=self.workflow - ) - - return super( - WorkflowDocumentListView, self - ).dispatch(request, *args, **kwargs) - - def get_document_queryset(self): - return Document.objects.filter( - document_type__in=self.workflow.document_types.all() - ) - - def get_extra_context(self): - return { - 'hide_links': True, - 'object': self.workflow, - 'title': _('Documents with the workflow: %s') % self.workflow - } - - class WorkflowInstanceDetailView(SingleObjectListView): def dispatch(self, request, *args, **kwargs): AccessControlList.objects.check_access( - permissions=permission_workflow_view, users=request.user, + permissions=permission_workflow_view, user=request.user, obj=self.get_workflow_instance().document ) @@ -432,6 +410,121 @@ class SetupWorkflowTransitionEditView(SingleObjectEditView): ) +class WorkflowListView(SingleObjectListView): + view_permission = permission_workflow_view + + def get_queryset(self): + return WorkflowRuntimeProxy.objects.all() + + def get_extra_context(self): + return { + 'hide_link': True, + 'title': _('Workflows') + } + + +class WorkflowDocumentListView(DocumentListView): + def dispatch(self, request, *args, **kwargs): + self.workflow = get_object_or_404( + WorkflowRuntimeProxy, pk=self.kwargs['pk'] + ) + + AccessControlList.objects.check_access( + permissions=permission_workflow_view, user=request.user, + obj=self.workflow + ) + + return super( + WorkflowDocumentListView, self + ).dispatch(request, *args, **kwargs) + + def get_document_queryset(self): + return Document.objects.filter(workflows__workflow=self.workflow) + + def get_extra_context(self): + return { + 'hide_links': True, + 'object': self.workflow, + 'title': _('Documents with the workflow: %s') % self.workflow + } + + +class WorkflowStateDocumentListView(DocumentListView): + def dispatch(self, request, *args, **kwargs): + self.workflow_state = get_object_or_404( + WorkflowStateRuntimeProxy, pk=self.kwargs['pk'] + ) + + AccessControlList.objects.check_access( + permissions=permission_workflow_view, user=request.user, + obj=self.workflow_state.workflow + ) + + return super( + WorkflowStateDocumentListView, self + ).dispatch(request, *args, **kwargs) + + def get_document_queryset(self): + latest_entries = WorkflowInstanceLogEntry.objects.annotate( + max_datetime=Max( + 'workflow_instance__log_entries__datetime' + ) + ).filter( + datetime=F('max_datetime') + ) + + state_latest_entries = latest_entries.filter( + transition__destination_state=self.workflow_state + ) + + return Document.objects.filter( + workflows__pk__in=state_latest_entries.values_list( + 'workflow_instance', flat=True + ) + ) + + def get_extra_context(self): + return { + 'hide_links': True, + 'object': self.workflow_state, + 'navigation_object_list': ('object', 'workflow'), + 'workflow': WorkflowRuntimeProxy.objects.get( + pk=self.workflow_state.workflow.pk + ), + 'title': _( + 'Documents in the workflow "%s", state "%s"' + ) % (self.workflow_state.workflow, self.workflow_state) + } + + +class WorkflowStateListView(SingleObjectListView): + def dispatch(self, request, *args, **kwargs): + AccessControlList.objects.check_access( + permissions=permission_workflow_view, user=request.user, + obj=self.get_workflow() + ) + + return super( + WorkflowStateListView, self + ).dispatch(request, *args, **kwargs) + + def get_extra_context(self): + return { + 'hide_columns': True, + 'hide_link': True, + 'object': self.get_workflow(), + 'title': _('States of workflow: %s') % self.get_workflow() + } + + def get_queryset(self): + return WorkflowStateRuntimeProxy.objects.filter( + workflow=self.get_workflow() + ) + + def get_workflow(self): + return get_object_or_404(WorkflowRuntimeProxy, pk=self.kwargs['pk']) + + class ToolLaunchAllWorkflows(ConfirmView): extra_context = { 'title': _('Launch all workflows?')