216 lines
7.3 KiB
Python
216 lines
7.3 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django.db import models, transaction
|
|
from django.db.models import Q
|
|
from django.template import Context, Template
|
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from mayan.apps.documents.events import event_document_type_edited
|
|
from mayan.apps.documents.models import Document, DocumentType
|
|
|
|
from .events import event_smart_link_created, event_smart_link_edited
|
|
from .literals import (
|
|
INCLUSION_AND, INCLUSION_CHOICES, INCLUSION_OR, OPERATOR_CHOICES
|
|
)
|
|
from .managers import SmartLinkManager
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class SmartLink(models.Model):
|
|
"""
|
|
This model stores the basic fields for a smart link. Smart links allow
|
|
linking documents using a programmatic method of conditions that mirror
|
|
Django's database filter operations.
|
|
"""
|
|
label = models.CharField(
|
|
db_index=True, max_length=96, verbose_name=_('Label')
|
|
)
|
|
dynamic_label = models.CharField(
|
|
blank=True, max_length=96, help_text=_(
|
|
'Enter a template to render. '
|
|
'Use Django\'s default templating language '
|
|
'(https://docs.djangoproject.com/en/1.11/ref/templates/builtins/). '
|
|
'The {{ document }} context variable is available.'
|
|
), verbose_name=_('Dynamic label')
|
|
)
|
|
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
|
|
document_types = models.ManyToManyField(
|
|
related_name='smart_links', to=DocumentType,
|
|
verbose_name=_('Document types')
|
|
)
|
|
|
|
objects = SmartLinkManager()
|
|
|
|
class Meta:
|
|
ordering = ('label',)
|
|
verbose_name = _('Smart link')
|
|
verbose_name_plural = _('Smart links')
|
|
|
|
def __str__(self):
|
|
return self.label
|
|
|
|
def document_types_add(self, queryset, _user=None):
|
|
with transaction.atomic():
|
|
event_smart_link_edited.commit(
|
|
actor=_user, target=self
|
|
)
|
|
for obj in queryset:
|
|
self.document_types.add(obj)
|
|
event_document_type_edited.commit(
|
|
actor=_user, action_object=self, target=obj
|
|
)
|
|
|
|
def document_types_remove(self, queryset, _user=None):
|
|
with transaction.atomic():
|
|
event_smart_link_edited.commit(
|
|
actor=_user, target=self
|
|
)
|
|
for obj in queryset:
|
|
self.document_types.remove(obj)
|
|
event_document_type_edited.commit(
|
|
actor=_user, action_object=self, target=obj
|
|
)
|
|
|
|
def get_dynamic_label(self, document):
|
|
"""
|
|
If the smart links was created using a template label instead of a
|
|
static label, resolve the template and return the result.
|
|
"""
|
|
if self.dynamic_label:
|
|
context = Context({'document': document})
|
|
try:
|
|
template = Template(self.dynamic_label)
|
|
return template.render(context=context)
|
|
except Exception as exception:
|
|
return _(
|
|
'Error generating dynamic label; %s' % force_text(
|
|
exception
|
|
)
|
|
)
|
|
else:
|
|
return None
|
|
|
|
def get_linked_document_for(self, document):
|
|
"""
|
|
Execute the corresponding smart links conditions for the document
|
|
provided and return the resulting document queryset.
|
|
"""
|
|
if document.document_type.pk not in self.document_types.values_list('pk', flat=True):
|
|
raise Exception(
|
|
_(
|
|
'This smart link is not allowed for the selected '
|
|
'document\'s type.'
|
|
)
|
|
)
|
|
|
|
smart_link_query = Q()
|
|
|
|
context = Context({'document': document})
|
|
|
|
for condition in self.conditions.filter(enabled=True):
|
|
template = Template(condition.expression)
|
|
|
|
condition_query = Q(**{
|
|
'%s__%s' % (
|
|
condition.foreign_document_data, condition.operator
|
|
): template.render(context=context)
|
|
})
|
|
if condition.negated:
|
|
condition_query = ~condition_query
|
|
|
|
if condition.inclusion == INCLUSION_AND:
|
|
smart_link_query &= condition_query
|
|
elif condition.inclusion == INCLUSION_OR:
|
|
smart_link_query |= condition_query
|
|
|
|
if smart_link_query:
|
|
return Document.objects.filter(smart_link_query)
|
|
else:
|
|
return Document.objects.none()
|
|
|
|
def resolve_for(self, document):
|
|
return ResolvedSmartLink(
|
|
smart_link=self, queryset=self.get_linked_document_for(
|
|
document=document
|
|
)
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
_user = kwargs.pop('_user', None)
|
|
|
|
with transaction.atomic():
|
|
is_new = not self.pk
|
|
super(SmartLink, self).save(*args, **kwargs)
|
|
if is_new:
|
|
event_smart_link_created.commit(
|
|
actor=_user, target=self
|
|
)
|
|
else:
|
|
event_smart_link_edited.commit(
|
|
actor=_user, target=self
|
|
)
|
|
|
|
|
|
class ResolvedSmartLink(SmartLink):
|
|
"""
|
|
Proxy model to represent an already resolved smart link. Used for easier
|
|
colums registration.
|
|
"""
|
|
class Meta:
|
|
proxy = True
|
|
|
|
def get_label_for(self, document):
|
|
return self.get_dynamic_label(document=document) or self.label
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class SmartLinkCondition(models.Model):
|
|
"""
|
|
This model stores a single smart link condition. A smart link is a
|
|
collection of one of more smart link conditions.
|
|
"""
|
|
smart_link = models.ForeignKey(
|
|
on_delete=models.CASCADE, related_name='conditions', to=SmartLink,
|
|
verbose_name=_('Smart link')
|
|
)
|
|
inclusion = models.CharField(
|
|
choices=INCLUSION_CHOICES, default=INCLUSION_AND,
|
|
help_text=_('The inclusion is ignored for the first item.'),
|
|
max_length=16
|
|
)
|
|
foreign_document_data = models.CharField(
|
|
help_text=_('This represents the metadata of all other documents.'),
|
|
max_length=128, verbose_name=_('Foreign document attribute')
|
|
)
|
|
operator = models.CharField(choices=OPERATOR_CHOICES, max_length=16)
|
|
expression = models.TextField(
|
|
help_text=_(
|
|
'Enter a template to render. '
|
|
'Use Django\'s default templating language '
|
|
'(https://docs.djangoproject.com/en/1.11/ref/templates/builtins/). '
|
|
'The {{ document }} context variable is available.'
|
|
), verbose_name=_('Expression')
|
|
)
|
|
negated = models.BooleanField(
|
|
default=False, help_text=_('Inverts the logic of the operator.'),
|
|
verbose_name=_('Negated')
|
|
)
|
|
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
|
|
|
|
class Meta:
|
|
verbose_name = _('Link condition')
|
|
verbose_name_plural = _('Link conditions')
|
|
|
|
def __str__(self):
|
|
return self.get_full_label()
|
|
|
|
def get_full_label(self):
|
|
return '%s foreign %s %s %s %s' % (
|
|
self.get_inclusion_display(),
|
|
self.foreign_document_data, _('not') if self.negated else '',
|
|
self.get_operator_display(), self.expression
|
|
)
|
|
|
|
get_full_label.short_description = _('Full label')
|