Refactor dynamic search app API

Convert the API to use viewsets.

The search function is now a service of the search model
resource.

The simple and advance search are now the same service. The
difference is determined by the URL query. A ?q= means a
simple search. For advanced search pass the search model
fields in the URL query, example: ?q=document_type__label=

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-02-23 05:05:34 -04:00
parent 23d56c3147
commit 8599d69d23
7 changed files with 120 additions and 122 deletions

View File

@@ -244,7 +244,12 @@
- Remove the sidebar menu and unify its links with the
secondary menu.
- Increate the default maximum title lenght to 120 characters.
- In the search API, the search function is now a service
of the search model resource.
- The simple and advance search are now the same service. The
difference is determined by the URL query. A ?q= means a
simple search. For advanced search pass the search model
fields in the URL query, example: ?q=document_type__label=
3.1.9 (2018-11-01)
==================

View File

@@ -2,91 +2,56 @@ from __future__ import unicode_literals
from django.utils.encoding import force_text
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.exceptions import ParseError
from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter
from mayan.apps.rest_api.viewsets import MayanAPIReadOnlyModelViewSet
from .classes import SearchModel
from .mixins import SearchModelMixin
from .serializers import SearchModelSerializer
class APISearchView(SearchModelMixin, generics.ListAPIView):
"""
get: Perform a search operation
"""
filter_backends = (MayanObjectPermissionsFilter,)
class SearchModelAPIViewSet(MayanAPIReadOnlyModelViewSet):
lookup_field = 'name'
lookup_url_kwarg = 'search_model_name'
lookup_value_regex = r'[\w\.]+'
serializer_class = SearchModelSerializer
queryset = SearchModel.all()
def get_queryset(self):
search_model = self.get_search_model()
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
return SearchModel.get(**filter_kwargs)
@action(detail=True, url_name='search', url_path='search')
def search(self, request, *args, **kwargs):
search_model = self.get_object()
# Override serializer class just before producing the queryset of
# search results
self.serializer_class = search_model.serializer
if search_model.permission:
self.mayan_object_permissions = {'GET': (search_model.permission,)}
try:
queryset = search_model.search(
query_string=self.request.GET, user=self.request.user
)
except Exception as exception:
raise ParseError(force_text(exception))
return queryset
def get_serializer(self, *args, **kwargs):
if self.get_search_model_name():
return super(APISearchView, self).get_serializer(*args, **kwargs)
else:
return None
class APIAdvancedSearchView(SearchModelMixin, generics.ListAPIView):
"""
get: Perform an advanced search operation
"""
filter_backends = (MayanObjectPermissionsFilter,)
def get_queryset(self):
self.search_model = self.get_search_model()
# Override serializer class just before producing the queryset of
# search results
self.serializer_class = self.search_model.serializer
if self.search_model.permission:
self.mayan_object_permissions = {
'GET': (self.search_model.permission,)
}
if self.request.GET.get('_match_all', 'off') == 'on':
global_and_search = True
else:
global_and_search = False
try:
queryset = self.search_model.search(
query_string=self.request.GET, user=self.request.user,
global_and_search=global_and_search
queryset = search_model.search(
global_and_search=global_and_search,
query_string=self.request.GET, user=self.request.user
)
except Exception as exception:
raise ParseError(force_text(exception))
return queryset
page = self.paginate_queryset(queryset)
def get_serializer(self, *args, **kwargs):
if self.get_search_model_name():
return super(APISearchView, self).get_serializer(*args, **kwargs)
else:
return None
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
class APISearchModelList(generics.ListAPIView):
"""
get: Returns a list of all the available search models.
"""
serializer_class = SearchModelSerializer
queryset = SearchModel.all()
return Response(serializer.data)

View File

@@ -84,9 +84,9 @@ class SearchModel(object):
return cls._registry
@classmethod
def get(cls, full_name):
def get(cls, name):
try:
result = cls._registry[full_name]
result = cls._registry[name]
except KeyError:
raise KeyError(_('No search model matching the query'))
if not hasattr(result, 'serializer'):

View File

@@ -12,6 +12,6 @@ class SearchModelMixin(object):
def get_search_model(self):
try:
return SearchModel.get(self.get_search_model_name())
return SearchModel.get(name=self.get_search_model_name())
except KeyError as exception:
raise Http404(force_text(exception))

View File

@@ -13,3 +13,13 @@ class SearchModelSerializer(serializers.Serializer):
model_name = serializers.CharField(read_only=True)
pk = serializers.CharField(read_only=True)
search_fields = SearchFieldSerializer(many=True, read_only=True)
search_url = serializers.HyperlinkedIdentityField(
lookup_field='pk',
lookup_url_kwarg='search_model_name',
view_name='rest_api:search_model-search'
)
url = serializers.HyperlinkedIdentityField(
lookup_field='pk',
lookup_url_kwarg='search_model_name',
view_name='rest_api:search_model-detail'
)

View File

@@ -1,7 +1,5 @@
from __future__ import unicode_literals
from django.urls import reverse
from rest_framework import status
from mayan.apps.documents.permissions import permission_document_view
@@ -12,46 +10,10 @@ from mayan.apps.rest_api.tests import BaseAPITestCase
from ..classes import SearchModel
class SearchAPITestCase(DocumentTestMixin, BaseAPITestCase):
auto_upload_document = False
def setUp(self):
super(SearchAPITestCase, self).setUp()
self.login_user()
def _request_search_view(self):
return self.get(
path='{}?q={}'.format(
reverse(
viewname='rest_api:search-view', kwargs={
'search_model': document_search.get_full_name()
}
), self.document.label
)
)
def test_search_no_access(self):
self.document = self.upload_document()
response = self._request_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)
def test_search_with_access(self):
self.document = self.upload_document()
self.grant_access(
permission=permission_document_view, obj=self.document
)
response = self._request_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['label'], self.document.label
)
self.assertEqual(response.data['count'], 1)
def test_search_models_view(self):
class SearchModelAPITestCase(BaseAPITestCase):
def test_search_models_list_view(self):
response = self.get(
viewname='rest_api:searchmodel-list'
viewname='rest_api:search_model-list'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -59,3 +21,67 @@ class SearchAPITestCase(DocumentTestMixin, BaseAPITestCase):
[search_model['pk'] for search_model in response.data['results']],
[search_model.pk for search_model in SearchModel.all()]
)
class SearchModelSearchAPITestCase(DocumentTestMixin, BaseAPITestCase):
auto_create_document_type = False
auto_upload_document = False
def setUp(self):
super(SearchModelSearchAPITestCase, self).setUp()
self._create_document_type_random()
self._create_document()
self._create_document_type_random()
self._create_document()
def _request_search_view(self):
query = {'q': self.test_document.document_type.label}
return self.get(
viewname='rest_api:search_model-search', kwargs={
'search_model_name': document_search.get_full_name()
}, query=query
)
def test_search_api_view_no_permission(self):
response = self._request_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)
def test_search_api_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_view,
)
response = self._request_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['label'], self.test_document.label
)
self.assertEqual(response.data['count'], 1)
def _request_advanced_search_view(self):
query = {'document_type__label': self.test_document.document_type.label}
return self.get(
viewname='rest_api:search_model-search', kwargs={
'search_model_name': document_search.get_full_name()
}, query=query
)
def test_advanced_search_api_view_no_permission(self):
response = self._request_advanced_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 0)
def test_advanced_search_api_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_view,
)
response = self._request_advanced_search_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['label'], self.test_document.label
)
self.assertEqual(response.data['count'], 1)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import APIAdvancedSearchView, APISearchModelList, APISearchView
from .api_views import SearchModelAPIViewSet
from .views import AdvancedSearchView, ResultsView, SearchAgainView, SearchView
urlpatterns = [
@@ -24,17 +24,9 @@ urlpatterns = [
)
]
api_urls = [
url(
regex=r'^search_models/$', name='searchmodel-list',
view=APISearchModelList.as_view()
),
url(
regex=r'^search/(?P<search_model>[\.\w]+)/$', name='search-view',
view=APISearchView.as_view()
),
url(
regex=r'^search/advanced/(?P<search_model>[\.\w]+)/$',
name='advanced-search-view', view=APIAdvancedSearchView.as_view()
),
]
api_router_entries = (
{
'prefix': r'search_models', 'viewset': SearchModelAPIViewSet,
'basename': 'search_model'
},
)