Skip to content

Commit 125d81f

Browse files
authored
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 0d6daf8 commit 125d81f

File tree

1 file changed

+80
-38
lines changed

1 file changed

+80
-38
lines changed

readthedocs/proxito/views/hosting.py

+80-38
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
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

1013
from readthedocs.api.mixins import CDNCacheTagsMixin
14+
from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion
1115
from readthedocs.api.v3.serializers import (
1216
BuildSerializer,
1317
ProjectSerializer,
@@ -16,6 +20,7 @@
1620
from readthedocs.builds.models import Version
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(CDNCacheTagsMixin, View):
43+
class BaseReadTheDocsConfigJson(CDNCacheTagsMixin, APIView):
3944

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

57+
http_method_names = ["get"]
58+
permission_classes = [IsAuthorizedToViewVersion]
59+
renderer_classes = [JSONRenderer]
5260
project_cache_tag = "rtd-addons"
5361

54-
def get(self, request):
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
93+
94+
def _get_project(self):
95+
project, version, build, filename = self._resolve_resources()
96+
return project
5597

98+
def _get_version(self):
99+
project, version, build, filename = self._resolve_resources()
100+
return version
101+
102+
def get(self, request, format=None):
56103
url = request.GET.get("url")
57104
if not url:
58105
return JsonResponse(
@@ -85,36 +132,16 @@ def get(self, request):
85132
status=400,
86133
)
87134

88-
unresolved_domain = request.unresolved_domain
89-
project = unresolved_domain.project
90-
91-
try:
92-
unresolved_url = unresolver.unresolve_url(url)
93-
version = unresolved_url.version
94-
filename = unresolved_url.filename
95-
build = version.builds.last()
96-
97-
except UnresolverError as exc:
98-
# If an exception is raised and there is a ``project`` in the
99-
# exception, it's a partial match. This could be because of an
100-
# invalid URL path, but on a valid project domain. In this case, we
101-
# continue with the ``project``, but without a ``version``.
102-
# Otherwise, we return 404 NOT FOUND.
103-
project = getattr(exc, "project", None)
104-
if not project:
105-
raise Http404() from exc
106-
107-
version = None
108-
filename = None
109-
build = None
110-
111-
# We need to defined these methods because of ``CDNCacheTagsMixin``,
112-
# but we don't have a simple/easy way to split these methods, so we use lambda here
113-
# after calculating them via the unresolver.
114-
self._get_project = lambda: project
115-
self._get_version = lambda: version
135+
project, version, build, filename = self._resolve_resources()
116136

117-
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+
)
118145
return JsonResponse(data, json_dumps_params={"indent": 4, "sort_keys": True})
119146

120147

@@ -157,20 +184,28 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer):
157184

158185

159186
class AddonsResponse:
160-
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+
):
161196
"""
162197
Unique entry point to get the proper API response.
163198
164199
It will evaluate the ``addons_version`` passed and decide which is the
165200
best JSON structure for that particular version.
166201
"""
167202
if addons_version.major == 0:
168-
return self._v0(project, version, build, filename)
203+
return self._v0(project, version, build, filename, user)
169204

170205
if addons_version.major == 1:
171-
return self._v1(project, version, build, filename)
206+
return self._v1(project, version, build, filename, user)
172207

173-
def _v0(self, project, version, build, filename):
208+
def _v0(self, project, version, build, filename, user):
174209
"""
175210
Initial JSON data structure consumed by the JavaScript client.
176211
@@ -188,7 +223,10 @@ def _v0(self, project, version, build, filename):
188223
if not project.single_version:
189224
versions_active_built_not_hidden = (
190225
Version.internal.public(
191-
project=project, only_active=True, only_built=True
226+
project=project,
227+
only_active=True,
228+
only_built=True,
229+
user=user,
192230
)
193231
.exclude(hidden=True)
194232
.only("slug")
@@ -377,7 +415,11 @@ def _v0(self, project, version, build, filename):
377415

378416
return data
379417

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

0 commit comments

Comments
 (0)