Files
mayan-edms/mayan/apps/metadata/models.py
Roberto Rosario 8e69178e07 Project: Switch to full app paths
Instead of inserting the path of the apps into the Python app,
the apps are now referenced by their full import path.

This app name claves with external or native Python libraries.
Example: Mayan statistics app vs. Python new statistics library.

Every app reference is now prepended with 'mayan.apps'.

Existing config.yml files need to be updated manually.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:04:20 -04:00

332 lines
11 KiB
Python

from __future__ import unicode_literals
import shlex
from jinja2 import Template
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.module_loading import import_string
from django.utils.six import PY2
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.models import Document, DocumentType
from .classes import MetadataLookup
from .events import (
event_document_metadata_added, event_document_metadata_edited,
event_document_metadata_removed, event_metadata_type_created,
event_metadata_type_edited, event_metadata_type_relationship
)
from .managers import DocumentTypeMetadataTypeManager, MetadataTypeManager
from .settings import setting_available_parsers, setting_available_validators
def validation_choices():
return zip(
setting_available_validators.value,
setting_available_validators.value
)
def parser_choices():
return zip(
setting_available_parsers.value,
setting_available_parsers.value
)
@python_2_unicode_compatible
class MetadataType(models.Model):
"""
Model to store a type of metadata. Metadata are user defined properties
that can be assigned a value for each document. Metadata types need
to be assigned to a document type before they can be used.
"""
name = models.CharField(
max_length=48,
help_text=_(
'Name used by other apps to reference this metadata type. '
'Do not use python reserved words, or spaces.'
),
unique=True, verbose_name=_('Name')
)
label = models.CharField(max_length=48, verbose_name=_('Label'))
default = models.CharField(
blank=True, max_length=128, null=True,
help_text=_(
'Enter a template to render. '
'Use Django\'s default templating language '
'(https://docs.djangoproject.com/en/1.11/ref/templates/builtins/)'
),
verbose_name=_('Default')
)
lookup = models.TextField(
blank=True, null=True,
help_text=_(
'Enter a template to render. '
'Must result in a comma delimited string. '
'Use Django\'s default templating language '
'(https://docs.djangoproject.com/en/1.11/ref/templates/builtins/).'
),
verbose_name=_('Lookup')
)
validation = models.CharField(
blank=True, choices=validation_choices(),
help_text=_(
'The validator will reject data entry if the value entered does '
'not conform to the expected format.'
), max_length=64, verbose_name=_('Validator')
)
parser = models.CharField(
blank=True, choices=parser_choices(), help_text=_(
'The parser will reformat the value entered to conform to the '
'expected format.'
), max_length=64, verbose_name=_('Parser')
)
objects = MetadataTypeManager()
class Meta:
ordering = ('label',)
verbose_name = _('Metadata type')
verbose_name_plural = _('Metadata types')
def __str__(self):
return self.label
def get_absolute_url(self):
return reverse('metadata:setup_metadata_type_edit', args=(self.pk,))
if PY2:
# Python 2 non unicode version
@staticmethod
def comma_splitter(string):
splitter = shlex.shlex(string.encode('utf-8'), posix=True)
splitter.whitespace = ','.encode('utf-8')
splitter.whitespace_split = True
splitter.commenters = ''.encode('utf-8')
return [force_text(e) for e in splitter]
else:
# Python 3 unicode version
@staticmethod
def comma_splitter(string):
splitter = shlex.shlex(string, posix=True)
splitter.whitespace = ','
splitter.whitespace_split = True
splitter.commenters = ''
return [force_text(e) for e in splitter]
def get_default_value(self):
template = Template(self.default)
return template.render()
def get_lookup_values(self):
template = Template(self.lookup)
context = MetadataLookup.get_as_context()
return MetadataType.comma_splitter(template.render(**context))
def get_required_for(self, document_type):
"""
Return a queryset of metadata types that are required for the
specified document type.
"""
return document_type.metadata.filter(
required=True, metadata_type=self
).exists()
def natural_key(self):
return (self.name,)
def save(self, *args, **kwargs):
user = kwargs.pop('_user', None)
created = not self.pk
result = super(MetadataType, self).save(*args, **kwargs)
if created:
event_metadata_type_created.commit(
actor=user, target=self
)
else:
event_metadata_type_edited.commit(
actor=user, target=self
)
return result
def validate_value(self, document_type, value):
# Check default
if not value and self.default:
value = self.get_default_value()
if not value and self.get_required_for(document_type=document_type):
raise ValidationError(
_('"%s" is required for this document type.') % self.label
)
if self.lookup:
lookup_options = self.get_lookup_values()
if value and value not in lookup_options:
raise ValidationError(
_('Value is not one of the provided options.')
)
if self.validation:
validator = import_string(self.validation)()
validator.validate(value)
if self.parser:
parser = import_string(self.parser)()
value = parser.parse(value)
return value
@python_2_unicode_compatible
class DocumentMetadata(models.Model):
"""
Model used to link an instance of a metadata type with a value to a
document.
"""
document = models.ForeignKey(
on_delete=models.CASCADE, related_name='metadata', to=Document,
verbose_name=_('Document')
)
metadata_type = models.ForeignKey(
on_delete=models.CASCADE, to=MetadataType, verbose_name=_('Type')
)
value = models.CharField(
blank=True, db_index=True, help_text=_(
'The actual value stored in the metadata type field for '
'the document.'
), max_length=255, null=True, verbose_name=_('Value')
)
class Meta:
ordering = ('metadata_type',)
unique_together = ('document', 'metadata_type')
verbose_name = _('Document metadata')
verbose_name_plural = _('Document metadata')
def __str__(self):
return force_text(self.metadata_type)
def clean_fields(self, *args, **kwargs):
super(DocumentMetadata, self).clean_fields(*args, **kwargs)
self.value = self.metadata_type.validate_value(
document_type=self.document.document_type, value=self.value
)
def delete(self, enforce_required=True, *args, **kwargs):
"""
Delete a metadata from a document. enforce_required which defaults
to True, prevents deletion of required metadata at the model level.
It used set to False when deleting document metadata on document
type change.
"""
if enforce_required and self.metadata_type.pk in self.document.document_type.metadata.filter(required=True).values_list('metadata_type', flat=True):
raise ValidationError(
_('Metadata type is required for this document type.')
)
user = kwargs.pop('_user', None)
result = super(DocumentMetadata, self).delete(*args, **kwargs)
event_document_metadata_removed.commit(
action_object=self.metadata_type, actor=user, target=self.document,
)
return result
def natural_key(self):
return self.document.natural_key() + self.metadata_type.natural_key()
natural_key.dependencies = ['documents.Document', 'metadata.MetadataType']
@property
def is_required(self):
"""
Return a boolean value of True of this metadata instance's parent type
is required for the stored document type.
"""
return self.metadata_type.get_required_for(
document_type=self.document.document_type
)
def save(self, *args, **kwargs):
if self.metadata_type.pk not in self.document.document_type.metadata.values_list('metadata_type', flat=True):
raise ValidationError(
_('Metadata type is not valid for this document type.')
)
user = kwargs.pop('_user', None)
created = not self.pk
result = super(DocumentMetadata, self).save(*args, **kwargs)
if created:
event_document_metadata_added.commit(
action_object=self.metadata_type, actor=user,
target=self.document,
)
else:
event_document_metadata_edited.commit(
action_object=self.metadata_type, actor=user,
target=self.document,
)
return result
@python_2_unicode_compatible
class DocumentTypeMetadataType(models.Model):
"""
Model used to store the relationship between a metadata type and a
document type.
"""
document_type = models.ForeignKey(
on_delete=models.CASCADE, related_name='metadata', to=DocumentType,
verbose_name=_('Document type')
)
metadata_type = models.ForeignKey(
on_delete=models.CASCADE, to=MetadataType,
verbose_name=_('Metadata type')
)
required = models.BooleanField(default=False, verbose_name=_('Required'))
objects = DocumentTypeMetadataTypeManager()
class Meta:
ordering = ('metadata_type',)
unique_together = ('document_type', 'metadata_type')
verbose_name = _('Document type metadata type options')
verbose_name_plural = _('Document type metadata types options')
def __str__(self):
return force_text(self.metadata_type)
def delete(self, *args, **kwargs):
user = kwargs.pop('_user', None)
result = super(DocumentTypeMetadataType, self).delete(*args, **kwargs)
event_metadata_type_relationship.commit(
action_object=self.document_type, actor=user, target=self.metadata_type,
)
return result
def save(self, *args, **kwargs):
user = kwargs.pop('_user', None)
result = super(DocumentTypeMetadataType, self).save(*args, **kwargs)
event_metadata_type_relationship.commit(
action_object=self.document_type, actor=user, target=self.metadata_type,
)
return result