Skip to content

Commit aa92a07

Browse files
committed
Search: refactor api view
Mainly this was done to allow us support searching on different versions of subprojects. Currently, we assume we search the same version for all subprojects. - _get_all_projects_data and _get_all_projects were merged into just one method. And instead of returning a list of projects we return a dictionary of project slugs mapped to a VersionData object. - There is some duplication in .com. `_has_permission` and `_get_subproject_versions_queryset` are overridden in .com. - ProjectData was renamed to VersionData since it has more attributes related to a version than a project.
1 parent c5878dc commit aa92a07

File tree

4 files changed

+82
-59
lines changed

4 files changed

+82
-59
lines changed

readthedocs/search/api.py

+64-49
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from readthedocs.search import tasks
1616
from readthedocs.search.faceted_search import PageSearch
1717

18-
from .serializers import PageSearchSerializer
18+
from .serializers import PageSearchSerializer, VersionData
1919

2020
log = logging.getLogger(__name__)
2121

@@ -185,67 +185,82 @@ def _validate_query_params(self):
185185

186186
def _get_all_projects_data(self):
187187
"""
188-
Return a dict containing the project slug and its version URL and version's doctype.
189-
190-
The dictionary contains the project and its subprojects. Each project's
191-
slug is used as a key and a tuple with the documentation URL and doctype
192-
from the version. Example:
193-
194-
{
195-
"requests": (
196-
"https://requests.readthedocs.io/en/latest/",
197-
"sphinx",
198-
),
199-
"requests-oauth": (
200-
"https://requests-oauth.readthedocs.io/en/latest/",
201-
"sphinx_htmldir",
202-
),
203-
}
188+
Return a dictionary of the project itself and all its subprojects.
204189
205-
:rtype: dict
206-
"""
207-
all_projects = self._get_all_projects()
208-
version_slug = self._get_version().slug
209-
project_urls = {}
210-
for project in all_projects:
211-
project_urls[project.slug] = project.get_docs_url(version_slug=version_slug)
212-
213-
versions_doctype = (
214-
Version.objects
215-
.filter(project__slug__in=project_urls.keys(), slug=version_slug)
216-
.values_list('project__slug', 'documentation_type')
217-
)
190+
Example:
218191
219-
projects_data = {
220-
project_slug: (project_urls[project_slug], doctype)
221-
for project_slug, doctype in versions_doctype
222-
}
223-
return projects_data
192+
.. code::
224193
225-
def _get_all_projects(self):
226-
"""
227-
Returns a list of the project itself and all its subprojects the user has permissions over.
194+
{
195+
"requests": VersionData(
196+
"latest",
197+
"sphinx",
198+
"https://requests.readthedocs.io/en/latest/",
199+
),
200+
"requests-oauth": VersionData(
201+
"latest",
202+
"sphinx_htmldir",
203+
"https://requests-oauth.readthedocs.io/en/latest/",
204+
),
205+
}
228206
229-
:rtype: list
207+
.. note:: The response is cached into the instance.
208+
209+
:rtype: A dictionary of project slugs mapped to a `VersionData` object.
230210
"""
211+
cache_key = '__cached_projects_data'
212+
projects_data = getattr(self, cache_key, None)
213+
if projects_data is not None:
214+
return projects_data
215+
231216
main_version = self._get_version()
232217
main_project = self._get_project()
233218

234-
all_projects = [main_project]
219+
projects_data = {
220+
main_project.slug: VersionData(
221+
slug=main_version.slug,
222+
doctype=main_version.documentation_type,
223+
docs_url=main_project.get_docs_url(version_slug=main_version.slug),
224+
)
225+
}
235226

236227
subprojects = Project.objects.filter(
237228
superprojects__parent_id=main_project.id,
238229
)
239230
for project in subprojects:
240-
version = (
241-
Version.internal
242-
.public(user=self.request.user, project=project, include_hidden=False)
243-
.filter(slug=main_version.slug)
244-
.first()
231+
version = self._get_subproject_version(
232+
version_slug=main_version.slug,
233+
subproject=project,
245234
)
246-
if version:
247-
all_projects.append(version.project)
248-
return all_projects
235+
if version and self._has_permission(self.request.user, version):
236+
url = project.get_docs_url(version_slug=version.slug)
237+
projects_data[project.slug] = VersionData(
238+
slug=version.slug,
239+
doctype=version.documentation_type,
240+
docs_url=url,
241+
)
242+
243+
setattr(self, cache_key, projects_data)
244+
return projects_data
245+
246+
def _get_subproject_version(self, version_slug, subproject):
247+
"""Get a version from the subproject."""
248+
return (
249+
Version.internal
250+
.public(user=self.request.user, project=subproject, include_hidden=False)
251+
.filter(slug=version_slug)
252+
.first()
253+
)
254+
255+
def _has_permission(self, user, version):
256+
"""
257+
Check if `user` is authorized to access `version`.
258+
259+
The queryset from `_get_subproject_version` already filters public
260+
projects. This is mainly to be overriden in .com to make use of
261+
the auth backends in the proxied API.
262+
"""
263+
return True
249264

250265
def _record_query(self, response):
251266
project_slug = self._get_project().slug
@@ -276,7 +291,7 @@ def get_queryset(self):
276291
is compatible with DRF's paginator.
277292
"""
278293
filters = {}
279-
filters['project'] = [p.slug for p in self._get_all_projects()]
294+
filters['project'] = list(self._get_all_projects_data().keys())
280295
filters['version'] = self._get_version().slug
281296

282297
# Check to avoid searching all projects in case these filters are empty.

readthedocs/search/serializers.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from readthedocs.projects.models import Project
2121

2222

23-
# Structure used for storing cached data of a project mostly.
24-
ProjectData = namedtuple('ProjectData', ['docs_url', 'version_doctype'])
23+
# Structure used for storing cached data of a version mostly.
24+
VersionData = namedtuple('VersionData', ['slug', 'docs_url', 'doctype'])
2525

2626

2727
class ProjectHighlightSerializer(serializers.Serializer):
@@ -89,14 +89,14 @@ def _get_full_path(self, obj):
8989
it's cached into ``project_data``.
9090
"""
9191
# First try to build the URL from the context.
92-
project_data = self.context.get('projects_data', {}).get(obj.project)
93-
if project_data:
94-
docs_url, doctype = project_data
92+
version_data = self.context.get('projects_data', {}).get(obj.project)
93+
if version_data:
94+
docs_url = version_data.docs_url
9595
path = obj.full_path
9696

9797
# Generate an appropriate link for the doctypes that use htmldir,
9898
# and always end it with / so it goes directly to proxito.
99-
if doctype in {SPHINX_HTMLDIR, MKDOCS}:
99+
if version_data.doctype in {SPHINX_HTMLDIR, MKDOCS}:
100100
path = re.sub('(^|/)index.html$', '/', path)
101101

102102
return docs_url.rstrip('/') + '/' + path.lstrip('/')
@@ -107,7 +107,11 @@ def _get_full_path(self, obj):
107107
docs_url = project.get_docs_url(version_slug=obj.version)
108108
# cache the project URL
109109
projects_data = self.context.setdefault('projects_data', {})
110-
projects_data[obj.project] = ProjectData(docs_url, '')
110+
projects_data[obj.project] = VersionData(
111+
slug=obj.version,
112+
docs_url=docs_url,
113+
doctype=None,
114+
)
111115
return docs_url + obj.full_path
112116

113117
return None

readthedocs/search/tests/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def test_doc_search_unexisting_version(self, api_client, project):
293293
resp = self.get_search(api_client, search_params)
294294
assert resp.status_code == 404
295295

296-
@mock.patch.object(PageSearchAPIView, '_get_all_projects', list)
296+
@mock.patch.object(PageSearchAPIView, '_get_all_projects_data', dict)
297297
def test_get_all_projects_returns_empty_results(self, api_client, project):
298298
"""If there is a case where `_get_all_projects` returns empty, we could be querying all projects."""
299299

readthedocs/search/views.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
from .serializers import (
1717
PageSearchSerializer,
18-
ProjectData,
1918
ProjectSearchSerializer,
19+
VersionData,
2020
)
2121

2222
log = logging.getLogger(__name__)
@@ -63,7 +63,11 @@ def _get_project_data(self, project, version_slug):
6363
)
6464
docs_url = project.get_docs_url(version_slug=version_slug)
6565
project_data = {
66-
project.slug: ProjectData(docs_url, version_doctype)
66+
project.slug: VersionData(
67+
slug=version_slug,
68+
docs_url=docs_url,
69+
doctype=version_doctype,
70+
)
6771
}
6872
return project_data
6973

0 commit comments

Comments
 (0)