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
« 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.
5They are used by simply instantiating a Router class, and then registering
6all the required ViewSets with that router.
8For example, you might have a `urls.py` that looks something like this:
10 router = routers.DefaultRouter()
11 router.register('users', UserViewSet, 'user')
12 router.register('accounts', AccountViewSet, 'account')
14 urlpatterns = router.urls
15"""
16import itertools
17from collections import OrderedDict, namedtuple
19from django.core.exceptions import ImproperlyConfigured
20from django.urls import NoReverseMatch, re_path
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
30Route = namedtuple('Route', ['url', 'mapping', 'name', 'detail', 'initkwargs'])
31DynamicRoute = namedtuple('DynamicRoute', ['url', 'name', 'detail', 'initkwargs'])
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('}', '}}')
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)
48class BaseRouter:
49 def __init__(self):
50 self.registry = []
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))
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
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')
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')
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
81class SimpleRouter(BaseRouter):
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 ]
126 def __init__(self, trailing_slash=True):
127 self.trailing_slash = '/' if trailing_slash else ''
128 super().__init__()
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)
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.'
141 return queryset.model._meta.object_name.lower()
143 def get_routes(self, viewset):
144 """
145 Augment `self.routes` with any dynamically generated routes.
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()
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))
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]
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)
177 return routes
179 def _get_dynamic_route(self, route, action):
180 initkwargs = route.initkwargs.copy()
181 initkwargs.update(action.kwargs)
183 url_path = escape_curly_brackets(action.url_path)
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 )
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
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.
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.
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 )
228 def get_urls(self):
229 """
230 Use the registered viewsets to generate a list of URL patterns.
231 """
232 ret = []
234 for prefix, viewset, basename in self.registry:
235 lookup = self.get_lookup_regex(viewset)
236 routes = self.get_routes(viewset)
238 for route in routes:
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
245 # Build the url pattern
246 regex = route.url.format(
247 prefix=prefix,
248 lookup=lookup,
249 trailing_slash=self.trailing_slash
250 )
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:]
259 initkwargs = route.initkwargs.copy()
260 initkwargs.update({
261 'basename': basename,
262 'detail': route.detail,
263 })
265 view = viewset.as_view(mapping, **initkwargs)
266 name = route.name.format(basename=basename)
267 ret.append(re_path(regex, view, name=name))
269 return ret
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
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
299 return Response(ret)
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
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)
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)
331 return self.APIRootView.as_view(api_root_dict=api_root_dict)
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()
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)
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)
348 return urls