diff --git a/HISTORY.rst b/HISTORY.rst
index bc1de6d87d..c01c4062b1 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -151,6 +151,10 @@
templates that only refresh the menu when there are changes.
Closes GitLab issue #511. Thanks to Daniel Carrico
@daniel1113 for the report.
+- Refactor the ModelAttribute class into two separate classes:
+ ModelAttribute for executable model attributes and ModelField
+ for actual ORM fields.
+- Expose more document fields for use in smart links.
3.0.3 (2018-08-17)
==================
diff --git a/docs/releases/3.1.rst b/docs/releases/3.1.rst
index 51b1b901e2..ed834f2d46 100644
--- a/docs/releases/3.1.rst
+++ b/docs/releases/3.1.rst
@@ -367,6 +367,11 @@ classes beyond the provide line chart.
templates that only refresh the menu when there are changes.
Closes GitLab issue #511. Thanks to Daniel Carrico
@daniel1113 for the report.
+- Refactor the ModelAttribute class into two separate classes:
+ ModelAttribute for executable model attributes and ModelField
+ for actual ORM fields.
+- Expose more document fields for use in smart links.
+
Removals
--------
diff --git a/mayan/apps/common/classes.py b/mayan/apps/common/classes.py
index e1ec09eaa0..abaf25af8d 100644
--- a/mayan/apps/common/classes.py
+++ b/mayan/apps/common/classes.py
@@ -178,19 +178,15 @@ class MissingItem(object):
@python_2_unicode_compatible
class ModelAttribute(object):
- __registry = {}
+ _registry = {}
@classmethod
- def get_for(cls, model, type_names=None):
+ def get_for(cls, model):
result = []
try:
- for type_name, attributes in cls.__registry[model].items():
- if not type_names or type_name in type_names:
- result.extend(attributes)
-
- return result
- except IndexError:
+ return cls._registry[model]
+ except KeyError:
# We were passed a model instance, try again using the model of
# the instance
@@ -198,30 +194,39 @@ class ModelAttribute(object):
if model.__class__ == models.base.ModelBase:
raise
- return cls.get_for[type(model)]
+ return cls.get_for(model=type(model))
@classmethod
- def get_choices_for(cls, model, type_names=None):
+ def get_choices_for(cls, model):
return [
- (
- attribute.name, attribute
- ) for attribute in cls.get_for(model, type_names)
+ (attribute.name, attribute) for attribute in cls.get_for(model)
]
@classmethod
- def help_text_for(cls, model, type_names=None):
+ def get_help_text_for(cls, model, show_name=False):
result = []
- for count, attribute in enumerate(cls.get_for(model, type_names), 1):
+ for count, attribute in enumerate(cls.get_for(model=model), 1):
result.append(
'{}) {}'.format(
- count, force_text(attribute.get_display(show_name=True))
+ count, force_text(attribute.get_display(show_name=show_name))
)
)
return ' '.join(
- [ugettext('Available attributes: \n'), ', \n'.join(result)]
+ [ugettext('Available attributes: \n'), '\n'.join(result)]
)
+ def __init__(self, model, name, label=None, description=None):
+ self.model = model
+ self.label = label
+ self.name = name
+ self.description = description
+ self._registry.setdefault(model, [])
+ self._registry[model].append(self)
+
+ def __str__(self):
+ return self.get_display()
+
def get_display(self, show_name=False):
if self.description:
return '{} - {}'.format(
@@ -230,29 +235,101 @@ class ModelAttribute(object):
else:
return force_text(self.name if show_name else self.label)
- def __str__(self):
- return self.get_display()
- def __init__(self, model, name, label=None, description=None, type_name=None):
- self.model = model
- self.label = label
- self.name = name
- self.description = description
+class ModelField(ModelAttribute):
+ """Subclass to handle model database fields"""
+ _registry = {}
- for field in model._meta.fields:
- if field.name == name:
- self.label = field.verbose_name
- self.description = field.help_text
+ @classmethod
+ def get_help_text_for(cls, model, show_name=False):
+ result = []
+ for count, model_field in enumerate(cls.get_for(model=model), 1):
+ result.append(
+ '{}) {} - {}'.format(
+ count,
+ model_field.name if show_name else model_field.label,
+ model_field.description
+ )
+ )
- self.__registry.setdefault(model, {})
+ return ' '.join(
+ [ugettext('Available fields: \n'), '\n'.join(result)]
+ )
- if isinstance(type_name, list):
- for single_type in type_name:
- self.__registry[model].setdefault(single_type, [])
- self.__registry[model][single_type].append(self)
+ def __init__(self, *args, **kwargs):
+ super(ModelField, self).__init__(*args, **kwargs)
+ self._final_model_verbose_name = None
+
+ if not self.label:
+ self.label = self.get_field_attribute(
+ attribute='verbose_name'
+ )
+ if self.label != self._final_model_verbose_name:
+ self.label = '{} {}'.format(
+ self._final_model_verbose_name, self.label
+ )
+
+ if not self.description:
+ self.description = self.get_field_attribute(
+ attribute='help_text'
+ )
+
+ def get_field_attribute(self, attribute, model=None, field_name=None):
+ if not model:
+ model = self.model
+
+ if not field_name:
+ field_name = self.name
+
+ parts = field_name.split('__')
+ if len(parts) > 1:
+ return self.get_field_attribute(
+ model=model._meta.get_field(parts[0]).related_model,
+ field_name='__'.join(parts[1:]), attribute=attribute
+ )
else:
- self.__registry[model].setdefault(type_name, [])
- self.__registry[model][type_name].append(self)
+ self._final_model_verbose_name = model._meta.verbose_name
+ return getattr(
+ model._meta.get_field(field_name=field_name),
+ attribute
+ )
+
+
+class ModelProperty(object):
+ _registry = []
+
+ @classmethod
+ def get_for(cls, model):
+ result = []
+
+ for klass in cls._registry:
+ result.extend(klass.get_for(model=model))
+
+ return result
+
+ @classmethod
+ def get_choices_for(cls, model):
+ result = []
+
+ for klass in cls._registry:
+ result.extend(klass.get_choices_for(model=model))
+
+ return result
+
+ @classmethod
+ def get_help_text_for(cls, model, show_name=False):
+ result = []
+
+ for klass in cls._registry:
+ result.append(
+ klass.get_help_text_for(model=model, show_name=show_name)
+ )
+
+ return '\n'.join(result)
+
+ @classmethod
+ def register(cls, klass):
+ cls._registry.append(klass)
class Package(object):
@@ -321,3 +398,7 @@ class Template(object):
self.html = result.content
self.hex_hash = hashlib.sha256(result.content).hexdigest()
return self
+
+
+ModelProperty.register(ModelAttribute)
+ModelProperty.register(ModelField)
diff --git a/mayan/apps/document_indexing/forms.py b/mayan/apps/document_indexing/forms.py
index 4131f91ccf..dd558ed0d1 100644
--- a/mayan/apps/document_indexing/forms.py
+++ b/mayan/apps/document_indexing/forms.py
@@ -5,7 +5,7 @@ from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
-from common.classes import ModelAttribute
+from common.classes import ModelProperty
from documents.models import Document
from .models import Index, IndexTemplateNode
@@ -40,8 +40,9 @@ class IndexTemplateNodeForm(forms.ModelForm):
self.fields['expression'].help_text = ' '.join(
[
force_text(self.fields['expression'].help_text),
- ModelAttribute.help_text_for(
- Document, type_names=['indexing']
+ '
',
+ ModelProperty.get_help_text_for(
+ model=Document, show_name=True
).replace('\n', '
')
]
)
diff --git a/mayan/apps/document_parsing/apps.py b/mayan/apps/document_parsing/apps.py
index 8140e881c0..ac7671cd29 100644
--- a/mayan/apps/document_parsing/apps.py
+++ b/mayan/apps/document_parsing/apps.py
@@ -15,6 +15,7 @@ from common import (
MayanAppConfig, menu_facet, menu_multi_item, menu_object, menu_secondary,
menu_tools
)
+from common.classes import ModelField
from common.settings import settings_db_sync_task_delay
from documents.search import document_search, document_page_search
from documents.signals import post_version_upload
@@ -97,6 +98,10 @@ class DocumentParsingApp(MayanAppConfig):
'submit_for_parsing', document_version_parsing_submit
)
+ ModelField(
+ Document, name='versions__pages__content__content'
+ )
+
ModelPermission.register(
model=Document, permissions=(
permission_content_view, permission_parse_document
@@ -110,6 +115,7 @@ class DocumentParsingApp(MayanAppConfig):
ModelPermission.register_inheritance(
model=DocumentTypeSettings, related='document_type',
)
+
SourceColumn(
source=DocumentVersionParseError, label=_('Document'),
func=lambda context: document_link(context['object'].document_version.document)
diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py
index 85a64ad6d7..9535f7abca 100644
--- a/mayan/apps/document_states/apps.py
+++ b/mayan/apps/document_states/apps.py
@@ -90,18 +90,19 @@ class DocumentStatesApp(MayanAppConfig):
WorkflowAction.initialize()
ModelAttribute(
- Document, 'workflow.< workflow internal name >.get_current_state',
+ model=Document,
+ name='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',
+ model=Document,
+ name='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(
diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py
index 232ca92bc9..e168341286 100644
--- a/mayan/apps/documents/apps.py
+++ b/mayan/apps/documents/apps.py
@@ -14,7 +14,7 @@ from common import (
MayanAppConfig, MissingItem, menu_facet, menu_main, menu_object,
menu_secondary, menu_setup, menu_sidebar, menu_multi_item, menu_tools
)
-from common.classes import ModelAttribute
+from common.classes import ModelAttribute, ModelField
from common.dashboards import dashboard_main
from common.signals import post_initial_setup
from common.widgets import TwoStateWidget
@@ -142,14 +142,29 @@ class DocumentsApp(MayanAppConfig):
view='documents:document_type_list'
)
- ModelAttribute(
- Document, label=_('Label'), name='label', type_name='field'
+ ModelField(Document, name='description')
+ ModelField(Document, name='date_added')
+ ModelField(Document, name='deleted_date_time')
+ ModelField(Document, name='document_type__label')
+ ModelField(Document, name='in_trash')
+ ModelField(Document, name='is_stub')
+ ModelField(Document, name='label')
+ ModelField(Document, name='language')
+ ModelField(Document, name='uuid')
+ ModelField(
+ Document, name='versions__checksum'
)
-
- ModelAttribute(
- Document,
- description=_('The MIME type of any of the versions of a document'),
- label=_('MIME type'), name='versions__mimetype', type_name='field'
+ ModelField(
+ Document, label=_('Versions comment'), name='versions__comment'
+ )
+ ModelField(
+ Document, label=_('Versions encoding'), name='versions__encoding'
+ )
+ ModelField(
+ Document, label=_('Versions mime type'), name='versions__mimetype'
+ )
+ ModelField(
+ Document, label=_('Versions timestamp'), name='versions__timestamp'
)
ModelEventType.register(
diff --git a/mayan/apps/linking/forms.py b/mayan/apps/linking/forms.py
index 56f0f2195a..c3b499068f 100644
--- a/mayan/apps/linking/forms.py
+++ b/mayan/apps/linking/forms.py
@@ -4,7 +4,7 @@ from django import forms
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
-from common.classes import ModelAttribute
+from common.classes import ModelAttribute, ModelField, ModelProperty
from documents.models import Document
from .models import SmartLink, SmartLinkCondition
@@ -16,8 +16,8 @@ class SmartLinkForm(forms.ModelForm):
self.fields['dynamic_label'].help_text = ' '.join(
[
force_text(self.fields['dynamic_label'].help_text),
- ModelAttribute.help_text_for(
- Document, type_names=['field', 'related', 'property']
+ ModelProperty.get_help_text_for(
+ model=Document, show_name=True
).replace('\n', '
')
]
)
@@ -31,15 +31,15 @@ class SmartLinkConditionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SmartLinkConditionForm, self).__init__(*args, **kwargs)
self.fields['foreign_document_data'] = forms.ChoiceField(
- choices=ModelAttribute.get_choices_for(
- Document, type_names=['field', 'query']
- ), label=_('Foreign document attribute')
+ choices=ModelField.get_choices_for(
+ model=Document,
+ ), label=_('Foreign document field')
)
self.fields['expression'].help_text = ' '.join(
[
force_text(self.fields['expression'].help_text),
- ModelAttribute.help_text_for(
- Document, type_names=['field', 'related', 'property']
+ ModelProperty.get_help_text_for(
+ model=Document, show_name=True
).replace('\n', '
')
]
)
diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py
index 0fb3e6ad9a..e8ca1e1405 100644
--- a/mayan/apps/metadata/apps.py
+++ b/mayan/apps/metadata/apps.py
@@ -15,7 +15,7 @@ from common import (
MayanAppConfig, menu_facet, menu_multi_item, menu_object, menu_secondary,
menu_setup, menu_sidebar
)
-from common.classes import ModelAttribute
+from common.classes import ModelAttribute, ModelField
from common.widgets import TwoStateWidget
from documents.search import document_page_search, document_search
from documents.signals import post_document_type_change
@@ -92,26 +92,18 @@ class MetadataApp(MayanAppConfig):
)
ModelAttribute(
- Document, 'metadata', type_name='related',
- description=_(
- 'Queryset containing a MetadataType instance reference and a '
- 'value for that metadata type'
- )
- )
- ModelAttribute(
- Document, 'metadata__metadata_type__name',
- label=_('Metadata type name'), type_name='query'
- )
- ModelAttribute(
- Document, 'metadata__value', label=_('Metadata type value'),
- type_name='query'
- )
- ModelAttribute(
- Document, 'metadata_value_of', label=_('Value of a metadata'),
+ Document, 'metadata_value_of',
description=_(
'Return the value of a specific document metadata'
),
- type_name=['property', 'indexing']
+ )
+
+ ModelField(
+ Document, 'metadata__metadata_type__name',
+ label=_('Metadata type name')
+ )
+ ModelField(
+ Document, 'metadata__value', label=_('Metadata type value'),
)
ModelEventType.register(
diff --git a/mayan/apps/ocr/apps.py b/mayan/apps/ocr/apps.py
index 089a07deab..96d8dd5d08 100644
--- a/mayan/apps/ocr/apps.py
+++ b/mayan/apps/ocr/apps.py
@@ -15,6 +15,7 @@ from common import (
MayanAppConfig, menu_facet, menu_multi_item, menu_object, menu_secondary,
menu_tools
)
+from common.classes import ModelField
from common.settings import settings_db_sync_task_delay
from documents.search import document_search, document_page_search
from documents.signals import post_version_upload
@@ -98,6 +99,10 @@ class OCRApp(MayanAppConfig):
'submit_for_ocr', document_version_ocr_submit
)
+ ModelField(
+ Document, name='versions__pages__ocr_content__content'
+ )
+
ModelPermission.register(
model=Document, permissions=(
permission_ocr_document, permission_ocr_content_view
@@ -111,6 +116,7 @@ class OCRApp(MayanAppConfig):
ModelPermission.register_inheritance(
model=DocumentTypeSettings, related='document_type',
)
+
SourceColumn(
source=DocumentVersionOCRError, label=_('Document'),
func=lambda context: document_link(context['object'].document_version.document)
diff --git a/mayan/apps/tags/apps.py b/mayan/apps/tags/apps.py
index 10770b3010..18e12e0eb0 100644
--- a/mayan/apps/tags/apps.py
+++ b/mayan/apps/tags/apps.py
@@ -11,6 +11,7 @@ from common import (
MayanAppConfig, menu_facet, menu_object, menu_main, menu_multi_item,
menu_sidebar
)
+from common.classes import ModelField
from documents.search import document_page_search, document_search
from events import ModelEventType
from events.links import (
@@ -73,6 +74,13 @@ class TagsApp(MayanAppConfig):
)
)
+ ModelField(
+ Document, name='tags__label'
+ )
+ ModelField(
+ Document, name='tags__color'
+ )
+
ModelPermission.register(
model=Document, permissions=(
permission_tag_attach, permission_tag_remove,