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,