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:
@@ -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)
|
||||
==================
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user