From 4abfe61a7f7ab2d56bac42089376e2f019db48e2 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 3 Apr 2023 19:07:23 +0200 Subject: [PATCH 01/10] API: hosting integrations endpoint versioning/structure Review initial JSON response to keep consistency with APIv3 resources. It uses a small modified version of the APIv3 serializers for known resources, removing some URL fields that can't be resolved from El Proxito. Sharing the same serializers that our APIv3 make our response a lot more consitent between the endpoints and allow us to expand the behavior by adding more fields in a structured way. Besides, these changes include a minimal checking of the version coming from `X-RTD-Hosting-Integrations-Version` header to decide which JSON structure return, allowing us to support multiple version at the same time in case we require to do some breaking changes. The JavaScript client is already sending this header in all of its requests. See https://github.com/readthedocs/readthedocs-client/issues/28 --- readthedocs/proxito/tests/test_hosting.py | 139 ++++++++----------- readthedocs/proxito/views/hosting.py | 161 +++++++++++++++++++--- 2 files changed, 199 insertions(+), 101 deletions(-) diff --git a/readthedocs/proxito/tests/test_hosting.py b/readthedocs/proxito/tests/test_hosting.py index fb3569d18e2..8b14bfc226d 100644 --- a/readthedocs/proxito/tests/test_hosting.py +++ b/readthedocs/proxito/tests/test_hosting.py @@ -1,13 +1,15 @@ """Test hosting views.""" +import json +from pathlib import Path + import django_dynamic_fixture as fixture import pytest -from django.conf import settings from django.contrib.auth.models import User from django.test import TestCase, override_settings from django.urls import reverse -from readthedocs.builds.constants import EXTERNAL, INTERNAL, LATEST +from readthedocs.builds.constants import LATEST from readthedocs.builds.models import Build from readthedocs.projects.constants import PUBLIC from readthedocs.projects.models import Project @@ -21,114 +23,89 @@ @pytest.mark.proxito class TestReadTheDocsConfigJson(TestCase): def setUp(self): - self.user = fixture.get(User, username="user") - self.user.set_password("user") + self.user = fixture.get(User, username="testuser") + self.user.set_password("testuser") self.user.save() self.project = fixture.get( Project, slug="project", + name="project", language="en", privacy_level=PUBLIC, external_builds_privacy_level=PUBLIC, - repo="git://10.10.0.1/project", + repo="https://github.com/readthedocs/project", programming_language="words", single_version=False, users=[self.user], main_language_project=None, + project_url="http://project.com", ) + + for tag in ("tag", "project", "test"): + self.project.tags.add(tag) + self.project.versions.update( privacy_level=PUBLIC, built=True, active=True, - type=INTERNAL, - identifier="1a2b3c", + type="tag", + identifier="a1b2c3", ) self.version = self.project.versions.get(slug=LATEST) self.build = fixture.get( Build, + project=self.project, version=self.version, + commit="a1b2c3", + length=60, + state="finished", + success=True, + ) + + def _get_response_dict(self, view_name, filepath=None): + filepath = filepath or __file__ + filename = Path(filepath).absolute().parent / "responses" / f"{view_name}.json" + return json.load(open(filename)) + + def _normalize_datetime_fields(self, obj): + obj["project"]["created"] = "2019-04-29T10:00:00Z" + obj["project"]["modified"] = "2019-04-29T12:00:00Z" + obj["build"]["created"] = "2019-04-29T10:00:00Z" + obj["build"]["finished"] = "2019-04-29T10:01:00Z" + return obj + + def test_get_config_v0(self): + r = self.client.get( + reverse("proxito_readthedocs_config_json"), + {"url": "https://project.dev.readthedocs.io/en/latest/"}, + secure=True, + HTTP_HOST="project.dev.readthedocs.io", + HTTP_X_RTD_HOSTING_INTEGRATIONS_VERSION="0.1.0", + ) + assert r.status_code == 200 + assert self._normalize_datetime_fields(r.json()) == self._get_response_dict( + "v0" ) - def test_get_config(self): + def test_get_config_v1(self): r = self.client.get( reverse("proxito_readthedocs_config_json"), {"url": "https://project.dev.readthedocs.io/en/latest/"}, secure=True, HTTP_HOST="project.dev.readthedocs.io", + HTTP_X_RTD_HOSTING_INTEGRATIONS_VERSION="1.0.0", ) assert r.status_code == 200 + assert r.json() == self._get_response_dict("v1") - expected = { - "comment": "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!", - "project": { - "slug": self.project.slug, - "language": self.project.language, - "repository_url": self.project.repo, - "programming_language": self.project.programming_language, - }, - "version": { - "slug": self.version.slug, - "external": self.version.type == EXTERNAL, - }, - "build": { - "id": self.build.pk, - }, - "domains": { - "dashboard": settings.PRODUCTION_DOMAIN, - }, - "readthedocs": { - "analytics": { - "code": None, - } - }, - "features": { - "analytics": { - "code": None, - }, - "external_version_warning": { - "enabled": True, - "query_selector": "[role=main]", - }, - "non_latest_version_warning": { - "enabled": True, - "query_selector": "[role=main]", - "versions": [ - "latest", - ], - }, - "doc_diff": { - "enabled": True, - "base_url": "https://project.dev.readthedocs.io/en/latest/index.html", - "root_selector": "[role=main]", - "inject_styles": True, - "base_host": "", - "base_page": "", - }, - "flyout": { - "translations": [], - "versions": [ - {"slug": "latest", "url": "/en/latest/"}, - ], - "downloads": [], - "vcs": { - "url": "https://github.com", - "username": "readthedocs", - "repository": "test-builds", - "branch": self.version.identifier, - "filepath": "/docs/index.rst", - }, - }, - "search": { - "api_endpoint": "/_/api/v3/search/", - "default_filter": "subprojects:project/latest", - "filters": [ - ["Search only in this project", "project:project/latest"], - ["Search subprojects", "subprojects:project/latest"], - ], - "project": "project", - "version": "latest", - }, - }, - } - assert r.json() == expected + def test_get_config_unsupported_version(self): + r = self.client.get( + reverse("proxito_readthedocs_config_json"), + {"url": "https://project.dev.readthedocs.io/en/latest/"}, + secure=True, + HTTP_HOST="project.dev.readthedocs.io", + HTTP_X_RTD_HOSTING_INTEGRATIONS_VERSION="2.0.0", + ) + assert r.status_code == 400 + assert r.json() == self._get_response_dict("v2") diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 2ed7ef4fb88..2c9f94741e7 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -1,11 +1,16 @@ """Views for hosting features.""" +import packaging import structlog from django.conf import settings from django.http import JsonResponse from django.views import View -from readthedocs.builds.constants import EXTERNAL +from readthedocs.api.v3.serializers import ( + BuildSerializer, + ProjectSerializer, + VersionSerializer, +) from readthedocs.core.mixins import CDNCacheControlMixin from readthedocs.core.resolver import resolver from readthedocs.core.unresolver import unresolver @@ -13,7 +18,34 @@ log = structlog.get_logger(__name__) # noqa +CLIENT_VERSIONS_SUPPORTED = (0, 1) + + +class ClientError(Exception): + VERSION_NOT_CURRENTLY_SUPPORTED = ( + "The version specified in 'X-RTD-Hosting-Integrations-Version'" + " is currently not supported" + ) + VERSION_INVALID = "'X-RTD-Hosting-Integrations-Version' header version is invalid" + VERSION_HEADER_MISSING = ( + "'X-RTD-Hosting-Integrations-Version' header attribute is required" + ) + + class ReadTheDocsConfigJson(CDNCacheControlMixin, View): + + """ + API response consumed by our JavaScript client. + + The code for the JavaScript client lives at: + https://github.com/readthedocs/readthedocs-client/ + + Attributes: + + url (required): absolute URL from where the request is performed + (e.g. ``window.location.href``) + """ + def get(self, request): url = request.GET.get("url") @@ -23,37 +55,120 @@ def get(self, request): status=400, ) + client_version = request.headers.get("X-RTD-Hosting-Integrations-Version") + if not client_version: + return JsonResponse( + { + "error": ClientError.VERSION_HEADER_MISSING, + }, + status=400, + ) + try: + client_version = packaging.version.parse(client_version) + if client_version.major not in CLIENT_VERSIONS_SUPPORTED: + raise ClientError + except packaging.version.InvalidVersion: + return JsonResponse( + { + "error": ClientError.VERSION_INVALID, + }, + status=400, + ) + except ClientError: + return JsonResponse( + {"error": ClientError.VERSION_NOT_CURRENTLY_SUPPORTED}, + status=400, + ) + unresolved_domain = request.unresolved_domain project = unresolved_domain.project unresolved_url = unresolver.unresolve_url(url) version = unresolved_url.version + filename = unresolved_url.filename project.get_default_version() build = version.builds.last() - # TODO: define how it will be the exact JSON object returned here - # NOTE: we could use the APIv3 serializers for some of these objects - # if we want to keep consistency. However, those may require some - # extra db calls that we probably want to avoid. + data = ClientResponse().get(client_version, project, version, build, filename) + return JsonResponse(data, json_dumps_params=dict(indent=4)) + + +class NoLinksMixin: + + """Mixin to remove conflicting fields from serializers.""" + + FIELDS_TO_REMOVE = ( + "_links", + "urls", + ) + + def __init__(self, *args, **kwargs): + super(NoLinksMixin, self).__init__(*args, **kwargs) + + for field in self.FIELDS_TO_REMOVE: + if field in self.fields: + del self.fields[field] + + if field in self.Meta.fields: + del self.Meta.fields[self.Meta.fields.index(field)] + + +# NOTE: the following serializers are required only to remove some fields we +# can't expose yet in this API endpoint because it running under El Proxito +# which cannot resolve some dashboard URLs because they are not defined on El +# Proxito. +# +# See https://github.com/readthedocs/readthedocs-ops/issues/1323 +class ProjectSerializerNoLinks(NoLinksMixin, ProjectSerializer): + pass + + +class VersionSerializerNoLinks(NoLinksMixin, VersionSerializer): + pass + + +class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer): + pass + + +class ClientResponse: + def get(self, client_version, project, version, build, filename): + """ + Unique entry point to get the proper API response. + + It will evaluate the ``client_version`` passed and decide which is the + best JSON structure for that particular version. + """ + if client_version.major == 0: + return self._v0(project, version, build, filename) + + if client_version.major == 1: + return self._v1(project, version, build, filename) + + def _v0(self, project, version, build, filename): + """ + Initial JSON data structure consumed by the JavaScript client. + + This response is definitely in *alpha* state currently and shouldn't be + used for anyone to customize their documentation or the integration + with the Read the Docs JavaScript client. It's under active development + and anything can change without notice. + + It tries to follow some similarity with the APIv3 for already-known resources + (Project, Version, Build, etc). + """ + data = { "comment": ( "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY" " AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!" ), - "project": { - "slug": project.slug, - "language": project.language, - "repository_url": project.repo, - "programming_language": project.programming_language, - }, - "version": { - "slug": version.slug, - "external": version.type == EXTERNAL, - }, - "build": { - "id": build.pk, - }, + "project": ProjectSerializerNoLinks(project).data, + "version": VersionSerializerNoLinks(version).data, + "build": BuildSerializerNoLinks(build).data, + # TODO: consider creating one serializer per field here. + # The resulting JSON will be the same, but maybe it's easier/cleaner? "domains": { "dashboard": settings.PRODUCTION_DOMAIN, }, @@ -64,6 +179,7 @@ def get(self, request): }, "features": { "analytics": { + # TODO: consider adding this field into the ProjectSerializer itself. "code": project.analytics_code, }, "external_version_warning": { @@ -86,7 +202,7 @@ def get(self, request): project=project, version_slug=project.get_default_version(), language=project.language, - filename=unresolved_url.filename, + filename=filename, ), "root_selector": "[role=main]", "inject_styles": True, @@ -139,4 +255,9 @@ def get(self, request): if version.build_data: data.update(version.build_data) - return JsonResponse(data, json_dumps_params=dict(indent=4)) + return data + + def _v1(self, project, version, build, filename): + return { + "comment": "Undefined yet. Use v0 for now", + } From a9d30a96dab9b03da8a6e00d79f68d7c172852a8 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 4 Apr 2023 18:54:55 +0200 Subject: [PATCH 02/10] Test: add json files used for API responses --- readthedocs/proxito/tests/responses/v0.json | 120 ++++++++++++++++++++ readthedocs/proxito/tests/responses/v1.json | 3 + readthedocs/proxito/tests/responses/v2.json | 3 + 3 files changed, 126 insertions(+) create mode 100644 readthedocs/proxito/tests/responses/v0.json create mode 100644 readthedocs/proxito/tests/responses/v1.json create mode 100644 readthedocs/proxito/tests/responses/v2.json diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json new file mode 100644 index 00000000000..1b4d5b4bb56 --- /dev/null +++ b/readthedocs/proxito/tests/responses/v0.json @@ -0,0 +1,120 @@ +{ + "comment": "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!", + "project": { + "created": "2019-04-29T10:00:00Z", + "default_branch": "master", + "default_version": "latest", + "id": 1, + "language": { + "code": "en", + "name": "English" + }, + "modified": "2019-04-29T12:00:00Z", + "name": "project", + "programming_language": { + "code": "words", + "name": "Only Words" + }, + "homepage": "http://project.com", + "repository": { + "type": "git", + "url": "https://github.com/readthedocs/project" + }, + "slug": "project", + "subproject_of": null, + "tags": [ + "project", + "tag", + "test" + ], + "translation_of": null, + "users": [ + { + "username": "testuser" + } + ] + }, + "version": { + "active": true, + "hidden": false, + "built": true, + "downloads": {}, + "id": 1, + "identifier": "a1b2c3", + "ref": null, + "slug": "latest", + "type": "tag", + "verbose_name": "latest" + }, + "build": { + "commit": "a1b2c3", + "created": "2019-04-29T10:00:00Z", + "duration": 60, + "error": "", + "finished": "2019-04-29T10:01:00Z", + "id": 1, + "project": "project", + "state": { + "code": "finished", + "name": "Finished" + }, + "success": true, + "version": "latest" + }, + "domains": { + "dashboard": "readthedocs.org" + }, + "readthedocs": { + "analytics": { + "code": null + } + }, + "features": { + "analytics": { + "code": null + }, + "external_version_warning": { + "enabled": true, + "query_selector": "[role=main]" + }, + "non_latest_version_warning": { + "enabled": true, + "query_selector": "[role=main]", + "versions": [ + "latest" + ] + }, + "doc_diff": { + "enabled": true, + "base_url": "https://project.dev.readthedocs.io/en/latest/index.html", + "root_selector": "[role=main]", + "inject_styles": true, + "base_host": "", + "base_page": "" + }, + "flyout": { + "translations": [], + "versions": [ + {"slug": "latest", "url": "/en/latest/"} + ], + "downloads": [], + "vcs": { + "url": "https://github.com", + "username": "readthedocs", + "repository": "test-builds", + "branch": "a1b2c3", + "filepath": "/docs/index.rst" + } + }, + "search": { + "api_endpoint": "/_/api/v3/search/", + "default_filter": "subprojects:project/latest", + "filters": [ + ["Search only in this project", "project:project/latest"], + ["Search subprojects", "subprojects:project/latest"] + ], + "project": "project", + "version": "latest" + } + } + } diff --git a/readthedocs/proxito/tests/responses/v1.json b/readthedocs/proxito/tests/responses/v1.json new file mode 100644 index 00000000000..21a1c1b6dfb --- /dev/null +++ b/readthedocs/proxito/tests/responses/v1.json @@ -0,0 +1,3 @@ +{ + "comment": "Undefined yet. Use v0 for now" +} diff --git a/readthedocs/proxito/tests/responses/v2.json b/readthedocs/proxito/tests/responses/v2.json new file mode 100644 index 00000000000..7251149988f --- /dev/null +++ b/readthedocs/proxito/tests/responses/v2.json @@ -0,0 +1,3 @@ +{ + "error": "The version specified in 'X-RTD-Hosting-Integrations-Version' is currently not supported" +} From dc923d4e2fe2b3d67c5c9016698fd9c16946e0ab Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 5 Apr 2023 13:30:24 +0200 Subject: [PATCH 03/10] Refactor: rename `client` to `addons` --- readthedocs/proxito/views/hosting.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 2c9f94741e7..f8b1bb40e9f 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -18,7 +18,7 @@ log = structlog.get_logger(__name__) # noqa -CLIENT_VERSIONS_SUPPORTED = (0, 1) +ADDONS_VERSIONS_SUPPORTED = (0, 1) class ClientError(Exception): @@ -55,8 +55,8 @@ def get(self, request): status=400, ) - client_version = request.headers.get("X-RTD-Hosting-Integrations-Version") - if not client_version: + addons_version = request.headers.get("X-RTD-Hosting-Integrations-Version") + if not addons_version: return JsonResponse( { "error": ClientError.VERSION_HEADER_MISSING, @@ -64,8 +64,8 @@ def get(self, request): status=400, ) try: - client_version = packaging.version.parse(client_version) - if client_version.major not in CLIENT_VERSIONS_SUPPORTED: + addons_version = packaging.version.parse(addons_version) + if addons_version.major not in ADDONS_VERSIONS_SUPPORTED: raise ClientError except packaging.version.InvalidVersion: return JsonResponse( @@ -90,7 +90,7 @@ def get(self, request): project.get_default_version() build = version.builds.last() - data = ClientResponse().get(client_version, project, version, build, filename) + data = AddonsResponse().get(addons_version, project, version, build, filename) return JsonResponse(data, json_dumps_params=dict(indent=4)) @@ -132,18 +132,18 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer): pass -class ClientResponse: - def get(self, client_version, project, version, build, filename): +class AddonsResponse: + def get(self, addons_version, project, version, build, filename): """ Unique entry point to get the proper API response. - It will evaluate the ``client_version`` passed and decide which is the + It will evaluate the ``addons_version`` passed and decide which is the best JSON structure for that particular version. """ - if client_version.major == 0: + if addons_version.major == 0: return self._v0(project, version, build, filename) - if client_version.major == 1: + if addons_version.major == 1: return self._v1(project, version, build, filename) def _v0(self, project, version, build, filename): From b7f9e0f6a250c79f7900d89bd5775e5539860c4a Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 6 Apr 2023 12:19:41 +0200 Subject: [PATCH 04/10] Docs: todo comment about `features` key --- readthedocs/proxito/views/hosting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index f8b1bb40e9f..b11966eca2b 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -177,6 +177,9 @@ def _v0(self, project, version, build, filename): "code": settings.GLOBAL_ANALYTICS_CODE, }, }, + # TODO: the ``features`` is not polished and we expect to change drastically. + # Mainly, all the fields including a Project, Version or Build will use the exact same + # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "features": { "analytics": { # TODO: consider adding this field into the ProjectSerializer itself. From e8c7a0850ced441888a69cec74fa38631f983956 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 12 Apr 2023 10:30:33 +0200 Subject: [PATCH 05/10] API: use a `current` sub-key for known objects --- readthedocs/proxito/views/hosting.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index b11966eca2b..7c9bd58b8a3 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -164,9 +164,15 @@ def _v0(self, project, version, build, filename): "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY" " AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!" ), - "project": ProjectSerializerNoLinks(project).data, - "version": VersionSerializerNoLinks(version).data, - "build": BuildSerializerNoLinks(build).data, + "projects": { + "current": ProjectSerializerNoLinks(project).data, + }, + "versions": { + "current": VersionSerializerNoLinks(version).data, + }, + "builds": { + "current": BuildSerializerNoLinks(build).data, + }, # TODO: consider creating one serializer per field here. # The resulting JSON will be the same, but maybe it's easier/cleaner? "domains": { From 309eee2639fb7d0e4a9b71f4ac3efdd538f166fe Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 12 Apr 2023 10:41:36 +0200 Subject: [PATCH 06/10] json prettier --- readthedocs/proxito/tests/responses/v0.json | 234 ++++++++++---------- 1 file changed, 116 insertions(+), 118 deletions(-) diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json index 1b4d5b4bb56..1d42a241f64 100644 --- a/readthedocs/proxito/tests/responses/v0.json +++ b/readthedocs/proxito/tests/responses/v0.json @@ -1,120 +1,118 @@ { - "comment": "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!", - "project": { - "created": "2019-04-29T10:00:00Z", - "default_branch": "master", - "default_version": "latest", - "id": 1, - "language": { - "code": "en", - "name": "English" - }, - "modified": "2019-04-29T12:00:00Z", - "name": "project", - "programming_language": { - "code": "words", - "name": "Only Words" - }, - "homepage": "http://project.com", - "repository": { - "type": "git", - "url": "https://github.com/readthedocs/project" - }, - "slug": "project", - "subproject_of": null, - "tags": [ - "project", - "tag", - "test" - ], - "translation_of": null, - "users": [ - { - "username": "testuser" - } - ] - }, - "version": { - "active": true, - "hidden": false, - "built": true, - "downloads": {}, - "id": 1, - "identifier": "a1b2c3", - "ref": null, - "slug": "latest", - "type": "tag", - "verbose_name": "latest" - }, - "build": { - "commit": "a1b2c3", - "created": "2019-04-29T10:00:00Z", - "duration": 60, - "error": "", - "finished": "2019-04-29T10:01:00Z", - "id": 1, - "project": "project", - "state": { - "code": "finished", - "name": "Finished" - }, - "success": true, - "version": "latest" - }, - "domains": { - "dashboard": "readthedocs.org" - }, - "readthedocs": { - "analytics": { - "code": null - } - }, - "features": { - "analytics": { - "code": null - }, - "external_version_warning": { - "enabled": true, - "query_selector": "[role=main]" - }, - "non_latest_version_warning": { - "enabled": true, - "query_selector": "[role=main]", - "versions": [ - "latest" - ] - }, - "doc_diff": { - "enabled": true, - "base_url": "https://project.dev.readthedocs.io/en/latest/index.html", - "root_selector": "[role=main]", - "inject_styles": true, - "base_host": "", - "base_page": "" - }, - "flyout": { - "translations": [], - "versions": [ - {"slug": "latest", "url": "/en/latest/"} - ], - "downloads": [], - "vcs": { - "url": "https://github.com", - "username": "readthedocs", - "repository": "test-builds", - "branch": "a1b2c3", - "filepath": "/docs/index.rst" - } - }, - "search": { - "api_endpoint": "/_/api/v3/search/", - "default_filter": "subprojects:project/latest", - "filters": [ - ["Search only in this project", "project:project/latest"], - ["Search subprojects", "subprojects:project/latest"] - ], - "project": "project", - "version": "latest" - } - } + "comment": "THIS RESPONSE IS IN ALPHA FOR TEST PURPOSES ONLY AND IT'S GOING TO CHANGE COMPLETELY -- DO NOT USE IT!", + "projects": { + "current": { + "created": "2019-04-29T10:00:00Z", + "default_branch": "master", + "default_version": "latest", + "id": 1, + "language": { + "code": "en", + "name": "English" + }, + "modified": "2019-04-29T12:00:00Z", + "name": "project", + "programming_language": { + "code": "words", + "name": "Only Words" + }, + "homepage": "http://project.com", + "repository": { + "type": "git", + "url": "https://github.com/readthedocs/project" + }, + "slug": "project", + "subproject_of": null, + "tags": ["project", "tag", "test"], + "translation_of": null, + "users": [ + { + "username": "testuser" } + ] + } + }, + "versions": { + "current": { + "active": true, + "hidden": false, + "built": true, + "downloads": {}, + "id": 1, + "identifier": "a1b2c3", + "ref": null, + "slug": "latest", + "type": "tag", + "verbose_name": "latest" + } + }, + "builds": { + "current": { + "commit": "a1b2c3", + "created": "2019-04-29T10:00:00Z", + "duration": 60, + "error": "", + "finished": "2019-04-29T10:01:00Z", + "id": 1, + "project": "project", + "state": { + "code": "finished", + "name": "Finished" + }, + "success": true, + "version": "latest" + } + }, + "domains": { + "dashboard": "readthedocs.org" + }, + "readthedocs": { + "analytics": { + "code": null + } + }, + "features": { + "analytics": { + "code": null + }, + "external_version_warning": { + "enabled": true, + "query_selector": "[role=main]" + }, + "non_latest_version_warning": { + "enabled": true, + "query_selector": "[role=main]", + "versions": ["latest"] + }, + "doc_diff": { + "enabled": true, + "base_url": "https://project.dev.readthedocs.io/en/latest/index.html", + "root_selector": "[role=main]", + "inject_styles": true, + "base_host": "", + "base_page": "" + }, + "flyout": { + "translations": [], + "versions": [{ "slug": "latest", "url": "/en/latest/" }], + "downloads": [], + "vcs": { + "url": "https://github.com", + "username": "readthedocs", + "repository": "test-builds", + "branch": "a1b2c3", + "filepath": "/docs/index.rst" + } + }, + "search": { + "api_endpoint": "/_/api/v3/search/", + "default_filter": "subprojects:project/latest", + "filters": [ + ["Search only in this project", "project:project/latest"], + ["Search subprojects", "subprojects:project/latest"] + ], + "project": "project", + "version": "latest" + } + } +} From d0fb118e34e2610174fee7126cd25eb491801533 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 12 Apr 2023 11:03:23 +0200 Subject: [PATCH 07/10] Tests: update structure --- readthedocs/proxito/tests/test_hosting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readthedocs/proxito/tests/test_hosting.py b/readthedocs/proxito/tests/test_hosting.py index 8b14bfc226d..f315898e1bc 100644 --- a/readthedocs/proxito/tests/test_hosting.py +++ b/readthedocs/proxito/tests/test_hosting.py @@ -69,10 +69,10 @@ def _get_response_dict(self, view_name, filepath=None): return json.load(open(filename)) def _normalize_datetime_fields(self, obj): - obj["project"]["created"] = "2019-04-29T10:00:00Z" - obj["project"]["modified"] = "2019-04-29T12:00:00Z" - obj["build"]["created"] = "2019-04-29T10:00:00Z" - obj["build"]["finished"] = "2019-04-29T10:01:00Z" + obj["projects"]["current"]["created"] = "2019-04-29T10:00:00Z" + obj["projects"]["current"]["modified"] = "2019-04-29T12:00:00Z" + obj["builds"]["current"]["created"] = "2019-04-29T10:00:00Z" + obj["builds"]["current"]["finished"] = "2019-04-29T10:01:00Z" return obj def test_get_config_v0(self): From c37d555210f39bf5615e007ed86684b0853a1ecc Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 12 Apr 2023 11:10:49 +0200 Subject: [PATCH 08/10] API: rename `features` field into `addons` --- readthedocs/proxito/tests/responses/v0.json | 2 +- readthedocs/proxito/views/hosting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json index 1d42a241f64..410f66520f5 100644 --- a/readthedocs/proxito/tests/responses/v0.json +++ b/readthedocs/proxito/tests/responses/v0.json @@ -71,7 +71,7 @@ "code": null } }, - "features": { + "addons": { "analytics": { "code": null }, diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 7c9bd58b8a3..d1cd5dff5c1 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -186,7 +186,7 @@ def _v0(self, project, version, build, filename): # TODO: the ``features`` is not polished and we expect to change drastically. # Mainly, all the fields including a Project, Version or Build will use the exact same # serializer than the keys ``project``, ``version`` and ``build`` from the top level. - "features": { + "addons": { "analytics": { # TODO: consider adding this field into the ProjectSerializer itself. "code": project.analytics_code, From 43b160e83eeafbb164e3670e9b9585459d06394e Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 17 Apr 2023 12:26:34 +0200 Subject: [PATCH 09/10] `features.search.enabled` was missing --- readthedocs/proxito/tests/responses/v0.json | 1 + readthedocs/proxito/views/hosting.py | 1 + 2 files changed, 2 insertions(+) diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json index 410f66520f5..a4b78a8d224 100644 --- a/readthedocs/proxito/tests/responses/v0.json +++ b/readthedocs/proxito/tests/responses/v0.json @@ -105,6 +105,7 @@ } }, "search": { + "enabled": true, "api_endpoint": "/_/api/v3/search/", "default_filter": "subprojects:project/latest", "filters": [ diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index d1cd5dff5c1..953a332d2a0 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -241,6 +241,7 @@ def _v0(self, project, version, build, filename): }, }, "search": { + "enabled": True, "project": project.slug, "version": version.slug, "api_endpoint": "/_/api/v3/search/", From 7d98b8c513487216e4d44ba4ac72ebf61c923d5f Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Mon, 17 Apr 2023 12:38:19 +0200 Subject: [PATCH 10/10] Add `addons.analytics.enabled` --- readthedocs/proxito/tests/responses/v0.json | 1 + readthedocs/proxito/views/hosting.py | 1 + 2 files changed, 2 insertions(+) diff --git a/readthedocs/proxito/tests/responses/v0.json b/readthedocs/proxito/tests/responses/v0.json index a4b78a8d224..b890bd80e4f 100644 --- a/readthedocs/proxito/tests/responses/v0.json +++ b/readthedocs/proxito/tests/responses/v0.json @@ -73,6 +73,7 @@ }, "addons": { "analytics": { + "enabled": true, "code": null }, "external_version_warning": { diff --git a/readthedocs/proxito/views/hosting.py b/readthedocs/proxito/views/hosting.py index 953a332d2a0..9cb39f85c8c 100644 --- a/readthedocs/proxito/views/hosting.py +++ b/readthedocs/proxito/views/hosting.py @@ -188,6 +188,7 @@ def _v0(self, project, version, build, filename): # serializer than the keys ``project``, ``version`` and ``build`` from the top level. "addons": { "analytics": { + "enabled": True, # TODO: consider adding this field into the ProjectSerializer itself. "code": project.analytics_code, },