From 8599d69d23072a9ab5ea70f5f31482a6db401d0a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 23 Feb 2019 05:05:34 -0400 Subject: [PATCH] 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 --- HISTORY.rst | 7 +- mayan/apps/dynamic_search/api_views.py | 89 +++++----------- mayan/apps/dynamic_search/classes.py | 4 +- mayan/apps/dynamic_search/mixins.py | 2 +- mayan/apps/dynamic_search/serializers.py | 10 ++ mayan/apps/dynamic_search/tests/test_api.py | 108 ++++++++++++-------- mayan/apps/dynamic_search/urls.py | 22 ++-- 7 files changed, 120 insertions(+), 122 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index cfacdc86aa..321dfc6963 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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) ================== diff --git a/mayan/apps/dynamic_search/api_views.py b/mayan/apps/dynamic_search/api_views.py index e6d8eb1ac2..f6c235aad8 100644 --- a/mayan/apps/dynamic_search/api_views.py +++ b/mayan/apps/dynamic_search/api_views.py @@ -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) diff --git a/mayan/apps/dynamic_search/classes.py b/mayan/apps/dynamic_search/classes.py index d49f5af4dc..2fe32892d1 100644 --- a/mayan/apps/dynamic_search/classes.py +++ b/mayan/apps/dynamic_search/classes.py @@ -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'): diff --git a/mayan/apps/dynamic_search/mixins.py b/mayan/apps/dynamic_search/mixins.py index 225adf104c..9808cbdf1d 100644 --- a/mayan/apps/dynamic_search/mixins.py +++ b/mayan/apps/dynamic_search/mixins.py @@ -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)) diff --git a/mayan/apps/dynamic_search/serializers.py b/mayan/apps/dynamic_search/serializers.py index 09c2442dff..06fd62bb24 100644 --- a/mayan/apps/dynamic_search/serializers.py +++ b/mayan/apps/dynamic_search/serializers.py @@ -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' + ) diff --git a/mayan/apps/dynamic_search/tests/test_api.py b/mayan/apps/dynamic_search/tests/test_api.py index 9af5df8eea..0efbf27004 100644 --- a/mayan/apps/dynamic_search/tests/test_api.py +++ b/mayan/apps/dynamic_search/tests/test_api.py @@ -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) diff --git a/mayan/apps/dynamic_search/urls.py b/mayan/apps/dynamic_search/urls.py index a2fd2eb969..8789ff5025 100644 --- a/mayan/apps/dynamic_search/urls.py +++ b/mayan/apps/dynamic_search/urls.py @@ -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[\.\w]+)/$', name='search-view', - view=APISearchView.as_view() - ), - url( - regex=r'^search/advanced/(?P[\.\w]+)/$', - name='advanced-search-view', view=APIAdvancedSearchView.as_view() - ), -] +api_router_entries = ( + { + 'prefix': r'search_models', 'viewset': SearchModelAPIViewSet, + 'basename': 'search_model' + }, +)