diff --git a/readthedocs/search/api/v2/serializers.py b/readthedocs/search/api/v2/serializers.py index 6ae6cd21f3c..98d8adc11aa 100644 --- a/readthedocs/search/api/v2/serializers.py +++ b/readthedocs/search/api/v2/serializers.py @@ -61,9 +61,11 @@ class PageSearchSerializer(serializers.Serializer): """ Page serializer. - If ``projects_data`` is passed into the context, the serializer - will try to use that to generate the link before querying the database. - It's a dictionary mapping the project slug to a ProjectData object. + If ``projects`` is passed in the constructor, the serializer + will pre-generate a cache with that information, + this is to avoid querying the database again for each result. + + :param projects: A list of tuples of project and version. """ type = serializers.CharField(default="page", source=None, read_only=True) @@ -76,6 +78,28 @@ class PageSearchSerializer(serializers.Serializer): highlights = PageHighlightSerializer(source="meta.highlight", default=dict) blocks = serializers.SerializerMethodField() + def __init__(self, *args, projects=None, **kwargs): + if projects: + context = kwargs.setdefault("context", {}) + context["projects_data"] = { + project.slug: self._build_project_data(project, version.slug) + for project, version in projects + } + super().__init__(*args, **kwargs) + + def _build_project_data(self, project, version_slug): + """Build a `ProjectData` object given a project and its version.""" + url = project.get_docs_url(version_slug=version_slug) + project_alias = project.superprojects.values_list("alias", flat=True).first() + version_data = VersionData( + slug=version_slug, + docs_url=url, + ) + return ProjectData( + alias=project_alias, + version=version_data, + ) + def _get_project_data(self, obj): """ Get and cache the project data. @@ -91,20 +115,8 @@ def _get_project_data(self, obj): project = Project.objects.filter(slug=obj.project).first() if project: - docs_url = project.get_docs_url(version_slug=obj.version) - project_alias = project.superprojects.values_list( - "alias", flat=True - ).first() - projects_data = self.context.setdefault("projects_data", {}) - version_data = VersionData( - slug=obj.version, - docs_url=docs_url, - ) - projects_data[obj.project] = ProjectData( - alias=project_alias, - version=version_data, - ) + projects_data[obj.project] = self._build_project_data(project, obj.version) return projects_data[obj.project] return None diff --git a/readthedocs/search/api/v2/views.py b/readthedocs/search/api/v2/views.py index d2613fc1a67..555e3549314 100644 --- a/readthedocs/search/api/v2/views.py +++ b/readthedocs/search/api/v2/views.py @@ -14,11 +14,7 @@ from readthedocs.projects.models import Feature, Project from readthedocs.search import tasks from readthedocs.search.api.pagination import SearchPagination -from readthedocs.search.api.v2.serializers import ( - PageSearchSerializer, - ProjectData, - VersionData, -) +from readthedocs.search.api.v2.serializers import PageSearchSerializer from readthedocs.search.faceted_search import PageSearch log = structlog.get_logger(__name__) @@ -80,45 +76,15 @@ def _validate_query_params(self): raise ValidationError(errors) @lru_cache(maxsize=1) - def _get_all_projects_data(self): - """ - Return a dictionary of the project itself and all its subprojects. - - Example: - - .. code:: - - { - "requests": ProjectData( - alias='alias', - version=VersionData( - "latest", - "https://requests.readthedocs.io/en/latest/", - ), - ), - "requests-oauth": ProjectData( - alias=None, - version=VersionData( - "latest", - "https://requests-oauth.readthedocs.io/en/latest/", - ), - ), - } - - .. note:: The response is cached into the instance. - - :rtype: A dictionary of project slugs mapped to a `VersionData` object. - """ + def _get_projects_to_search(self): + """Get all projects to search.""" main_version = self._get_version() main_project = self._get_project() if not self._has_permission(self.request.user, main_version): - return {} - - projects_data = { - main_project.slug: self._get_project_data(main_project, main_version), - } + return [] + projects_to_search = [(main_project, main_version)] subprojects = Project.objects.filter(superprojects__parent_id=main_project.id) for subproject in subprojects: version = self._get_project_version( @@ -136,24 +102,9 @@ def _get_all_projects_data(self): ) if version and self._has_permission(self.request.user, version): - projects_data[subproject.slug] = self._get_project_data( - subproject, version - ) + projects_to_search.append((subproject, version)) - return projects_data - - def _get_project_data(self, project, version): - """Build a `ProjectData` object given a project and its version.""" - url = project.get_docs_url(version_slug=version.slug) - project_alias = project.superprojects.values_list("alias", flat=True).first() - version_data = VersionData( - slug=version.slug, - docs_url=url, - ) - return ProjectData( - alias=project_alias, - version=version_data, - ) + return projects_to_search def _get_project_version(self, project, version_slug, include_hidden=True): """ @@ -219,8 +170,8 @@ def get_queryset(self): is compatible with DRF's paginator. """ projects = { - project: project_data.version.slug - for project, project_data in self._get_all_projects_data().items() + project.slug: version.slug + for project, version in self._get_projects_to_search() } # Check to avoid searching all projects in case it's empty. if not projects: @@ -236,11 +187,6 @@ def get_queryset(self): ) return queryset - def get_serializer_context(self): - context = super().get_serializer_context() - context["projects_data"] = self._get_all_projects_data() - return context - def get(self, request, *args, **kwargs): self._validate_query_params() result = self.list() @@ -255,7 +201,9 @@ def list(self): self.request, view=self, ) - serializer = self.get_serializer(page, many=True) + serializer = self.get_serializer( + page, many=True, projects=self._get_projects_to_search() + ) return self.paginator.get_paginated_response(serializer.data) diff --git a/readthedocs/search/tests/test_api.py b/readthedocs/search/tests/test_api.py index 18db6e70b7b..b6663ab8c50 100644 --- a/readthedocs/search/tests/test_api.py +++ b/readthedocs/search/tests/test_api.py @@ -342,9 +342,9 @@ def test_doc_search_unexisting_version(self, api_client, project): resp = self.get_search(api_client, search_params) assert resp.status_code == 404 - @mock.patch.object(PageSearchAPIView, '_get_all_projects_data', dict) + @mock.patch.object(PageSearchAPIView, "_get_projects_to_search", list) def test_get_all_projects_returns_empty_results(self, api_client, project): - """If there is a case where `_get_all_projects` returns empty, we could be querying all projects.""" + """If there is a case where `_get_projects_to_search` returns empty, we could be querying all projects.""" # `documentation` word is present both in `kuma` and `docs` files # and not in `pipeline`, so search with this phrase but filter through project diff --git a/readthedocs/search/views.py b/readthedocs/search/views.py index b9963541a5f..baa18478b4a 100644 --- a/readthedocs/search/views.py +++ b/readthedocs/search/views.py @@ -10,9 +10,7 @@ from readthedocs.projects.models import Feature, Project from readthedocs.search.api.v2.serializers import ( PageSearchSerializer, - ProjectData, ProjectSearchSerializer, - VersionData, ) from readthedocs.search.faceted_search import ALL_FACETS, PageSearch, ProjectSearch @@ -91,26 +89,6 @@ def _get_project(self, project_slug): project = get_object_or_404(queryset, slug=project_slug) return project - def _get_project_data(self, project, version_slug): - docs_url = project.get_docs_url(version_slug=version_slug) - version_data = VersionData( - slug=version_slug, - docs_url=docs_url, - ) - project_data = { - project.slug: ProjectData( - alias=None, - version=version_data, - ) - } - return project_data - - def get_serializer_context(self, project, version_slug): - context = { - 'projects_data': self._get_project_data(project, version_slug), - } - return context - def get(self, request, project_slug): project_obj = self._get_project(project_slug) use_advanced_query = not project_obj.has_feature( @@ -132,8 +110,7 @@ def get(self, request, project_slug): use_advanced_query=use_advanced_query, ) - context = self.get_serializer_context(project_obj, user_input.version) - results = PageSearchSerializer(results, many=True, context=context).data + results = PageSearchSerializer(results, many=True).data template_context = user_input._asdict() template_context.update({