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:
@@ -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)
|
||||
================
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
32
mayan/apps/common/validators.py
Normal file
32
mayan/apps/common/validators.py
Normal 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'
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>')
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
13
mayan/apps/document_states/classes.py
Normal file
13
mayan/apps/document_states/classes.py
Normal 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)
|
||||
@@ -8,7 +8,7 @@ from .models import Workflow, WorkflowState, WorkflowTransition
|
||||
|
||||
class WorkflowForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ('label',)
|
||||
fields = ('label', 'internal_name')
|
||||
model = Workflow
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
125
mayan/apps/document_states/tests/test_models.py
Normal file
125
mayan/apps/document_states/tests/test_models.py
Normal 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)
|
||||
), ['']
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>')
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user