Add support to update the document indexes from workflow state changes.

Add a new workflow field called internal_name for easier workflow
reference in document index templates.
Generalize the PropertyHelper class.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-06-06 20:07:15 -04:00
parent f7f0d27a05
commit 5798cabd7c
30 changed files with 497 additions and 102 deletions

View File

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

View File

@@ -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 <https://gitlab.com/mayan-edms/mayan-edms/issues/371>`_ Auto select checkbox when updating metadata
* `GitLab issue #372 <https://gitlab.com/mayan-edms/mayan-edms/issues/372>`_ (Feature request) Allow 'rebuild index' to rebuild only a selected index
* `GitLab issue #383 <https://gitlab.com/mayan-edms/mayan-edms/issues/383>`_ Page not found when deployed to sub-uri
* `GitLab issue #385 <https://gitlab.com/mayan-edms/mayan-edms/issues/385>`_ mountindex: how to specify FUSE mount option allow_other?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from .models import Workflow, WorkflowState, WorkflowTransition
class WorkflowForm(forms.ModelForm):
class Meta:
fields = ('label',)
fields = ('label', 'internal_name')
model = Workflow

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:
def get_result(self, name):
return self.instance.metadata.get(metadata_type__name=name).value
except ObjectDoesNotExist:
raise AttributeError(
_('\'metadata\' object has no attribute \'%s\'') % name
)
class MetadataLookup(object):

View File

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

View File

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