Skip to content

API: hosting integrations endpoint versioning/structure #10216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 17, 2023
120 changes: 120 additions & 0 deletions readthedocs/proxito/tests/responses/v0.json
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

project.language.name here is an example of where we could support a translated API response, probably based on accept headers. If the addition of a language aware API endpoint changes the implementation here drastically, maybe this is a now conversation. But I'm assuming it's not and we can come back to this as we give users more access into this response structure.

},
"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"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably commentary on the build serializer and not this PR, but I found myself wanting some helpers here around the build state data. The data is all here, but I had to replicate a good deal of backend logic to determine things like is_finished, is_cancelled, is_active, etc.

Consider this a side request I supposed 🙂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, I had to sort through a lot of build logic with the API response this the build detail view, for example: https://github.com/readthedocs/ext-theme/blob/main/src/js/build/detail.js#L236-L283

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data is all here, but I had to replicate a good deal of backend logic to determine things like is_finished, is_cancelled, is_active, etc.

This is a pretty common discussion between backend and frontend teams 😄 . IMHO, the data is there and it's the FE who should create "the helpers" required to present the data as they need --but I guess this is kind of a philosophical discussion. We can continue talking about this outside this PR.

In any case, if we decide changing anything here, it should be done in the BuildSerializer.

"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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this pattern. It also helps push the translations into the application which I like. Translations will be A Thing though, so we can punt on that conversation either way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is just a hard coded/default example.

In theory, what I understood from the work that @stsewd did, is that the author of the docs is whom will decide the names and the values of the filters. So, we need to keep talking how that data will be passed (see readthedocs/addons#13)

],
"project": "project",
"version": "latest"
}
}
}
3 changes: 3 additions & 0 deletions readthedocs/proxito/tests/responses/v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"comment": "Undefined yet. Use v0 for now"
}
3 changes: 3 additions & 0 deletions readthedocs/proxito/tests/responses/v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"error": "The version specified in 'X-RTD-Hosting-Integrations-Version' is currently not supported"
}
139 changes: 58 additions & 81 deletions readthedocs/proxito/tests/test_hosting.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Loading