Skip to content

Commit 258908f

Browse files
committed
Refactor search view to make use of permission_classes
This is the same as #6133 but for the search view. This allow us to override `get_all_projects` in .com to filter projects using the auth backends. And make use of the permission class from .com that makes use of the auth backends. This doesn't break the current search endpoint in .com since it filters the versions using `public(user=self.request.user)` This is required to proxy search on .com
1 parent 50201cc commit 258908f

File tree

4 files changed

+73
-49
lines changed

4 files changed

+73
-49
lines changed

readthedocs/api/v2/permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class IsAuthorizedToViewVersion(permissions.BasePermission):
9191
"""
9292
Checks if the user from the request has permissions to see the version.
9393
94-
This permission class used in the FooterHTML view.
94+
This permission class used in the FooterHTML and PageSearchAPIView views.
9595
9696
.. note::
9797

readthedocs/search/api.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import itertools
22
import logging
33

4+
from django.shortcuts import get_object_or_404
45
from django.utils import timezone
56
from rest_framework import generics, serializers
67
from rest_framework.exceptions import ValidationError
78
from rest_framework.pagination import PageNumberPagination
89

9-
from readthedocs.projects.models import HTMLFile
10+
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
11+
from readthedocs.builds.models import Version
12+
from readthedocs.projects.models import HTMLFile, Project
1013
from readthedocs.search import tasks, utils
1114
from readthedocs.search.faceted_search import PageSearch
1215

13-
1416
log = logging.getLogger(__name__)
1517

1618

@@ -60,11 +62,50 @@ def get_inner_hits(self, obj):
6062

6163
class PageSearchAPIView(generics.ListAPIView):
6264

63-
"""Main entry point to perform a search using Elasticsearch."""
65+
"""
66+
Main entry point to perform a search using Elasticsearch.
67+
68+
Required query params:
69+
- q (search term)
70+
- project
71+
- version
72+
73+
.. note::
74+
75+
The methods `_get_project` and `_get_version`
76+
are called many times, so a basic cache is implemented.
77+
"""
6478

79+
permission_classes = [IsAuthorizedToViewVersion]
6580
pagination_class = SearchPagination
6681
serializer_class = PageSearchSerializer
6782

83+
def _get_project(self):
84+
cache_key = '_cached_project'
85+
project = getattr(self, cache_key, None)
86+
87+
if not project:
88+
project_slug = self.request.GET.get('project', None)
89+
project = get_object_or_404(Project, slug=project_slug)
90+
setattr(self, cache_key, project)
91+
92+
return project
93+
94+
def _get_version(self):
95+
cache_key = '_cached_version'
96+
version = getattr(self, cache_key, None)
97+
98+
if not version:
99+
version_slug = self.request.GET.get('version', None)
100+
project = self._get_project()
101+
version = get_object_or_404(
102+
project.versions.all(),
103+
slug=version_slug,
104+
)
105+
setattr(self, cache_key, version)
106+
107+
return version
108+
68109
def get_queryset(self):
69110
"""
70111
Return Elasticsearch DSL Search object instead of Django Queryset.
@@ -78,13 +119,7 @@ def get_queryset(self):
78119
query = self.request.query_params.get('q', '')
79120
kwargs = {'filter_by_user': False, 'filters': {}}
80121
kwargs['filters']['project'] = [p.slug for p in self.get_all_projects()]
81-
kwargs['filters']['version'] = self.request.query_params.get('version')
82-
if not kwargs['filters']['project']:
83-
log.info("Unable to find a project to search")
84-
return HTMLFile.objects.none()
85-
if not kwargs['filters']['version']:
86-
log.info("Unable to find a version to search")
87-
return HTMLFile.objects.none()
122+
kwargs['filters']['version'] = self._get_version().slug
88123
user = self.request.user
89124
queryset = PageSearch(
90125
query=query, user=user, **kwargs
@@ -120,17 +155,24 @@ def get_all_projects(self):
120155
"""
121156
Return a list containing the project itself and all its subprojects.
122157
123-
The project slug is retrieved from ``project`` query param.
124-
125158
:rtype: list
126-
127-
:raises: Http404 if project is not found
128159
"""
129-
project_slug = self.request.query_params.get('project')
130-
version_slug = self.request.query_params.get('version')
131-
all_projects = utils.get_project_list_or_404(
132-
project_slug=project_slug, user=self.request.user, version_slug=version_slug,
160+
main_version = self._get_version()
161+
main_project = self._get_project()
162+
163+
subprojects = Project.objects.filter(
164+
superprojects__parent_id=main_project.id,
133165
)
166+
all_projects = []
167+
for project in list(subprojects) + [main_project]:
168+
version = (
169+
Version.objects
170+
.public(user=self.request.user, project=project)
171+
.filter(slug=main_version.slug)
172+
.first()
173+
)
174+
if version:
175+
all_projects.append(version.project)
134176
return all_projects
135177

136178
def get_all_projects_url(self):
@@ -151,7 +193,7 @@ def get_all_projects_url(self):
151193
:rtype: dict
152194
"""
153195
all_projects = self.get_all_projects()
154-
version_slug = self.request.query_params.get('version')
196+
version_slug = self._get_version().slug
155197
projects_url = {}
156198
for project in all_projects:
157199
projects_url[project.slug] = project.get_docs_url(version_slug=version_slug)
@@ -162,8 +204,8 @@ def list(self, request, *args, **kwargs):
162204

163205
response = super().list(request, *args, **kwargs)
164206

165-
project_slug = self.request.query_params.get('project', None)
166-
version_slug = self.request.query_params.get('version', None)
207+
project_slug = self._get_project().slug
208+
version_slug = self._get_version().slug
167209
total_results = response.data.get('count', 0)
168210
time = timezone.now()
169211

readthedocs/search/tests/test_api.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,17 @@ def test_doc_search_pagination(self, api_client, project):
200200
assert len(resp.data['results']) == 5
201201

202202
def test_doc_search_without_parameters(self, api_client, project):
203-
"""Hitting Document Search endpoint without query parameters should return error"""
203+
"""Hitting Document Search endpoint without project and version should return 404."""
204204
resp = self.get_search(api_client, {})
205+
assert resp.status_code == 404
206+
207+
def test_doc_search_without_query(self, api_client, project):
208+
"""Hitting Document Search endpoint without a query should return error."""
209+
resp = self.get_search(
210+
api_client, {'project': project.slug, 'version': project.versions.first().slug})
205211
assert resp.status_code == 400
206212
# Check error message is there
207-
assert sorted(['q', 'project', 'version']) == sorted(resp.data.keys())
213+
assert 'q' in resp.data.keys()
208214

209215
def test_doc_search_subprojects(self, api_client, all_projects):
210216
"""Test Document search return results from subprojects also"""
@@ -256,7 +262,4 @@ def test_doc_search_unexisting_version(self, api_client, project):
256262
'version': version,
257263
}
258264
resp = self.get_search(api_client, search_params)
259-
assert resp.status_code == 200
260-
261-
data = resp.data['results']
262-
assert len(data) == 0
265+
assert resp.status_code == 404

readthedocs/search/utils.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,6 @@ def remove_indexed_files(model, project_slug, version_slug=None, build_id=None):
8282
log.exception('Unable to delete a subset of files. Continuing.')
8383

8484

85-
# TODO: Rewrite all the views using this in Class Based View,
86-
# and move this function to a mixin
87-
def get_project_list_or_404(project_slug, user, version_slug=None):
88-
"""
89-
Return list of project and its subprojects.
90-
91-
It filters by Version privacy instead of Project privacy,
92-
so we can support public versions on private projects.
93-
"""
94-
project_list = []
95-
main_project = get_object_or_404(Project, slug=project_slug)
96-
subprojects = Project.objects.filter(superprojects__parent_id=main_project.id)
97-
for project in list(subprojects) + [main_project]:
98-
version = Version.internal.public(user).filter(
99-
project__slug=project.slug, slug=version_slug
100-
)
101-
if version.exists():
102-
project_list.append(version.first().project)
103-
return project_list
104-
105-
10685
def _get_index(indices, index_name):
10786
"""
10887
Get Index from all the indices.

0 commit comments

Comments
 (0)