Commit c7cc61c1 authored by Roberto Rosario's avatar Roberto Rosario

Add feature complete document indexing API

Signed-off-by: Roberto Rosario's avatarRoberto Rosario <[email protected]>
parent eacfa15b
......@@ -8,6 +8,7 @@
in web link app resource name. Thanks to forum user @qra
(https://forum.mayan-edms.com/viewtopic.php?t=3009) for the
report.
- Feature complete document indexing API.
3.4.16 (2020-08-30)
===================
......
......@@ -5,103 +5,209 @@ from mayan.apps.documents.models import Document
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.serializers import DocumentSerializer
from mayan.apps.rest_api import generics
from mayan.apps.rest_api.mixins import AsymmetricSerializerViewMixin
from .models import Index, IndexInstanceNode, IndexTemplateNode
from .models import Index, IndexInstance
from .permissions import (
permission_document_indexing_create, permission_document_indexing_delete,
permission_document_indexing_edit, permission_document_indexing_view
permission_document_indexing_edit,
permission_document_indexing_instance_view,
permission_document_indexing_view
)
from .serializers import (
IndexInstanceNodeSerializer, IndexSerializer, IndexTemplateNodeSerializer
IndexInstanceNodeSerializer, IndexInstanceSerializer,
IndexTemplateSerializer, IndexTemplateNodeSerializer,
IndexTemplateNodeWriteSerializer, IndexTemplateWriteSerializer
)
class APIIndexListView(generics.ListCreateAPIView):
class APIDocumentIndexInstanceNodeListView(generics.ListAPIView):
"""
get: Returns a list of all the defined indexes.
post: Create a new index.
Returns a list of all the indexes instance nodes where this document is found.
"""
mayan_object_permissions = {'GET': (permission_document_indexing_view,)}
mayan_view_permissions = {'POST': (permission_document_indexing_create,)}
queryset = Index.objects.all()
serializer_class = IndexSerializer
mayan_object_permissions = {'GET': (permission_document_indexing_instance_view,)}
serializer_class = IndexInstanceNodeSerializer
def get_document(self):
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self.request.user
)
class APIIndexView(generics.RetrieveUpdateDestroyAPIView):
return get_object_or_404(
klass=queryset, pk=self.kwargs['document_id']
)
def get_queryset(self):
return self.get_document().index_instance_nodes.all()
class APIIndexInstanceDetailView(generics.RetrieveAPIView):
"""
delete: Delete the selected index.
get: Returns the details of the selected index.
patch: Partially edit an index.
put: Edit an index.
get: Returns the details of the selected index instance.
"""
mayan_object_permissions = {
'GET': (permission_document_indexing_view,),
'PUT': (permission_document_indexing_edit,),
'PATCH': (permission_document_indexing_edit,),
'DELETE': (permission_document_indexing_delete,)
}
queryset = Index.objects.all()
serializer_class = IndexSerializer
lookup_url_kwarg = 'index_instance_id'
mayan_object_permissions = {'GET': (permission_document_indexing_instance_view,)}
queryset = IndexInstance.objects.all()
serializer_class = IndexInstanceSerializer
class APIIndexNodeInstanceDocumentListView(generics.ListAPIView):
class APIIndexInstanceListView(generics.ListAPIView):
"""
Returns a list of all the documents contained by a particular index node
instance.
get: Returns a list of all the indexes instances.
"""
mayan_object_permissions = {'GET': (permission_document_view,)}
serializer_class = DocumentSerializer
mayan_object_permissions = {'GET': (permission_document_indexing_instance_view,)}
queryset = IndexInstance.objects.all()
serializer_class = IndexInstanceSerializer
def get_queryset(self):
index_node_instance = get_object_or_404(
klass=IndexInstanceNode, pk=self.kwargs['pk']
)
AccessControlList.objects.check_access(
obj=index_node_instance.index,
permissions=(permission_document_indexing_view,),
user=self.request.user
class APIIndexInstanceNodeViewMixin:
serializer_class = IndexInstanceNodeSerializer
def get_index_instance(self):
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_indexing_instance_view,
queryset=IndexInstance.objects.all(), user=self.request.user
)
return index_node_instance.documents.all()
return get_object_or_404(
klass=queryset, pk=self.kwargs['index_instance_id']
)
class APIIndexTemplateListView(generics.ListAPIView):
class APIIndexInstanceNodeListView(
APIIndexInstanceNodeViewMixin, generics.ListAPIView
):
"""
get: Returns a list of all the template nodes for the selected index.
post: Create a new index template node.
"""
mayan_object_permissions = {'GET': (permission_document_indexing_view,)}
serializer_class = IndexTemplateNodeSerializer
def get_queryset(self):
return self.get_index_instance().get_children()
class APIIndexTemplateView(generics.RetrieveUpdateDestroyAPIView):
class APIIndexInstanceNodeDetailView(
APIIndexInstanceNodeViewMixin, generics.RetrieveAPIView
):
"""
delete: Delete the selected index template node.
get: Returns the details of the selected index template node.
patch: Partially edit an index template node.
put: Edit an index template node.
"""
lookup_url_kwarg = 'index_instance_node_id'
def get_queryset(self):
return self.get_index_instance().get_nodes()
class APIIndexInstanceNodeDocumentListView(
APIIndexInstanceNodeViewMixin, generics.ListAPIView
):
"""
Returns a list of all the documents contained by a particular index node
instance.
"""
mayan_object_permissions = {'GET': (permission_document_view,)}
serializer_class = DocumentSerializer
def get_node(self):
return get_object_or_404(
klass=self.get_index_instance().get_nodes(),
pk=self.kwargs['index_instance_node_id']
)
def get_queryset(self):
return self.get_node().documents.all()
class APIIndexTemplateViewMixin(AsymmetricSerializerViewMixin):
queryset = Index.objects.all()
read_serializer_class = IndexTemplateSerializer
write_serializer_class = IndexTemplateWriteSerializer
class APIIndexTemplateListView(
APIIndexTemplateViewMixin, generics.ListCreateAPIView
):
"""
get: Returns a list of all the defined indexes template.
post: Create a new index template.
"""
mayan_object_permissions = {'GET': (permission_document_indexing_view,)}
mayan_view_permissions = {'POST': (permission_document_indexing_create,)}
queryset = Index.objects.all()
class APIIndexTemplateDetailView(
APIIndexTemplateViewMixin, generics.RetrieveUpdateDestroyAPIView
):
"""
delete: Delete the selected index template.
get: Returns the details of the selected index template.
patch: Partially edit an index template.
put: Edit an index template.
"""
lookup_url_kwarg = 'index_template_id'
mayan_object_permissions = {
'GET': (permission_document_indexing_view,),
'PUT': (permission_document_indexing_edit,),
'PATCH': (permission_document_indexing_edit,),
'DELETE': (permission_document_indexing_edit,)
'DELETE': (permission_document_indexing_delete,)
}
queryset = IndexTemplateNode.objects.all()
serializer_class = IndexTemplateNodeSerializer
queryset = Index.objects.all()
class APIDocumentIndexListView(generics.ListAPIView):
"""
Returns a list of all the indexes to which a document belongs.
"""
mayan_object_permissions = {'GET': (permission_document_indexing_view,)}
serializer_class = IndexInstanceNodeSerializer
class APIIndexTemplateNodeViewMixin(AsymmetricSerializerViewMixin):
object_permissions = {
'GET': permission_document_indexing_view,
'PATCH': permission_document_indexing_edit,
'PUT': permission_document_indexing_edit,
'POST': permission_document_indexing_edit,
'DELETE': permission_document_indexing_edit
}
read_serializer_class = IndexTemplateNodeSerializer
write_serializer_class = IndexTemplateNodeWriteSerializer
def get_queryset(self):
document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=document, permissions=(permission_document_view,),
def get_index_template(self):
permission = self.object_permissions[self.request.method]
queryset = AccessControlList.objects.restrict_queryset(
permission=permission, queryset=Index.objects.all(),
user=self.request.user
)
return document.index_instance_nodes.all()
return get_object_or_404(
klass=queryset, pk=self.kwargs['index_template_id']
)
def get_serializer_context(self):
context = super().get_serializer_context()
context['index_template'] = self.get_index_template()
return context
class APIIndexTemplateNodeListView(
APIIndexTemplateNodeViewMixin, generics.ListCreateAPIView
):
"""
get: Returns a list of all the template nodes for the selected index.
post: Create a new index template node.
"""
def get_queryset(self):
return self.get_index_template().template_root.get_children()
class APIIndexTemplateNodeDetailView(
APIIndexTemplateNodeViewMixin, generics.RetrieveUpdateDestroyAPIView
):
"""
delete: Delete the selected index template node.
get: Returns the details of the selected index template node.
patch: Partially edit an index template node.
put: Edit an index template node.
"""
lookup_url_kwarg = 'index_template_node_id'
def get_queryset(self):
return self.get_index_template().node_templates.all()
......@@ -198,30 +198,6 @@ class Index(models.Model):
return self.node_templates.get(parent=None)
class IndexInstance(Index):
"""
Model that represents an evaluated index. This is an index whose nodes
have been evaluated against a series of documents. If is a proxy model
at the moment.
"""
class Meta:
proxy = True
verbose_name = _('Index instance')
verbose_name_plural = _('Index instances')
def get_instance_node_count(self):
try:
return self.instance_root.get_descendant_count()
except IndexInstanceNode.DoesNotExist:
return 0
def get_item_count(self, user):
try:
return self.instance_root.get_item_count(user=user)
except IndexInstanceNode.DoesNotExist:
return 0
@python_2_unicode_compatible
class IndexTemplateNode(MPTTModel):
"""
......@@ -230,7 +206,8 @@ class IndexTemplateNode(MPTTModel):
documents but not both.
"""
parent = TreeForeignKey(
blank=True, null=True, on_delete=models.CASCADE, to='self',
blank=True, null=True, on_delete=models.CASCADE,
related_name='children', to='self',
)
index = models.ForeignKey(
on_delete=models.CASCADE, related_name='node_templates', to=Index,
......@@ -358,6 +335,42 @@ class IndexTemplateNode(MPTTModel):
self.index_instance_nodes.get_or_create(parent=None)
class IndexInstance(Index):
"""
Model that represents an evaluated index. This is an index whose nodes
have been evaluated against a series of documents. If is a proxy model
at the moment.
"""
class Meta:
proxy = True
verbose_name = _('Index instance')
verbose_name_plural = _('Index instances')
def get_children(self):
root_node_queryset = self.get_nodes().filter(parent=None)
if root_node_queryset.exists():
return root_node_queryset.first().get_children()
else:
return root_node_queryset
def get_nodes(self):
return IndexInstanceNode.objects.filter(
index_template_node__index=self
)
def get_instance_node_count(self):
try:
return self.instance_root.get_descendant_count()
except IndexInstanceNode.DoesNotExist:
return 0
def get_item_count(self, user):
try:
return self.instance_root.get_item_count(user=user)
except IndexInstanceNode.DoesNotExist:
return 0
@python_2_unicode_compatible
class IndexInstanceNode(MPTTModel):
"""
......@@ -367,7 +380,8 @@ class IndexInstanceNode(MPTTModel):
model also point to the original node template.
"""
parent = TreeForeignKey(
blank=True, null=True, on_delete=models.CASCADE, to='self',
blank=True, null=True, on_delete=models.CASCADE,
related_name='children', to='self'
)
index_template_node = models.ForeignKey(
on_delete=models.CASCADE, related_name='index_instance_nodes',
......
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from rest_framework_recursive.fields import RecursiveField
from .models import Index, IndexInstanceNode, IndexTemplateNode
from .models import Index, IndexInstance, IndexInstanceNode, IndexTemplateNode
class IndexInstanceSerializer(serializers.ModelSerializer):
item_count = serializers.SerializerMethodField(read_only=True)
node_count = serializers.SerializerMethodField(read_only=True)
url = serializers.SerializerMethodField(read_only=True)
nodes_url = serializers.SerializerMethodField(read_only=True)
class Meta:
fields = (
'label', 'id', 'item_count', 'node_count', 'nodes_url', 'url'
)
model = IndexInstance
def get_item_count(self, obj):
return obj.get_item_count(user=self.context['request'])
def get_node_count(self, obj):
return obj.get_instance_node_count()
def get_url(self, obj):
return reverse(
viewname='rest_api:indexinstance-detail', kwargs={
'index_instance_id': obj.pk,
}, format=self.context['format'], request=self.context['request']
)
def get_nodes_url(self, obj):
return reverse(
viewname='rest_api:indexinstancenode-list', kwargs={
'index_instance_id': obj.pk,
}, format=self.context['format'], request=self.context['request']
)
class IndexInstanceNodeSerializer(serializers.ModelSerializer):
children = RecursiveField(many=True, read_only=True)
documents_count = serializers.SerializerMethodField()
documents = serializers.HyperlinkedIdentityField(
view_name='rest_api:index-node-documents'
)
documents_url = serializers.SerializerMethodField(read_only=True)
index_url = serializers.SerializerMethodField(read_only=True)
parent_url = serializers.SerializerMethodField(read_only=True)
url = serializers.SerializerMethodField(read_only=True)
class Meta:
fields = (
'documents', 'documents_count', 'children', 'id', 'level', 'parent',
'value'
'documents_count', 'documents_url', 'children', 'id',
'index_url', 'level', 'parent', 'parent_url', 'value', 'url'
)
model = IndexInstanceNode
def get_documents_count(self, instance):
return instance.documents.count()
def get_documents_count(self, obj):
return obj.get_descendants_document_count(
user=self.context['request'].user
)
def get_documents_url(self, obj):
return reverse(
viewname='rest_api:indexinstancenode-document-list', kwargs={
'index_instance_id': obj.index().pk,
'index_instance_node_id': obj.pk
}, format=self.context['format'], request=self.context['request']
)
def get_index_url(self, obj):
return reverse(
viewname='rest_api:indexinstance-detail', kwargs={
'index_instance_id': obj.index().pk,
}, format=self.context['format'], request=self.context['request']
)
def get_parent_url(self, obj):
if obj.parent:
return reverse(
viewname='rest_api:indexinstancenode-detail', kwargs={
'index_instance_id': obj.index().pk,
'index_instance_node_id': obj.parent.pk
}, format=self.context['format'],
request=self.context['request']
)
else:
return ''
def get_url(self, obj):
return reverse(
viewname='rest_api:indexinstancenode-detail', kwargs={
'index_instance_id': obj.index().pk,
'index_instance_node_id': obj.pk
}, format=self.context['format'], request=self.context['request']
)
class IndexTemplateNodeSerializer(serializers.ModelSerializer):
children = RecursiveField(many=True, read_only=True)
index_url = serializers.SerializerMethodField(read_only=True)
parent_url = serializers.SerializerMethodField(read_only=True)
url = serializers.SerializerMethodField(read_only=True)
class Meta:
fields = (
'enabled', 'expression', 'id', 'index', 'level', 'link_documents',
'parent'
'children', 'enabled', 'expression', 'id', 'index', 'index_url',
'level', 'link_documents', 'parent', 'parent_url', 'url'
)
model = IndexTemplateNode
read_only_fields = ('children', 'id', 'index', 'level')
def get_index_url(self, obj):
return reverse(
viewname='rest_api:indextemplate-detail', kwargs={
'index_template_id': obj.index.pk,
}, format=self.context['format'], request=self.context['request']
)
def get_parent_url(self, obj):
if obj.parent:
return reverse(
viewname='rest_api:indextemplatenode-detail', kwargs={
'index_template_id': obj.index.pk,
'index_template_node_id': obj.parent.pk
}, format=self.context['format'],
request=self.context['request']
)
else:
return ''
class IndexSerializer(serializers.ModelSerializer):
instance_root = IndexInstanceNodeSerializer(read_only=True)
node_templates = IndexTemplateNodeSerializer(read_only=True, many=True)
def get_url(self, obj):
return reverse(
viewname='rest_api:indextemplatenode-detail', kwargs={
'index_template_id': obj.index.pk,
'index_template_node_id': obj.pk
}, format=self.context['format'], request=self.context['request']
)
class IndexTemplateNodeWriteSerializer(serializers.ModelSerializer):
children = RecursiveField(many=True, read_only=True)
index_url = serializers.SerializerMethodField(read_only=True)
parent_url = serializers.SerializerMethodField(read_only=True)
url = serializers.SerializerMethodField(read_only=True)
class Meta:
fields = (
'document_types', 'enabled', 'id', 'instance_root', 'label',
'node_templates',
'children', 'enabled', 'expression', 'id', 'index', 'index_url',
'level', 'link_documents', 'parent', 'parent_url', 'url'
)
model = IndexTemplateNode
read_only_fields = ('children', 'id', 'index', 'level')
def create(self, validated_data):
validated_data['index'] = self.context['index_template']
return super().create(validated_data=validated_data)
def get_index_url(self, obj):
return reverse(
viewname='rest_api:indextemplate-detail', kwargs={
'index_template_id': obj.index.pk,
}, format=self.context['format'], request=self.context['request']
)
def get_parent_url(self, obj):
if obj.parent:
return reverse(
viewname='rest_api:indextemplatenode-detail', kwargs={
'index_template_id': obj.index.pk,
'index_template_node_id': obj.parent.pk
}, format=self.context['format'],
request=self.context['request']
)
else:
return ''
def get_url(self, obj):
return reverse(
viewname='rest_api:indextemplatenode-detail', kwargs={
'index_template_id': obj.index.pk,
'index_template_node_id': obj.pk
}, format=self.context['format'], request=self.context['request']
)
def validate(self, attrs):
parent = attrs.get('parent', None)
if not parent:
raise ValidationError(
{'parent': [_('Parent cannot be empty.')]}
)
else:
if not self.context['index_template'].node_templates.filter(id=parent.pk).exists():
raise ValidationError(
{
'parent': [
_('Parent must be from the same index template.')
]
}
)
return attrs
class IndexTemplateSerializer(serializers.HyperlinkedModelSerializer):