Skip to content

Commit e2898a3

Browse files
authored
Addons: add CDN-Tags to endpoint and auto-purge cache (#10704)
* Addons: add `CDN-Tags` to endpoint and auto-purge cache It follows a similar approach than `FooterHTML` API by adding `rtd-addons` CDN tag and uses the same signals logic from `readthedocs-ext` to purge the cache on different events. Closes #10694 * Addons: allow to be extended by corporate (#10705) * Define `_get_project` and `_get_version` as individual methods because these are required by the API permissions. * Use `user=` attribute when performing the query for the versions to be displayed in the flyout. Requires #10704
1 parent 25dc389 commit e2898a3

File tree

1 file changed

+83
-33
lines changed

1 file changed

+83
-33
lines changed

readthedocs/proxito/views/hosting.py

+83-33
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
"""Views for hosting features."""
22

3+
from functools import lru_cache
4+
35
import packaging
46
import structlog
57
from django.conf import settings
68
from django.contrib.auth.models import AnonymousUser
79
from django.http import Http404, JsonResponse
8-
from django.views import View
10+
from rest_framework.renderers import JSONRenderer
11+
from rest_framework.views import APIView
912

13+
from readthedocs.api.mixins import CDNCacheTagsMixin
14+
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
1015
from readthedocs.api.v3.serializers import (
1116
BuildSerializer,
1217
ProjectSerializer,
1318
VersionSerializer,
1419
)
1520
from readthedocs.builds.models import Version
16-
from readthedocs.core.mixins import CDNCacheControlMixin
1721
from readthedocs.core.resolver import resolver
1822
from readthedocs.core.unresolver import UnresolverError, unresolver
23+
from readthedocs.core.utils.extend import SettingsOverrideObject
1924
from readthedocs.projects.models import Feature
2025

2126
log = structlog.get_logger(__name__) # noqa
@@ -35,7 +40,7 @@ class ClientError(Exception):
3540
)
3641

3742

38-
class ReadTheDocsConfigJson(CDNCacheControlMixin, View):
43+
class BaseReadTheDocsConfigJson(CDNCacheTagsMixin, APIView):
3944

4045
"""
4146
API response consumed by our JavaScript client.
@@ -49,8 +54,52 @@ class ReadTheDocsConfigJson(CDNCacheControlMixin, View):
4954
(e.g. ``window.location.href``)
5055
"""
5156

52-
def get(self, request):
57+
http_method_names = ["get"]
58+
permission_classes = [IsAuthorizedToViewVersion]
59+
renderer_classes = [JSONRenderer]
60+
project_cache_tag = "rtd-addons"
61+
62+
@lru_cache(maxsize=1)
63+
def _resolve_resources(self):
64+
url = self.request.GET.get("url")
65+
if not url:
66+
# TODO: not sure what to return here when it fails on the `has_permission`
67+
return None, None, None, None
68+
69+
unresolved_domain = self.request.unresolved_domain
70+
project = unresolved_domain.project
71+
72+
try:
73+
unresolved_url = unresolver.unresolve_url(url)
74+
version = unresolved_url.version
75+
filename = unresolved_url.filename
76+
build = version.builds.last()
77+
78+
except UnresolverError as exc:
79+
# If an exception is raised and there is a ``project`` in the
80+
# exception, it's a partial match. This could be because of an
81+
# invalid URL path, but on a valid project domain. In this case, we
82+
# continue with the ``project``, but without a ``version``.
83+
# Otherwise, we return 404 NOT FOUND.
84+
project = getattr(exc, "project", None)
85+
if not project:
86+
raise Http404() from exc
87+
88+
version = None
89+
filename = None
90+
build = None
91+
92+
return project, version, build, filename
5393

94+
def _get_project(self):
95+
project, version, build, filename = self._resolve_resources()
96+
return project
97+
98+
def _get_version(self):
99+
project, version, build, filename = self._resolve_resources()
100+
return version
101+
102+
def get(self, request, format=None):
54103
url = request.GET.get("url")
55104
if not url:
56105
return JsonResponse(
@@ -83,30 +132,16 @@ def get(self, request):
83132
status=400,
84133
)
85134

86-
unresolved_domain = request.unresolved_domain
87-
project = unresolved_domain.project
88-
89-
try:
90-
unresolved_url = unresolver.unresolve_url(url)
91-
version = unresolved_url.version
92-
filename = unresolved_url.filename
93-
build = version.builds.last()
94-
95-
except UnresolverError as exc:
96-
# If an exception is raised and there is a ``project`` in the
97-
# exception, it's a partial match. This could be because of an
98-
# invalid URL path, but on a valid project domain. In this case, we
99-
# continue with the ``project``, but without a ``version``.
100-
# Otherwise, we return 404 NOT FOUND.
101-
project = getattr(exc, "project", None)
102-
if not project:
103-
raise Http404() from exc
104-
105-
version = None
106-
filename = None
107-
build = None
135+
project, version, build, filename = self._resolve_resources()
108136

109-
data = AddonsResponse().get(addons_version, project, version, build, filename)
137+
data = AddonsResponse().get(
138+
addons_version,
139+
project,
140+
version,
141+
build,
142+
filename,
143+
user=request.user,
144+
)
110145
return JsonResponse(data, json_dumps_params={"indent": 4, "sort_keys": True})
111146

112147

@@ -149,20 +184,28 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer):
149184

150185

151186
class AddonsResponse:
152-
def get(self, addons_version, project, version=None, build=None, filename=None):
187+
def get(
188+
self,
189+
addons_version,
190+
project,
191+
version=None,
192+
build=None,
193+
filename=None,
194+
user=None,
195+
):
153196
"""
154197
Unique entry point to get the proper API response.
155198
156199
It will evaluate the ``addons_version`` passed and decide which is the
157200
best JSON structure for that particular version.
158201
"""
159202
if addons_version.major == 0:
160-
return self._v0(project, version, build, filename)
203+
return self._v0(project, version, build, filename, user)
161204

162205
if addons_version.major == 1:
163-
return self._v1(project, version, build, filename)
206+
return self._v1(project, version, build, filename, user)
164207

165-
def _v0(self, project, version, build, filename):
208+
def _v0(self, project, version, build, filename, user):
166209
"""
167210
Initial JSON data structure consumed by the JavaScript client.
168211
@@ -180,7 +223,10 @@ def _v0(self, project, version, build, filename):
180223
if not project.single_version:
181224
versions_active_built_not_hidden = (
182225
Version.internal.public(
183-
project=project, only_active=True, only_built=True
226+
project=project,
227+
only_active=True,
228+
only_built=True,
229+
user=user,
184230
)
185231
.exclude(hidden=True)
186232
.only("slug")
@@ -369,7 +415,11 @@ def _v0(self, project, version, build, filename):
369415

370416
return data
371417

372-
def _v1(self, project, version, build, filename):
418+
def _v1(self, project, version, build, filename, user):
373419
return {
374420
"comment": "Undefined yet. Use v0 for now",
375421
}
422+
423+
424+
class ReadTheDocsConfigJson(SettingsOverrideObject):
425+
_default_class = BaseReadTheDocsConfigJson

0 commit comments

Comments
 (0)