diff --git a/HISTORY.rst b/HISTORY.rst index f3e88a527d..6626624bd3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,10 +6,12 @@ FUSE index mirror. GitLab issue #385 - Add support for check for the latest released version of Mayan from the About menu. +- Support for rebuilding specific indexes. GitLab issue #372. - Rewrite document indexing code to be faster and use less locking. - Use a predefined file path for the file lock. - Catch documents with not document version when displaying their thumbnails. - Document page navigation fix when using Mayan as a sub URL app. +- Add support for indexing on workflow state changes. 2.2 (2017-04-26) ================ diff --git a/docs/releases/2.3.rst b/docs/releases/2.3.rst index 2f545f7eb3..1a0f38f059 100644 --- a/docs/releases/2.3.rst +++ b/docs/releases/2.3.rst @@ -18,10 +18,10 @@ Changes will select the adjacent checkbox. - Support for passing the FUSE option `allow-other` and `allow-root` was added to the index mirroring management command. -- Add support for check for the latest released version of Mayan from the +- Added support for checking for the latest released version of Mayan from the About menu. -- Add support for rebuilding specific indexes instead of only being able to - rebuild all index. +- Added support for rebuilding specific indexes instead of only being able to + rebuild all index. GitLab issue #372. - Rewrite document indexing code to be faster and use less locking. Thanks to Macrobb Simpson (@Macrobb) for the initial implementation. - Use a predefined file path for the file lock. @@ -30,7 +30,14 @@ Changes document page navigation views. Fixes an issue when Mayan is installed as a sub URL app. Thanks to Gustavo Teixeira(@gsteixei) for the issue and investigation. - +- Support was added to update document indexes after workflow state changes. +- An helper was added to access a documents workflow by name. To this end + a new field was added to the Workflow class called `Internal name`. + This new field makes it much easier to get a document's workflow instance. + If for example a document has a workflow called `Publish` with the internal + name `publish_workflow`, it will be accessible in the indexing template as + {{ document.workflow.publish_workflow }}. The latest state of the workflow + can be accessed using {{ document.workflow.publish_workflow.get_current_state }}. Removals -------- @@ -86,6 +93,7 @@ Bugs fixed or issues closed =========================== * `GitLab issue #371 `_ Auto select checkbox when updating metadata +* `GitLab issue #372 `_ (Feature request) Allow 'rebuild index' to rebuild only a selected index * `GitLab issue #383 `_ Page not found when deployed to sub-uri * `GitLab issue #385 `_ mountindex: how to specify FUSE mount option allow_other? diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py index 4fb01d844a..03ed9f5331 100644 --- a/mayan/apps/common/classes.py +++ b/mayan/apps/common/classes.py @@ -103,7 +103,7 @@ class ModelAttribute(object): ) return ' '.join( - [ugettext('Available attributes: '), ', '.join(result)] + [ugettext('Available attributes: \n'), ', \n'.join(result)] ) def get_display(self, show_name=False): @@ -208,3 +208,28 @@ class Package(object): self.label = label self.license_text = license_text self.__class__._registry.append(self) + + +class PropertyHelper(object): + """ + Makes adding fields using __class__.add_to_class easier. + Each subclass must implement the `constructor` and the `get_result` + method. + """ + @staticmethod + @property + def constructor(source_object): + return PropertyHelper(source_object) + + def __init__(self, instance): + self.instance = instance + + def __getattr__(self, name): + return self.get_result(name=name) + + def get_result(self, name): + """ + The method that produces the actual result. Must be implemented + by each subclass. + """ + raise NotImplementedError diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 161134eb19..4a78cdc6ef 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -4,8 +4,16 @@ import glob import os from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False): import psutil +from permissions.models import Role +from permissions.tests.literals import TEST_ROLE_LABEL +from user_management.tests import ( + TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, + TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD +) from ..settings import setting_temporary_directory @@ -103,3 +111,22 @@ class TempfileCheckMixin(object): ) ) super(TempfileCheckMixin, self).tearDown() + + +class UserMixin(object): + def setUp(self): + super(UserMixin, self).setUp() + self.admin_user = get_user_model().objects.create_superuser( + username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, + password=TEST_ADMIN_PASSWORD + ) + + self.user = get_user_model().objects.create_user( + username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, + password=TEST_USER_PASSWORD + ) + + self.group = Group.objects.create(name=TEST_GROUP_NAME) + self.role = Role.objects.create(label=TEST_ROLE_LABEL) + self.group.user_set.add(self.user) + self.role.groups.add(self.group) diff --git a/mayan/apps/common/validators.py b/mayan/apps/common/validators.py new file mode 100644 index 0000000000..84ff982615 --- /dev/null +++ b/mayan/apps/common/validators.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals + +import re + +from django.core.validators import RegexValidator +from django.utils import six +from django.utils.functional import SimpleLazyObject +from django.utils.translation import ugettext_lazy as _ + +# These values, if given to validate(), will trigger the self.required check. +EMPTY_VALUES = (None, '', [], (), {}) + + +def _lazy_re_compile(regex, flags=0): + """Lazily compile a regex with flags.""" + def _compile(): + # Compile the regex if it was not passed pre-compiled. + if isinstance(regex, six.string_types): + return re.compile(regex, flags) + else: + assert not flags, 'flags must be empty if regex is passed pre-compiled' + return regex + return SimpleLazyObject(_compile) + + +internal_name_re = _lazy_re_compile(r'^[a-zA-Z0-9_]+\Z') +validate_internal_name = RegexValidator( + internal_name_re, _( + "Enter a valid 'internal name' consisting of letters, numbers, and " + "underscores." + ), 'invalid' +) diff --git a/mayan/apps/document_indexing/apps.py b/mayan/apps/document_indexing/apps.py index 9b11b00813..278e6dd403 100644 --- a/mayan/apps/document_indexing/apps.py +++ b/mayan/apps/document_indexing/apps.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from kombu import Exchange, Queue from django.apps import apps -from django.db.models.signals import post_save, post_delete, pre_delete +from django.db.models.signals import post_delete, pre_delete from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission @@ -21,8 +21,7 @@ from navigation import SourceColumn from rest_api.classes import APIEndPoint from .handlers import ( - create_default_document_index, document_metadata_index_update, - document_metadata_index_post_delete, handler_delete_empty, + create_default_document_index, handler_delete_empty, handler_index_document, handler_remove_document ) from .links import ( @@ -59,10 +58,6 @@ class DocumentIndexingApp(MayanAppConfig): app_label='documents', model_name='DocumentType' ) - DocumentMetadata = apps.get_model( - app_label='metadata', model_name='DocumentMetadata' - ) - DocumentIndexInstanceNode = self.get_model('DocumentIndexInstanceNode') Index = self.get_model('Index') @@ -195,11 +190,6 @@ class DocumentIndexingApp(MayanAppConfig): handler_remove_document, dispatch_uid='handler_remove_document', sender=Document ) - post_delete.connect( - document_metadata_index_post_delete, - dispatch_uid='document_metadata_index_post_delete', - sender=DocumentMetadata - ) post_document_created.connect( handler_index_document, dispatch_uid='handler_index_document', sender=Document @@ -208,8 +198,3 @@ class DocumentIndexingApp(MayanAppConfig): create_default_document_index, dispatch_uid='create_default_document_index', sender=DocumentType ) - post_save.connect( - document_metadata_index_update, - dispatch_uid='document_metadata_index_update', - sender=DocumentMetadata - ) diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py index bc51aecf05..8bdf0dd1b7 100644 --- a/mayan/apps/document_indexing/forms.py +++ b/mayan/apps/document_indexing/forms.py @@ -28,7 +28,9 @@ class IndexTemplateNodeForm(forms.ModelForm): self.fields['expression'].help_text = ' '.join( [ unicode(self.fields['expression'].help_text), - ModelAttribute.help_text_for(Document, type_names=['indexing']) + ModelAttribute.help_text_for( + Document, type_names=['indexing'] + ).replace('\n', '
') ] ) diff --git a/mayan/apps/document_indexing/handlers.py b/mayan/apps/document_indexing/handlers.py index b3b327f180..d0e185cf64 100644 --- a/mayan/apps/document_indexing/handlers.py +++ b/mayan/apps/document_indexing/handlers.py @@ -47,15 +47,3 @@ def handler_remove_document(sender, **kwargs): task_remove_document.apply_async( kwargs=dict(document_id=kwargs['instance'].pk) ) - - -def document_metadata_index_update(sender, **kwargs): - task_index_document.apply_async( - kwargs=dict(document_id=kwargs['instance'].document.pk) - ) - - -def document_metadata_index_post_delete(sender, **kwargs): - task_index_document.apply_async( - kwargs=dict(document_id=kwargs['instance'].document.pk) - ) diff --git a/mayan/apps/document_indexing/migrations/0012_auto_20170530_0728.py b/mayan/apps/document_indexing/migrations/0012_auto_20170530_0728.py new file mode 100644 index 0000000000..e07de36b16 --- /dev/null +++ b/mayan/apps/document_indexing/migrations/0012_auto_20170530_0728.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-30 07:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('document_indexing', '0011_auto_20170524_0456'), + ] + + operations = [ + migrations.AlterField( + model_name='index', + name='slug', + field=models.SlugField(help_text='This value will be used by other apps to reference this index.', max_length=128, unique=True, verbose_name='Slug'), + ), + migrations.AlterField( + model_name='indexinstancenode', + name='documents', + field=models.ManyToManyField(related_name='index_instance_nodes', to='documents.Document', verbose_name='Documents'), + ), + migrations.AlterField( + model_name='indexinstancenode', + name='index_template_node', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='index_instance_nodes', to='document_indexing.IndexTemplateNode', verbose_name='Index template node'), + ), + ] diff --git a/mayan/apps/document_indexing/models.py b/mayan/apps/document_indexing/models.py index 366f8e6f78..1fa15ef6b8 100644 --- a/mayan/apps/document_indexing/models.py +++ b/mayan/apps/document_indexing/models.py @@ -34,7 +34,7 @@ class Index(models.Model): ) slug = models.SlugField( help_text=_( - 'This values will be used by other apps to reference this index.' + 'This value will be used by other apps to reference this index.' ), max_length=128, unique=True, verbose_name=_('Slug') ) enabled = models.BooleanField( diff --git a/mayan/apps/document_states/admin.py b/mayan/apps/document_states/admin.py index 9243c1cc65..6bdac355b2 100644 --- a/mayan/apps/document_states/admin.py +++ b/mayan/apps/document_states/admin.py @@ -30,7 +30,7 @@ class WorkflowAdmin(admin.ModelAdmin): filter_horizontal = ('document_types',) inlines = (WorkflowStateInline, WorkflowTransitionInline) - list_display = ('label', 'document_types_list') + list_display = ('label', 'internal_name', 'document_types_list') @admin.register(WorkflowInstance) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index c7fdaf1ebb..16bb920e92 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.apps import apps -from django.db.models.signals import post_save +from django.db.models.signals import post_delete, post_save from django.utils.translation import ugettext_lazy as _ from kombu import Exchange, Queue @@ -12,12 +12,14 @@ from common import ( MayanAppConfig, menu_facet, menu_main, menu_object, menu_secondary, menu_setup, menu_sidebar, menu_tools ) +from common.classes import ModelAttribute from common.widgets import two_state_template from mayan.celery import app from navigation import SourceColumn from rest_api.classes import APIEndPoint -from .handlers import launch_workflow +from .classes import DocumentStateHelper +from .handlers import handler_index_document, launch_workflow from .links import ( link_document_workflow_instance_list, link_setup_workflow_document_types, link_setup_workflow_create, link_setup_workflow_delete, @@ -49,6 +51,10 @@ class DocumentStatesApp(MayanAppConfig): app_label='documents', model_name='Document' ) + Document.add_to_class( + 'workflow', DocumentStateHelper.constructor + ) + Workflow = self.get_model('Workflow') WorkflowInstance = self.get_model('WorkflowInstance') WorkflowInstanceLogEntry = self.get_model('WorkflowInstanceLogEntry') @@ -57,6 +63,21 @@ class DocumentStatesApp(MayanAppConfig): WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy') WorkflowTransition = self.get_model('WorkflowTransition') + ModelAttribute( + Document, 'workflow.< workflow internal name >.get_current_state', + label=_('Current state of a workflow'), description=_( + 'Return the current state of the selected workflow' + ), type_name=['property', 'indexing'] + ) + ModelAttribute( + Document, + 'workflow.< workflow internal name >.get_current_state.completion', + label=_('Current state of a workflow'), description=_( + 'Return the completion value of the current state of the ' + 'selected workflow' + ), type_name=['property', 'indexing'] + ) + ModelPermission.register( model=Workflow, permissions=(permission_workflow_transition,) ) @@ -66,6 +87,13 @@ class DocumentStatesApp(MayanAppConfig): permissions=(permission_workflow_transition,) ) + SourceColumn( + source=Workflow, label=_('Label'), attribute='label' + ) + SourceColumn( + source=Workflow, label=_('Internal name'), + attribute='internal_name' + ) SourceColumn( source=Workflow, label=_('Initial state'), func=lambda context: context['object'].get_initial_state() or _('None') @@ -212,3 +240,16 @@ class DocumentStatesApp(MayanAppConfig): post_save.connect( launch_workflow, dispatch_uid='launch_workflow', sender=Document ) + + # Index updating + + post_delete.connect( + handler_index_document, + dispatch_uid='handler_index_document_delete', + sender=WorkflowInstanceLogEntry + ) + post_save.connect( + handler_index_document, + dispatch_uid='handler_index_document_save', + sender=WorkflowInstanceLogEntry + ) diff --git a/mayan/apps/document_states/classes.py b/mayan/apps/document_states/classes.py new file mode 100644 index 0000000000..cba92b76e0 --- /dev/null +++ b/mayan/apps/document_states/classes.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals + +from common.classes import PropertyHelper + + +class DocumentStateHelper(PropertyHelper): + @staticmethod + @property + def constructor(*args, **kwargs): + return DocumentStateHelper(*args, **kwargs) + + def get_result(self, name): + return self.instance.workflows.get(workflow__internal_name=name) diff --git a/mayan/apps/document_states/forms.py b/mayan/apps/document_states/forms.py index df288b255b..ca8612f170 100644 --- a/mayan/apps/document_states/forms.py +++ b/mayan/apps/document_states/forms.py @@ -8,7 +8,7 @@ from .models import Workflow, WorkflowState, WorkflowTransition class WorkflowForm(forms.ModelForm): class Meta: - fields = ('label',) + fields = ('label', 'internal_name') model = Workflow diff --git a/mayan/apps/document_states/handlers.py b/mayan/apps/document_states/handlers.py index 293d6a859c..fcdef979cf 100644 --- a/mayan/apps/document_states/handlers.py +++ b/mayan/apps/document_states/handlers.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals from django.apps import apps +from document_indexing.tasks import task_index_document + def launch_workflow(sender, instance, created, **kwargs): Workflow = apps.get_model( @@ -10,3 +12,11 @@ def launch_workflow(sender, instance, created, **kwargs): if created: Workflow.objects.launch_for(instance) + + +def handler_index_document(sender, **kwargs): + task_index_document.apply_async( + kwargs=dict( + document_id=kwargs['instance'].workflow_instance.document.pk + ) + ) diff --git a/mayan/apps/document_states/migrations/0004_workflow_internal_name.py b/mayan/apps/document_states/migrations/0004_workflow_internal_name.py new file mode 100644 index 0000000000..837b34bac1 --- /dev/null +++ b/mayan/apps/document_states/migrations/0004_workflow_internal_name.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-06-03 18:26 +from __future__ import unicode_literals + +from django.db import migrations, models +from django.utils.text import slugify + +from common.validators import validate_internal_name + + +def generate_internal_name(apps, schema_editor): + Workflow = apps.get_model('document_states', 'Workflow') + internal_names = [] + + for workflow in Workflow.objects.all(): + # Slugify and replace dashes (not allowed) by underscores + workflow.internal_name = slugify(workflow.label).replace('-', '_') + if workflow.internal_name in internal_names: + # Add a suffix in case two conversions yield the same + # result. + workflow.internal_name = '{}_'.format( + workflow.internal_name + ) + + internal_names.append(workflow.internal_name) + workflow.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('document_states', '0003_auto_20170325_0447'), + ] + + operations = [ + # Add the internal name field but make it non unique + # https://docs.djangoproject.com/en/1.10/howto/ + # writing-migrations/#migrations-that-add-unique-fields + migrations.AddField( + model_name='workflow', + name='internal_name', + field=models.CharField( + db_index=True, default=' ', + help_text='This value will be used by other apps to reference ' + 'this workflow. Can only contain letters, numbers, and ' + 'underscores.', max_length=255, unique=False, validators=[ + validate_internal_name + ], verbose_name='Internal name' + ), + ), + + # Generate the slugs based on the labels + migrations.RunPython( + generate_internal_name, reverse_code=migrations.RunPython.noop + ), + + # Make the internal name field unique + # Add the internal name field but make it non unique + # https://docs.djangoproject.com/en/1.10/howto/ + # writing-migrations/#migrations-that-add-unique-fields + migrations.AlterField( + model_name='workflow', + name='internal_name', + field=models.CharField( + db_index=True, + help_text='This value will be used by other apps to reference ' + 'this workflow. Can only contain letters, numbers, and ' + 'underscores.', max_length=255, unique=True, validators=[ + validate_internal_name + ], verbose_name='Internal name' + ), + ), + ] diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index d58ebb527c..6984cb15d9 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -10,6 +10,7 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from acls.models import AccessControlList +from common.validators import validate_internal_name from documents.models import Document, DocumentType from permissions import Permission @@ -25,7 +26,13 @@ class Workflow(models.Model): Fields: * label - Identifier. A name/label to call the workflow """ - + internal_name = models.CharField( + db_index=True, help_text=_( + 'This value will be used by other apps to reference this ' + 'workflow. Can only contain letters, numbers, and underscores.' + ), max_length=255, unique=True, validators=[validate_internal_name], + verbose_name=_('Internal name') + ) label = models.CharField( max_length=255, unique=True, verbose_name=_('Label') ) @@ -159,11 +166,11 @@ class WorkflowInstance(models.Model): 'document_states:workflow_instance_detail', args=(str(self.pk),) ) - def do_transition(self, comment, transition, user): + def do_transition(self, transition, user, comment=None): try: if transition in self.get_current_state().origin_transitions.all(): self.log_entries.create( - comment=comment, transition=transition, user=user + comment=comment or '', transition=transition, user=user ) except AttributeError: # No initial state has been set for this workflow diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 3a0daf4ab2..4739bfe714 100644 --- a/mayan/apps/document_states/serializers.py +++ b/mayan/apps/document_states/serializers.py @@ -182,8 +182,8 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer): 'url': {'view_name': 'rest_api:workflow-detail'}, } fields = ( - 'document_types_url', 'id', 'label', 'states', 'transitions', - 'url' + 'document_types_url', 'id', 'internal_name', 'label', 'states', + 'transitions', 'url' ) model = Workflow diff --git a/mayan/apps/document_states/tasks.py b/mayan/apps/document_states/tasks.py index ec550c27b3..2156a45ead 100644 --- a/mayan/apps/document_states/tasks.py +++ b/mayan/apps/document_states/tasks.py @@ -18,7 +18,7 @@ def task_launch_all_workflows(): logger.info('Start launching workflows') for document in Document.objects.all(): - print 'document :', document + logger.debug('Lauching workflows for document ID: %d', document.pk) Workflow.objects.launch_for(document=document) logger.info('Finished launching workflows') diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index 004d17de8d..212427d2d3 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -1,6 +1,9 @@ from __future__ import unicode_literals +TEST_INDEX_LABEL = 'test workflow index' + TEST_WORKFLOW_LABEL = 'test workflow label' +TEST_WORKFLOW_INTERNAL_NAME = 'test_workflow_label' TEST_WORKFLOW_LABEL_EDITED = 'test workflow label edited' TEST_WORKFLOW_INITIAL_STATE_LABEL = 'test initial state' TEST_WORKFLOW_INITIAL_STATE_COMPLETION = 33 @@ -11,3 +14,7 @@ TEST_WORKFLOW_STATE_COMPLETION = 66 TEST_WORKFLOW_TRANSITION_LABEL = 'test transition label' TEST_WORKFLOW_TRANSITION_LABEL_2 = 'test transition label 2' TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited' + +TEST_INDEX_TEMPLATE_METADATA_EXPRESSION = '{{{{ document.workflow.{}.get_current_state }}}}'.format( + TEST_WORKFLOW_INTERNAL_NAME +) diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index a29dfe8c7e..84ae9b930c 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -25,9 +25,10 @@ from ..models import Workflow from ..permissions import permission_workflow_transition from .literals import ( - TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED, - TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_INITIAL_STATE_LABEL, - TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT, TEST_WORKFLOW_STATE_COMPLETION, + TEST_WORKFLOW_INTERNAL_NAME, TEST_WORKFLOW_INITIAL_STATE_COMPLETION, + TEST_WORKFLOW_INITIAL_STATE_LABEL, + TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT, TEST_WORKFLOW_LABEL, + TEST_WORKFLOW_LABEL_EDITED, TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL, TEST_WORKFLOW_STATE_LABEL_EDITED, TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED ) @@ -61,7 +62,10 @@ class WorkflowAPITestCase(BaseAPITestCase): super(WorkflowAPITestCase, self).tearDown() def _create_workflow(self): - return Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + return Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) def test_workflow_create_view(self): response = self.client.post( @@ -239,7 +243,10 @@ class WorkflowStatesAPITestCase(BaseAPITestCase): super(WorkflowStatesAPITestCase, self).tearDown() def _create_workflow(self): - self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) def _create_workflow_state(self): self._create_workflow() @@ -371,7 +378,10 @@ class WorkflowTransitionsAPITestCase(BaseAPITestCase): super(WorkflowTransitionsAPITestCase, self).tearDown() def _create_workflow(self): - self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) def _create_workflow_states(self): self._create_workflow() @@ -545,7 +555,10 @@ class DocumentWorkflowsAPITestCase(BaseAPITestCase): ) def _create_workflow(self): - self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) self.workflow.document_types.add(self.document_type) def _create_workflow_states(self): @@ -678,7 +691,10 @@ class DocumentWorkflowsTransitionACLsAPITestCase(APITestCase): ) def _create_workflow(self): - self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) self.workflow.document_types.add(self.document_type) def _create_workflow_states(self): diff --git a/mayan/apps/document_states/tests/test_models.py b/mayan/apps/document_states/tests/test_models.py new file mode 100644 index 0000000000..f753c0a755 --- /dev/null +++ b/mayan/apps/document_states/tests/test_models.py @@ -0,0 +1,125 @@ +from __future__ import unicode_literals + +from django.test import override_settings + +from common.tests import BaseTestCase +from common.tests.mixins import UserMixin +from documents.models import DocumentType +from documents.tests import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE +from document_indexing.models import Index, IndexInstanceNode + +from ..models import Workflow + +from .literals import ( + TEST_INDEX_LABEL, TEST_INDEX_TEMPLATE_METADATA_EXPRESSION, + TEST_WORKFLOW_INTERNAL_NAME, TEST_WORKFLOW_INITIAL_STATE_LABEL, + TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_LABEL, + TEST_WORKFLOW_STATE_LABEL, TEST_WORKFLOW_STATE_COMPLETION, + TEST_WORKFLOW_TRANSITION_LABEL +) + + +@override_settings(OCR_AUTO_OCR=False) +class DocumentStateIndexingTestCase(UserMixin, BaseTestCase): + def tearDown(self): + self.document_type.delete() + super(DocumentStateIndexingTestCase, self).tearDown() + + def _create_document_type(self): + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + def _create_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object + ) + + def _create_workflow(self): + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) + self.workflow.document_types.add(self.document_type) + + def _create_workflow_states(self): + self._create_workflow() + self.workflow_state_1 = self.workflow.states.create( + completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, + initial=True, label=TEST_WORKFLOW_INITIAL_STATE_LABEL + ) + self.workflow_state_2 = self.workflow.states.create( + completion=TEST_WORKFLOW_STATE_COMPLETION, + label=TEST_WORKFLOW_STATE_LABEL + ) + + def _create_workflow_transition(self): + self._create_workflow_states() + self.workflow_transition = self.workflow.transitions.create( + label=TEST_WORKFLOW_TRANSITION_LABEL, + origin_state=self.workflow_state_1, + destination_state=self.workflow_state_2, + ) + + def _create_index(self): + # Create empty index + index = Index.objects.create(label=TEST_INDEX_LABEL) + + # Add our document type to the new index + index.document_types.add(self.document_type) + + # Create simple index template + root = index.template_root + index.node_templates.create( + parent=root, expression=TEST_INDEX_TEMPLATE_METADATA_EXPRESSION, + link_documents=True + ) + + def test_workflow_indexing_initial_state(self): + self._create_document_type() + self._create_workflow_transition() + self._create_index() + self._create_document() + + self.assertEqual( + list( + IndexInstanceNode.objects.values_list('value', flat=True) + ), ['', TEST_WORKFLOW_INITIAL_STATE_LABEL] + ) + + def test_workflow_indexing_transition(self): + self._create_document_type() + self._create_workflow_transition() + self._create_index() + self._create_document() + + self.document.workflows.first().do_transition( + transition=self.workflow_transition, + user=self.admin_user + ) + + self.assertEqual( + list( + IndexInstanceNode.objects.values_list('value', flat=True) + ), ['', TEST_WORKFLOW_STATE_LABEL] + ) + + def test_workflow_indexing_document_delete(self): + self._create_document_type() + self._create_workflow_transition() + self._create_index() + self._create_document() + + self.document.workflows.first().do_transition( + transition=self.workflow_transition, + user=self.admin_user + ) + + self.document.delete(to_trash=False) + + self.assertEqual( + list( + IndexInstanceNode.objects.values_list('value', flat=True) + ), [''] + ) diff --git a/mayan/apps/document_states/tests/test_views.py b/mayan/apps/document_states/tests/test_views.py index a2a02c24e3..04fdac14bc 100644 --- a/mayan/apps/document_states/tests/test_views.py +++ b/mayan/apps/document_states/tests/test_views.py @@ -11,10 +11,10 @@ from ..permissions import ( ) from .literals import ( - TEST_WORKFLOW_LABEL, TEST_WORKFLOW_INITIAL_STATE_LABEL, - TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL, - TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_TRANSITION_LABEL, - TEST_WORKFLOW_TRANSITION_LABEL_2 + TEST_WORKFLOW_INITIAL_STATE_LABEL, TEST_WORKFLOW_INITIAL_STATE_COMPLETION, + TEST_WORKFLOW_INTERNAL_NAME, TEST_WORKFLOW_LABEL, + TEST_WORKFLOW_STATE_LABEL, TEST_WORKFLOW_STATE_COMPLETION, + TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_2 ) @@ -25,7 +25,10 @@ class DocumentStateViewTestCase(GenericViewTestCase): self.login_admin_user() def _create_workflow(self): - self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create( + label=TEST_WORKFLOW_LABEL, + internal_name=TEST_WORKFLOW_INTERNAL_NAME + ) def _create_workflow_states(self): self.workflow_initial_state = WorkflowState.objects.create( @@ -49,6 +52,7 @@ class DocumentStateViewTestCase(GenericViewTestCase): 'document_states:setup_workflow_create', data={ 'label': TEST_WORKFLOW_LABEL, + 'internal_name': TEST_WORKFLOW_INTERNAL_NAME, }, follow=True ) diff --git a/mayan/apps/document_states/views.py b/mayan/apps/document_states/views.py index ffc0e4a86d..c77d77d538 100644 --- a/mayan/apps/document_states/views.py +++ b/mayan/apps/document_states/views.py @@ -128,7 +128,7 @@ class WorkflowInstanceTransitionView(FormView): class SetupWorkflowListView(SingleObjectListView): extra_context = { 'title': _('Workflows'), - 'hide_link': True, + 'hide_object': True, } model = Workflow view_permission = permission_workflow_view diff --git a/mayan/apps/documents/tests/test_models.py b/mayan/apps/documents/tests/test_models.py index 32d4465d8f..39d88391c6 100644 --- a/mayan/apps/documents/tests/test_models.py +++ b/mayan/apps/documents/tests/test_models.py @@ -16,23 +16,25 @@ from .literals import ( @override_settings(OCR_AUTO_OCR=False) -class DocumentTestCase(BaseTestCase): +class GenericDocumentTestCase(BaseTestCase): def setUp(self): - super(DocumentTestCase, self).setUp() + super(GenericDocumentTestCase, self).setUp() self.document_type = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE ) - with open(TEST_DOCUMENT_PATH) as file_object: + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: self.document = self.document_type.new_document( - file_object=file_object, label='mayan_11_1.pdf' + file_object=file_object ) def tearDown(self): self.document_type.delete() - super(DocumentTestCase, self).tearDown() + super(GenericDocumentTestCase, self).tearDown() + +class DocumentTestCase(GenericDocumentTestCase): def test_document_creation(self): self.assertEqual(self.document_type.label, TEST_DOCUMENT_TYPE) @@ -194,23 +196,7 @@ class MultiPageTiffTestCase(BaseTestCase): self.assertEqual(self.document.page_count, 2) -@override_settings(OCR_AUTO_OCR=False) -class DocumentVersionTestCase(BaseTestCase): - def setUp(self): - super(DocumentVersionTestCase, self).setUp() - self.document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE - ) - - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - self.document = self.document_type.new_document( - file_object=file_object - ) - - def tearDown(self): - self.document_type.delete() - super(DocumentVersionTestCase, self).setUp() - +class DocumentVersionTestCase(GenericDocumentTestCase): def test_add_new_version(self): self.assertEqual(self.document.versions.count(), 1) diff --git a/mayan/apps/linking/forms.py b/mayan/apps/linking/forms.py index 9fe71956bc..dad3d424cb 100644 --- a/mayan/apps/linking/forms.py +++ b/mayan/apps/linking/forms.py @@ -17,7 +17,7 @@ class SmartLinkForm(forms.ModelForm): unicode(self.fields['dynamic_label'].help_text), ModelAttribute.help_text_for( Document, type_names=['field', 'related', 'property'] - ) + ).replace('\n', '
') ] ) @@ -39,7 +39,7 @@ class SmartLinkConditionForm(forms.ModelForm): unicode(self.fields['expression'].help_text), ModelAttribute.help_text_for( Document, type_names=['field', 'related', 'property'] - ) + ).replace('\n', '
') ] ) diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py index 4463d936d9..3f3e380d1d 100644 --- a/mayan/apps/metadata/apps.py +++ b/mayan/apps/metadata/apps.py @@ -24,7 +24,7 @@ from rest_api.classes import APIEndPoint from .classes import DocumentMetadataHelper from .handlers import ( - post_document_type_metadata_type_add, + handler_index_document, post_document_type_metadata_type_add, post_document_type_metadata_type_delete, post_post_document_type_change_metadata ) @@ -248,3 +248,16 @@ class MetadataApp(MayanAppConfig): dispatch_uid='post_document_type_metadata_type_add', sender=DocumentTypeMetadataType ) + + # Index updating + + post_delete.connect( + handler_index_document, + dispatch_uid='handler_index_document_delete', + sender=DocumentMetadata + ) + post_save.connect( + handler_index_document, + dispatch_uid='handler_index_document_save', + sender=DocumentMetadata + ) diff --git a/mayan/apps/metadata/classes.py b/mayan/apps/metadata/classes.py index 08bd9a5d03..d4a5128b0b 100644 --- a/mayan/apps/metadata/classes.py +++ b/mayan/apps/metadata/classes.py @@ -1,25 +1,16 @@ from __future__ import unicode_literals -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import ugettext_lazy as _ +from common.classes import PropertyHelper -class DocumentMetadataHelper(object): +class DocumentMetadataHelper(PropertyHelper): @staticmethod @property - def constructor(source_object): - return DocumentMetadataHelper(source_object) + def constructor(*args, **kwargs): + return DocumentMetadataHelper(*args, **kwargs) - def __init__(self, instance): - self.instance = instance - - def __getattr__(self, name): - try: - return self.instance.metadata.get(metadata_type__name=name).value - except ObjectDoesNotExist: - raise AttributeError( - _('\'metadata\' object has no attribute \'%s\'') % name - ) + def get_result(self, name): + return self.instance.metadata.get(metadata_type__name=name).value class MetadataLookup(object): diff --git a/mayan/apps/metadata/forms.py b/mayan/apps/metadata/forms.py index 38916ad583..369e8c09e2 100644 --- a/mayan/apps/metadata/forms.py +++ b/mayan/apps/metadata/forms.py @@ -16,8 +16,10 @@ class DocumentMetadataForm(forms.Form): label=_('Name'), required=False, widget=forms.TextInput(attrs={'readonly': 'readonly'}) ) - value = forms.CharField(label=_('Value'), required=False, - widget=forms.TextInput(attrs={'class': 'metadata-value'}) + value = forms.CharField( + label=_('Value'), required=False, widget=forms.TextInput( + attrs={'class': 'metadata-value'} + ) ) update = forms.BooleanField( initial=True, label=_('Update'), required=False diff --git a/mayan/apps/metadata/handlers.py b/mayan/apps/metadata/handlers.py index da408481e4..7419426d8d 100644 --- a/mayan/apps/metadata/handlers.py +++ b/mayan/apps/metadata/handlers.py @@ -4,6 +4,8 @@ from django.apps import apps import logging +from document_indexing.tasks import task_index_document + from .tasks import task_add_required_metadata_type, task_remove_metadata_type logger = logging.getLogger(__name__) @@ -49,3 +51,9 @@ def post_post_document_type_change_metadata(sender, instance, **kwargs): metadata_type=document_type_metadata_type.metadata_type, value=None ) + + +def handler_index_document(sender, **kwargs): + task_index_document.apply_async( + kwargs=dict(document_id=kwargs['instance'].document.pk) + )