1
1
"""Views for hosting features."""
2
2
3
+ from functools import lru_cache
4
+
3
5
import packaging
4
6
import structlog
5
7
from django .conf import settings
6
8
from django .contrib .auth .models import AnonymousUser
7
9
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
9
12
13
+ from readthedocs .api .mixins import CDNCacheTagsMixin
14
+ from readthedocs .api .v2 .permissions import IsAuthorizedToViewVersion
10
15
from readthedocs .api .v3 .serializers import (
11
16
BuildSerializer ,
12
17
ProjectSerializer ,
13
18
VersionSerializer ,
14
19
)
15
20
from readthedocs .builds .models import Version
16
- from readthedocs .core .mixins import CDNCacheControlMixin
17
21
from readthedocs .core .resolver import resolver
18
22
from readthedocs .core .unresolver import UnresolverError , unresolver
23
+ from readthedocs .core .utils .extend import SettingsOverrideObject
19
24
from readthedocs .projects .models import Feature
20
25
21
26
log = structlog .get_logger (__name__ ) # noqa
@@ -35,7 +40,7 @@ class ClientError(Exception):
35
40
)
36
41
37
42
38
- class ReadTheDocsConfigJson ( CDNCacheControlMixin , View ):
43
+ class BaseReadTheDocsConfigJson ( CDNCacheTagsMixin , APIView ):
39
44
40
45
"""
41
46
API response consumed by our JavaScript client.
@@ -49,8 +54,52 @@ class ReadTheDocsConfigJson(CDNCacheControlMixin, View):
49
54
(e.g. ``window.location.href``)
50
55
"""
51
56
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
53
93
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 ):
54
103
url = request .GET .get ("url" )
55
104
if not url :
56
105
return JsonResponse (
@@ -83,30 +132,16 @@ def get(self, request):
83
132
status = 400 ,
84
133
)
85
134
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 ()
108
136
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
+ )
110
145
return JsonResponse (data , json_dumps_params = {"indent" : 4 , "sort_keys" : True })
111
146
112
147
@@ -149,20 +184,28 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer):
149
184
150
185
151
186
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
+ ):
153
196
"""
154
197
Unique entry point to get the proper API response.
155
198
156
199
It will evaluate the ``addons_version`` passed and decide which is the
157
200
best JSON structure for that particular version.
158
201
"""
159
202
if addons_version .major == 0 :
160
- return self ._v0 (project , version , build , filename )
203
+ return self ._v0 (project , version , build , filename , user )
161
204
162
205
if addons_version .major == 1 :
163
- return self ._v1 (project , version , build , filename )
206
+ return self ._v1 (project , version , build , filename , user )
164
207
165
- def _v0 (self , project , version , build , filename ):
208
+ def _v0 (self , project , version , build , filename , user ):
166
209
"""
167
210
Initial JSON data structure consumed by the JavaScript client.
168
211
@@ -180,7 +223,10 @@ def _v0(self, project, version, build, filename):
180
223
if not project .single_version :
181
224
versions_active_built_not_hidden = (
182
225
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 ,
184
230
)
185
231
.exclude (hidden = True )
186
232
.only ("slug" )
@@ -369,7 +415,11 @@ def _v0(self, project, version, build, filename):
369
415
370
416
return data
371
417
372
- def _v1 (self , project , version , build , filename ):
418
+ def _v1 (self , project , version , build , filename , user ):
373
419
return {
374
420
"comment" : "Undefined yet. Use v0 for now" ,
375
421
}
422
+
423
+
424
+ class ReadTheDocsConfigJson (SettingsOverrideObject ):
425
+ _default_class = BaseReadTheDocsConfigJson
0 commit comments