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
10
13
from readthedocs .api .mixins import CDNCacheTagsMixin
14
+ from readthedocs .api .v2 .permissions import IsAuthorizedToViewVersion
11
15
from readthedocs .api .v3 .serializers import (
12
16
BuildSerializer ,
13
17
ProjectSerializer ,
16
20
from readthedocs .builds .models import Version
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 (CDNCacheTagsMixin , View ):
43
+ class BaseReadTheDocsConfigJson (CDNCacheTagsMixin , APIView ):
39
44
40
45
"""
41
46
API response consumed by our JavaScript client.
@@ -49,10 +54,52 @@ class ReadTheDocsConfigJson(CDNCacheTagsMixin, View):
49
54
(e.g. ``window.location.href``)
50
55
"""
51
56
57
+ http_method_names = ["get" ]
58
+ permission_classes = [IsAuthorizedToViewVersion ]
59
+ renderer_classes = [JSONRenderer ]
52
60
project_cache_tag = "rtd-addons"
53
61
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
55
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 ):
56
103
url = request .GET .get ("url" )
57
104
if not url :
58
105
return JsonResponse (
@@ -85,36 +132,16 @@ def get(self, request):
85
132
status = 400 ,
86
133
)
87
134
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 ()
116
136
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
+ )
118
145
return JsonResponse (data , json_dumps_params = {"indent" : 4 , "sort_keys" : True })
119
146
120
147
@@ -157,20 +184,28 @@ class BuildSerializerNoLinks(NoLinksMixin, BuildSerializer):
157
184
158
185
159
186
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
+ ):
161
196
"""
162
197
Unique entry point to get the proper API response.
163
198
164
199
It will evaluate the ``addons_version`` passed and decide which is the
165
200
best JSON structure for that particular version.
166
201
"""
167
202
if addons_version .major == 0 :
168
- return self ._v0 (project , version , build , filename )
203
+ return self ._v0 (project , version , build , filename , user )
169
204
170
205
if addons_version .major == 1 :
171
- return self ._v1 (project , version , build , filename )
206
+ return self ._v1 (project , version , build , filename , user )
172
207
173
- def _v0 (self , project , version , build , filename ):
208
+ def _v0 (self , project , version , build , filename , user ):
174
209
"""
175
210
Initial JSON data structure consumed by the JavaScript client.
176
211
@@ -188,7 +223,10 @@ def _v0(self, project, version, build, filename):
188
223
if not project .single_version :
189
224
versions_active_built_not_hidden = (
190
225
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 ,
192
230
)
193
231
.exclude (hidden = True )
194
232
.only ("slug" )
@@ -377,7 +415,11 @@ def _v0(self, project, version, build, filename):
377
415
378
416
return data
379
417
380
- def _v1 (self , project , version , build , filename ):
418
+ def _v1 (self , project , version , build , filename , user ):
381
419
return {
382
420
"comment" : "Undefined yet. Use v0 for now" ,
383
421
}
422
+
423
+
424
+ class ReadTheDocsConfigJson (SettingsOverrideObject ):
425
+ _default_class = BaseReadTheDocsConfigJson
0 commit comments