Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/rest_framework/routers.py: 83%

139 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Routers provide a convenient and consistent way of automatically 

3determining the URL conf for your API. 

4 

5They are used by simply instantiating a Router class, and then registering 

6all the required ViewSets with that router. 

7 

8For example, you might have a `urls.py` that looks something like this: 

9 

10 router = routers.DefaultRouter() 

11 router.register('users', UserViewSet, 'user') 

12 router.register('accounts', AccountViewSet, 'account') 

13 

14 urlpatterns = router.urls 

15""" 

16import itertools 

17from collections import OrderedDict, namedtuple 

18 

19from django.core.exceptions import ImproperlyConfigured 

20from django.urls import NoReverseMatch, re_path 

21 

22from rest_framework import views 

23from rest_framework.response import Response 

24from rest_framework.reverse import reverse 

25from rest_framework.schemas import SchemaGenerator 

26from rest_framework.schemas.views import SchemaView 

27from rest_framework.settings import api_settings 

28from rest_framework.urlpatterns import format_suffix_patterns 

29 

30Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs']) 

31DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs']) 

32 

33 

34def escape_curly_brackets(url_path): 

35 """ 

36 Double brackets in regex of url_path for escape string formatting 

37 """ 

38 return url_path.replace('{', '{{').replace('}', '}}') 

39 

40 

41def flatten(list_of_lists): 

42 """ 

43 Takes an iterable of iterables, returns a single iterable containing all items 

44 """ 

45 return itertools.chain(*list_of_lists) 

46 

47 

48class BaseRouter: 

49 def __init__(self): 

50 self.registry = [] 

51 

52 def register(self, prefix, viewset, basename=None): 

53 if basename is None: 53 ↛ 54line 53 didn't jump to line 54, because the condition on line 53 was never true

54 basename = self.get_default_basename(viewset) 

55 self.registry.append((prefix, viewset, basename)) 

56 

57 # invalidate the urls cache 

58 if hasattr(self, '_urls'): 58 ↛ 59line 58 didn't jump to line 59, because the condition on line 58 was never true

59 del self._urls 

60 

61 def get_default_basename(self, viewset): 

62 """ 

63 If `basename` is not specified, attempt to automatically determine 

64 it from the viewset. 

65 """ 

66 raise NotImplementedError('get_default_basename must be overridden') 

67 

68 def get_urls(self): 

69 """ 

70 Return a list of URL patterns, given the registered viewsets. 

71 """ 

72 raise NotImplementedError('get_urls must be overridden') 

73 

74 @property 

75 def urls(self): 

76 if not hasattr(self, '_urls'): 76 ↛ 78line 76 didn't jump to line 78, because the condition on line 76 was never false

77 self._urls = self.get_urls() 

78 return self._urls 

79 

80 

81class SimpleRouter(BaseRouter): 

82 

83 routes = [ 

84 # List route. 

85 Route( 

86 url=r'^{prefix}{trailing_slash}$', 

87 mapping={ 

88 'get': 'list', 

89 'post': 'create' 

90 }, 

91 name='{basename}-list', 

92 detail=False, 

93 initkwargs={'suffix': 'List'} 

94 ), 

95 # Dynamically generated list routes. Generated using 

96 # @action(detail=False) decorator on methods of the viewset. 

97 DynamicRoute( 

98 url=r'^{prefix}/{url_path}{trailing_slash}$', 

99 name='{basename}-{url_name}', 

100 detail=False, 

101 initkwargs={} 

102 ), 

103 # Detail route. 

104 Route( 

105 url=r'^{prefix}/{lookup}{trailing_slash}$', 

106 mapping={ 

107 'get': 'retrieve', 

108 'put': 'update', 

109 'patch': 'partial_update', 

110 'delete': 'destroy' 

111 }, 

112 name='{basename}-detail', 

113 detail=True, 

114 initkwargs={'suffix': 'Instance'} 

115 ), 

116 # Dynamically generated detail routes. Generated using 

117 # @action(detail=True) decorator on methods of the viewset. 

118 DynamicRoute( 

119 url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$', 

120 name='{basename}-{url_name}', 

121 detail=True, 

122 initkwargs={} 

123 ), 

124 ] 

125 

126 def __init__(self, trailing_slash=True): 

127 self.trailing_slash = '/' if trailing_slash else '' 

128 super().__init__() 

129 

130 def get_default_basename(self, viewset): 

131 """ 

132 If `basename` is not specified, attempt to automatically determine 

133 it from the viewset. 

134 """ 

135 queryset = getattr(viewset, 'queryset', None) 

136 

137 assert queryset is not None, '`basename` argument not specified, and could ' \ 

138 'not automatically determine the name from the viewset, as ' \ 

139 'it does not have a `.queryset` attribute.' 

140 

141 return queryset.model._meta.object_name.lower() 

142 

143 def get_routes(self, viewset): 

144 """ 

145 Augment `self.routes` with any dynamically generated routes. 

146 

147 Returns a list of the Route namedtuple. 

148 """ 

149 # converting to list as iterables are good for one pass, known host needs to be checked again and again for 

150 # different functions. 

151 known_actions = list(flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)])) 

152 extra_actions = viewset.get_extra_actions() 

153 

154 # checking action names against the known actions list 

155 not_allowed = [ 

156 action.__name__ for action in extra_actions 

157 if action.__name__ in known_actions 

158 ] 

159 if not_allowed: 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true

160 msg = ('Cannot use the @action decorator on the following ' 

161 'methods, as they are existing routes: %s') 

162 raise ImproperlyConfigured(msg % ', '.join(not_allowed)) 

163 

164 # partition detail and list actions 

165 detail_actions = [action for action in extra_actions if action.detail] 

166 list_actions = [action for action in extra_actions if not action.detail] 

167 

168 routes = [] 

169 for route in self.routes: 

170 if isinstance(route, DynamicRoute) and route.detail: 

171 routes += [self._get_dynamic_route(route, action) for action in detail_actions] 

172 elif isinstance(route, DynamicRoute) and not route.detail: 

173 routes += [self._get_dynamic_route(route, action) for action in list_actions] 

174 else: 

175 routes.append(route) 

176 

177 return routes 

178 

179 def _get_dynamic_route(self, route, action): 

180 initkwargs = route.initkwargs.copy() 

181 initkwargs.update(action.kwargs) 

182 

183 url_path = escape_curly_brackets(action.url_path) 

184 

185 return Route( 

186 url=route.url.replace('{url_path}', url_path), 

187 mapping=action.mapping, 

188 name=route.name.replace('{url_name}', action.url_name), 

189 detail=route.detail, 

190 initkwargs=initkwargs, 

191 ) 

192 

193 def get_method_map(self, viewset, method_map): 

194 """ 

195 Given a viewset, and a mapping of http methods to actions, 

196 return a new mapping which only includes any mappings that 

197 are actually implemented by the viewset. 

198 """ 

199 bound_methods = {} 

200 for method, action in method_map.items(): 

201 if hasattr(viewset, action): 

202 bound_methods[method] = action 

203 return bound_methods 

204 

205 def get_lookup_regex(self, viewset, lookup_prefix=''): 

206 """ 

207 Given a viewset, return the portion of URL regex that is used 

208 to match against a single instance. 

209 

210 Note that lookup_prefix is not used directly inside REST rest_framework 

211 itself, but is required in order to nicely support nested router 

212 implementations, such as drf-nested-routers. 

213 

214 https://github.com/alanjds/drf-nested-routers 

215 """ 

216 base_regex = '(?P<{lookup_prefix}{lookup_url_kwarg}>{lookup_value})' 

217 # Use `pk` as default field, unset set. Default regex should not 

218 # consume `.json` style suffixes and should break at '/' boundaries. 

219 lookup_field = getattr(viewset, 'lookup_field', 'pk') 

220 lookup_url_kwarg = getattr(viewset, 'lookup_url_kwarg', None) or lookup_field 

221 lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+') 

222 return base_regex.format( 

223 lookup_prefix=lookup_prefix, 

224 lookup_url_kwarg=lookup_url_kwarg, 

225 lookup_value=lookup_value 

226 ) 

227 

228 def get_urls(self): 

229 """ 

230 Use the registered viewsets to generate a list of URL patterns. 

231 """ 

232 ret = [] 

233 

234 for prefix, viewset, basename in self.registry: 

235 lookup = self.get_lookup_regex(viewset) 

236 routes = self.get_routes(viewset) 

237 

238 for route in routes: 

239 

240 # Only actions which actually exist on the viewset will be bound 

241 mapping = self.get_method_map(viewset, route.mapping) 

242 if not mapping: 

243 continue 

244 

245 # Build the url pattern 

246 regex = route.url.format( 

247 prefix=prefix, 

248 lookup=lookup, 

249 trailing_slash=self.trailing_slash 

250 ) 

251 

252 # If there is no prefix, the first part of the url is probably 

253 # controlled by project's urls.py and the router is in an app, 

254 # so a slash in the beginning will (A) cause Django to give 

255 # warnings and (B) generate URLS that will require using '//'. 

256 if not prefix and regex[:2] == '^/': 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true

257 regex = '^' + regex[2:] 

258 

259 initkwargs = route.initkwargs.copy() 

260 initkwargs.update({ 

261 'basename': basename, 

262 'detail': route.detail, 

263 }) 

264 

265 view = viewset.as_view(mapping, **initkwargs) 

266 name = route.name.format(basename=basename) 

267 ret.append(re_path(regex, view, name=name)) 

268 

269 return ret 

270 

271 

272class APIRootView(views.APIView): 

273 """ 

274 The default basic root view for DefaultRouter 

275 """ 

276 _ignore_model_permissions = True 

277 schema = None # exclude from schema 

278 api_root_dict = None 

279 

280 def get(self, request, *args, **kwargs): 

281 # Return a plain {"name": "hyperlink"} response. 

282 ret = OrderedDict() 

283 namespace = request.resolver_match.namespace 

284 for key, url_name in self.api_root_dict.items(): 

285 if namespace: 

286 url_name = namespace + ':' + url_name 

287 try: 

288 ret[key] = reverse( 

289 url_name, 

290 args=args, 

291 kwargs=kwargs, 

292 request=request, 

293 format=kwargs.get('format') 

294 ) 

295 except NoReverseMatch: 

296 # Don't bail out if eg. no list routes exist, only detail routes. 

297 continue 

298 

299 return Response(ret) 

300 

301 

302class DefaultRouter(SimpleRouter): 

303 """ 

304 The default router extends the SimpleRouter, but also adds in a default 

305 API root view, and adds format suffix patterns to the URLs. 

306 """ 

307 include_root_view = True 

308 include_format_suffixes = True 

309 root_view_name = 'api-root' 

310 default_schema_renderers = None 

311 APIRootView = APIRootView 

312 APISchemaView = SchemaView 

313 SchemaGenerator = SchemaGenerator 

314 

315 def __init__(self, *args, **kwargs): 

316 if 'root_renderers' in kwargs: 316 ↛ 317line 316 didn't jump to line 317, because the condition on line 316 was never true

317 self.root_renderers = kwargs.pop('root_renderers') 

318 else: 

319 self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) 

320 super().__init__(*args, **kwargs) 

321 

322 def get_api_root_view(self, api_urls=None): 

323 """ 

324 Return a basic root view. 

325 """ 

326 api_root_dict = OrderedDict() 

327 list_name = self.routes[0].name 

328 for prefix, viewset, basename in self.registry: 

329 api_root_dict[prefix] = list_name.format(basename=basename) 

330 

331 return self.APIRootView.as_view(api_root_dict=api_root_dict) 

332 

333 def get_urls(self): 

334 """ 

335 Generate the list of URL patterns, including a default root view 

336 for the API, and appending `.json` style format suffixes. 

337 """ 

338 urls = super().get_urls() 

339 

340 if self.include_root_view: 340 ↛ 345line 340 didn't jump to line 345, because the condition on line 340 was never false

341 view = self.get_api_root_view(api_urls=urls) 

342 root_url = re_path(r'^$', view, name=self.root_view_name) 

343 urls.append(root_url) 

344 

345 if self.include_format_suffixes: 345 ↛ 348line 345 didn't jump to line 348, because the condition on line 345 was never false

346 urls = format_suffix_patterns(urls) 

347 

348 return urls