Skip to content

Commit d7f3b08

Browse files
authored
APIv3: add more generic fields (#11205)
* APIv3: add more generic fields Add `aliases` field to the `VersionSerializer` that's used to serialize `Version` objects. This field is a list of versions the current version "points to". Example `latest` version "points to" `origin/main`, and `stable` version "points to" `v3.12`. This is known in the code as "original stable/latest version". Besides, this PR adds `versions.stable` and `versions.latest` fields to the addons API which will be useful to solve front-end issues in a more generic way. * Update test cases to match changes * Update documentation to mention the `aliases` * Expose generic fields (`projects.translations`, `versions.active`) This is the pattern I want to have here, serializing the full object instead of a few small set of fields. This is a lot more generic, simplifies the interaction between the API and JS Addons. Besides, it exposes the same objects as the regular APIv3 which won't change over time. * Optimizations * Cache more method on `Resolver` * Simplification of API response * Use a shared `Resolver` instance to resolve version URLs * ToDo comment for future optimization * Adapt num queries from tests * Update tests to match changes * Update addons tests to match changes * Update tests to not share the resolver instance * Typo * Add `urls.donwloads` to Project serializer * Share a `Resolver()` between different serializers A better way of resolving this issue will be done in #10456 * Pass `version_slug` to be able to build URLs that point to a version * Test updates * Update test JSON responses for `urls.downloads` field * Update JSON responses * Increase `api_version` on JSON responses * Remove old comment We are sorting the translations in the front end * Fix proxito dummy URLs * Updates from review
1 parent d486011 commit d7f3b08

22 files changed

+436
-366
lines changed

docs/user/api/v3.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,10 @@ Project details
258258
"translation_of": null,
259259
"urls": {
260260
"documentation": "http://pip.pypa.io/en/stable/",
261-
"home": "https://pip.pypa.io/"
261+
"home": "https://readthedocs.org/projects/pip/",
262+
"downloads": "https://readthedocs.org/projects/pip/downloads/",
263+
"builds": "https://readthedocs.org/projects/pip/builds/",
264+
"versions": "https://readthedocs.org/projects/pip/versions/",
262265
},
263266
"tags": [
264267
"distutils",
@@ -556,6 +559,7 @@ Version detail
556559
"ref": "19.0.2",
557560
"built": true,
558561
"active": true,
562+
"aliases": ["VERSION"],
559563
"hidden": false,
560564
"type": "tag",
561565
"last_build": "{BUILD}",

readthedocs/api/v3/serializers.py

+45-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework import serializers
1212
from taggit.serializers import TaggitSerializer, TagListSerializerField
1313

14+
from readthedocs.builds.constants import LATEST, STABLE
1415
from readthedocs.builds.models import Build, Version
1516
from readthedocs.core.resolver import Resolver
1617
from readthedocs.core.utils import slugify
@@ -322,13 +323,15 @@ class VersionURLsSerializer(BaseLinksSerializer, serializers.Serializer):
322323
dashboard = VersionDashboardURLsSerializer(source="*")
323324

324325
def get_documentation(self, obj):
325-
return Resolver().resolve_version(
326+
resolver = getattr(self.parent, "resolver", Resolver())
327+
return resolver.resolve_version(
326328
project=obj.project,
327329
version=obj,
328330
)
329331

330332

331333
class VersionSerializer(FlexFieldsModelSerializer):
334+
aliases = serializers.SerializerMethodField()
332335
ref = serializers.CharField()
333336
downloads = serializers.SerializerMethodField()
334337
urls = VersionURLsSerializer(source="*")
@@ -337,6 +340,7 @@ class VersionSerializer(FlexFieldsModelSerializer):
337340
class Meta:
338341
model = Version
339342
fields = [
343+
"aliases",
340344
"id",
341345
"slug",
342346
"verbose_name",
@@ -354,6 +358,17 @@ class Meta:
354358

355359
expandable_fields = {"last_build": (BuildSerializer,)}
356360

361+
def __init__(self, *args, resolver=None, version_serializer=None, **kwargs):
362+
super().__init__(*args, **kwargs)
363+
364+
# Use a shared resolver to reduce the amount of DB queries while
365+
# resolving version URLs.
366+
self.resolver = kwargs.pop("resolver", Resolver())
367+
368+
# Allow passing a specific serializer when initializing it.
369+
# This is required to pass ``VersionSerializerNoLinks`` from the addons API.
370+
self.version_serializer = version_serializer or VersionSerializer
371+
357372
def get_downloads(self, obj):
358373
downloads = obj.get_downloads()
359374
data = {}
@@ -368,6 +383,16 @@ def get_downloads(self, obj):
368383

369384
return data
370385

386+
def get_aliases(self, obj):
387+
if obj.machine and obj.slug in (STABLE, LATEST):
388+
if obj.slug == STABLE:
389+
alias_version = obj.project.get_original_stable_version()
390+
if obj.slug == LATEST:
391+
alias_version = obj.project.get_original_latest_version()
392+
if alias_version and alias_version.active:
393+
return [self.version_serializer(alias_version).data]
394+
return []
395+
371396

372397
class VersionUpdateSerializer(serializers.ModelSerializer):
373398

@@ -426,10 +451,12 @@ class ProjectURLsSerializer(BaseLinksSerializer, serializers.Serializer):
426451

427452
"""Serializer with all the user-facing URLs under Read the Docs."""
428453

429-
documentation = serializers.CharField(source="get_docs_url")
454+
documentation = serializers.SerializerMethodField()
455+
430456
home = serializers.SerializerMethodField()
431457
builds = serializers.SerializerMethodField()
432458
versions = serializers.SerializerMethodField()
459+
downloads = serializers.SerializerMethodField()
433460

434461
def get_home(self, obj):
435462
path = reverse("projects_detail", kwargs={"project_slug": obj.slug})
@@ -443,6 +470,15 @@ def get_versions(self, obj):
443470
path = reverse("project_version_list", kwargs={"project_slug": obj.slug})
444471
return self._absolute_url(path)
445472

473+
def get_downloads(self, obj):
474+
path = reverse("project_downloads", kwargs={"project_slug": obj.slug})
475+
return self._absolute_url(path)
476+
477+
def get_documentation(self, obj):
478+
version_slug = getattr(self.parent, "version_slug", None)
479+
resolver = getattr(self.parent, "resolver", Resolver())
480+
return obj.get_docs_url(version_slug=version_slug, resolver=resolver)
481+
446482

447483
class RepositorySerializer(serializers.Serializer):
448484
url = serializers.CharField(source="repo")
@@ -765,6 +801,13 @@ class Meta:
765801
}
766802

767803
def __init__(self, *args, **kwargs):
804+
# Receive a `Version.slug` here to build URLs properly
805+
self.version_slug = kwargs.pop("version_slug", None)
806+
807+
# Use a shared resolver to reduce the amount of DB queries while
808+
# resolving version URLs.
809+
self.resolver = kwargs.pop("resolver", Resolver())
810+
768811
super().__init__(*args, **kwargs)
769812
# When using organizations, projects don't have the concept of users.
770813
# But we have organization.owners.

readthedocs/api/v3/tests/responses/projects-detail.json

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"active_versions": [
33
{
44
"active": true,
5+
"aliases": [],
56
"hidden": false,
67
"built": true,
78
"downloads": {
@@ -100,6 +101,7 @@
100101
"urls": {
101102
"builds": "https://readthedocs.org/projects/project/builds/",
102103
"documentation": "http://project.readthedocs.io/en/latest/",
104+
"downloads": "https://readthedocs.org/projects/project/downloads/",
103105
"home": "https://readthedocs.org/projects/project/",
104106
"versions": "https://readthedocs.org/projects/project/versions/"
105107
},

readthedocs/api/v3/tests/responses/projects-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"urls": {
3030
"builds": "https://readthedocs.org/projects/project/builds/",
3131
"documentation": "http://project.readthedocs.io/en/latest/",
32+
"downloads": "https://readthedocs.org/projects/project/downloads/",
3233
"home": "https://readthedocs.org/projects/project/",
3334
"versions": "https://readthedocs.org/projects/project/versions/"
3435
},

readthedocs/api/v3/tests/responses/projects-list_POST.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"urls": {
3737
"builds": "https://readthedocs.org/projects/test-project/builds/",
3838
"documentation": "http://test-project.readthedocs.io/en/latest/",
39+
"downloads": "https://readthedocs.org/projects/test-project/downloads/",
3940
"home": "https://readthedocs.org/projects/test-project/",
4041
"versions": "https://readthedocs.org/projects/test-project/versions/"
4142
},

readthedocs/api/v3/tests/responses/projects-subprojects-detail.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"urls": {
4242
"builds": "https://readthedocs.org/projects/subproject/builds/",
4343
"documentation": "http://project.readthedocs.io/projects/subproject/en/latest/",
44+
"downloads": "https://readthedocs.org/projects/subproject/downloads/",
4445
"home": "https://readthedocs.org/projects/subproject/",
4546
"versions": "https://readthedocs.org/projects/subproject/versions/"
4647
},

readthedocs/api/v3/tests/responses/projects-subprojects-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"urls": {
4747
"builds": "https://readthedocs.org/projects/subproject/builds/",
4848
"documentation": "http://project.readthedocs.io/projects/subproject/en/latest/",
49+
"downloads": "https://readthedocs.org/projects/subproject/downloads/",
4950
"home": "https://readthedocs.org/projects/subproject/",
5051
"versions": "https://readthedocs.org/projects/subproject/versions/"
5152
},

readthedocs/api/v3/tests/responses/projects-subprojects-list_POST.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"urls": {
4242
"builds": "https://readthedocs.org/projects/new-project/builds/",
4343
"documentation": "http://project.readthedocs.io/projects/subproject-alias/en/latest/",
44+
"downloads": "https://readthedocs.org/projects/new-project/downloads/",
4445
"home": "https://readthedocs.org/projects/new-project/",
4546
"versions": "https://readthedocs.org/projects/new-project/versions/"
4647
},

readthedocs/api/v3/tests/responses/projects-superproject.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"urls": {
4141
"builds": "https://readthedocs.org/projects/project/builds/",
4242
"documentation": "http://project.readthedocs.io/en/latest/",
43+
"downloads": "https://readthedocs.org/projects/project/downloads/",
4344
"home": "https://readthedocs.org/projects/project/",
4445
"versions": "https://readthedocs.org/projects/project/versions/"
4546
},

readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"urls": {
6868
"builds": "https://readthedocs.org/projects/project/builds/",
6969
"documentation": "http://project.readthedocs.io/en/latest/",
70+
"downloads": "https://readthedocs.org/projects/project/downloads/",
7071
"home": "https://readthedocs.org/projects/project/",
7172
"versions": "https://readthedocs.org/projects/project/versions/"
7273
},
@@ -83,6 +84,7 @@
8384
"triggered": true,
8485
"version": {
8586
"active": true,
87+
"aliases": [],
8688
"hidden": false,
8789
"built": true,
8890
"downloads": {

readthedocs/api/v3/tests/responses/projects-versions-detail.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"active": true,
3+
"aliases": [],
34
"hidden": false,
45
"built": true,
56
"downloads": {

readthedocs/api/v3/tests/responses/remoterepositories-list.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"urls": {
4646
"builds": "https://readthedocs.org/projects/project/builds/",
4747
"documentation": "http://project.readthedocs.io/en/latest/",
48+
"downloads": "https://readthedocs.org/projects/project/downloads/",
4849
"home": "https://readthedocs.org/projects/project/",
4950
"versions": "https://readthedocs.org/projects/project/versions/"
5051
},

readthedocs/core/resolver.py

+2
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def get_subproject_url_prefix(self, project, external_version_slug=None):
273273
path = project.subproject_prefix
274274
return urlunparse((protocol, domain, path, "", "", ""))
275275

276+
@lru_cache(maxsize=1)
276277
def _get_canonical_project(self, project):
277278
"""
278279
Get the parent project and subproject relationship from the canonical project of `project`.
@@ -346,6 +347,7 @@ def _get_project_subdomain(self, project):
346347
subdomain_slug = project.slug.replace("_", "-")
347348
return "{}.{}".format(subdomain_slug, settings.PUBLIC_DOMAIN)
348349

350+
@lru_cache(maxsize=1)
349351
def _is_external(self, project, version_slug):
350352
type_ = (
351353
project.versions.values_list("type", flat=True)

readthedocs/projects/models.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -635,13 +635,21 @@ def clean(self):
635635
def get_absolute_url(self):
636636
return reverse("projects_detail", args=[self.slug])
637637

638-
def get_docs_url(self, version_slug=None, lang_slug=None, external=False):
638+
def get_docs_url(
639+
self,
640+
version_slug=None,
641+
lang_slug=None,
642+
external=False,
643+
resolver=None,
644+
):
639645
"""
640646
Return a URL for the docs.
641647
642-
``external`` defaults False because we only link external versions in very specific places
648+
``external`` defaults False because we only link external versions in very specific places.
649+
``resolver`` is used to "share a resolver" between the same request.
643650
"""
644-
return Resolver().resolve(
651+
resolver = resolver or Resolver()
652+
return resolver.resolve(
645653
project=self,
646654
version_slug=version_slug,
647655
language=lang_slug,

0 commit comments

Comments
 (0)