Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/rest_framework/viewsets.py: 68%
90 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"""
2ViewSets are essentially just a type of class based view, that doesn't provide
3any method handlers, such as `get()`, `post()`, etc... but instead has actions,
4such as `list()`, `retrieve()`, `create()`, etc...
6Actions are only bound to methods at the point of instantiating the views.
8 user_list = UserViewSet.as_view({'get': 'list'})
9 user_detail = UserViewSet.as_view({'get': 'retrieve'})
11Typically, rather than instantiate views from viewsets directly, you'll
12register the viewset with a router and let the URL conf be determined
13automatically.
15 router = DefaultRouter()
16 router.register(r'users', UserViewSet, 'user')
17 urlpatterns = router.urls
18"""
19from collections import OrderedDict
20from functools import update_wrapper
21from inspect import getmembers
23from django.urls import NoReverseMatch
24from django.utils.decorators import classonlymethod
25from django.views.decorators.csrf import csrf_exempt
27from rest_framework import generics, mixins, views
28from rest_framework.decorators import MethodMapper
29from rest_framework.reverse import reverse
32def _is_extra_action(attr):
33 return hasattr(attr, 'mapping') and isinstance(attr.mapping, MethodMapper)
36def _check_attr_name(func, name):
37 assert func.__name__ == name, (
38 'Expected function (`{func.__name__}`) to match its attribute name '
39 '(`{name}`). If using a decorator, ensure the inner function is '
40 'decorated with `functools.wraps`, or that `{func.__name__}.__name__` '
41 'is otherwise set to `{name}`.').format(func=func, name=name)
42 return func
45class ViewSetMixin:
46 """
47 This is the magic.
49 Overrides `.as_view()` so that it takes an `actions` keyword that performs
50 the binding of HTTP methods to actions on the Resource.
52 For example, to create a concrete view binding the 'GET' and 'POST' methods
53 to the 'list' and 'create' actions...
55 view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
56 """
58 @classonlymethod
59 def as_view(cls, actions=None, **initkwargs):
60 """
61 Because of the way class based views create a closure around the
62 instantiated view, we need to totally reimplement `.as_view`,
63 and slightly modify the view function that is created and returned.
64 """
65 # The name and description initkwargs may be explicitly overridden for
66 # certain route configurations. eg, names of extra actions.
67 cls.name = None
68 cls.description = None
70 # The suffix initkwarg is reserved for displaying the viewset type.
71 # This initkwarg should have no effect if the name is provided.
72 # eg. 'List' or 'Instance'.
73 cls.suffix = None
75 # The detail initkwarg is reserved for introspecting the viewset type.
76 cls.detail = None
78 # Setting a basename allows a view to reverse its action urls. This
79 # value is provided by the router through the initkwargs.
80 cls.basename = None
82 # actions must not be empty
83 if not actions: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true
84 raise TypeError("The `actions` argument must be provided when "
85 "calling `.as_view()` on a ViewSet. For example "
86 "`.as_view({'get': 'list'})`")
88 # sanitize keyword arguments
89 for key in initkwargs:
90 if key in cls.http_method_names: 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 raise TypeError("You tried to pass in the %s method name as a "
92 "keyword argument to %s(). Don't do that."
93 % (key, cls.__name__))
94 if not hasattr(cls, key): 94 ↛ 95line 94 didn't jump to line 95, because the condition on line 94 was never true
95 raise TypeError("%s() received an invalid keyword %r" % (
96 cls.__name__, key))
98 # name and suffix are mutually exclusive
99 if 'name' in initkwargs and 'suffix' in initkwargs: 99 ↛ 100line 99 didn't jump to line 100, because the condition on line 99 was never true
100 raise TypeError("%s() received both `name` and `suffix`, which are "
101 "mutually exclusive arguments." % (cls.__name__))
103 def view(request, *args, **kwargs):
104 self = cls(**initkwargs)
106 if 'get' in actions and 'head' not in actions:
107 actions['head'] = actions['get']
109 # We also store the mapping of request methods to actions,
110 # so that we can later set the action attribute.
111 # eg. `self.action = 'list'` on an incoming GET request.
112 self.action_map = actions
114 # Bind methods to actions
115 # This is the bit that's different to a standard view
116 for method, action in actions.items():
117 handler = getattr(self, action)
118 setattr(self, method, handler)
120 self.request = request
121 self.args = args
122 self.kwargs = kwargs
124 # And continue as usual
125 return self.dispatch(request, *args, **kwargs)
127 # take name and docstring from class
128 update_wrapper(view, cls, updated=())
130 # and possible attributes set by decorators
131 # like csrf_exempt from dispatch
132 update_wrapper(view, cls.dispatch, assigned=())
134 # We need to set these on the view function, so that breadcrumb
135 # generation can pick out these bits of information from a
136 # resolved URL.
137 view.cls = cls
138 view.initkwargs = initkwargs
139 view.actions = actions
140 return csrf_exempt(view)
142 def initialize_request(self, request, *args, **kwargs):
143 """
144 Set the `.action` attribute on the view, depending on the request method.
145 """
146 request = super().initialize_request(request, *args, **kwargs)
147 method = request.method.lower()
148 if method == 'options': 148 ↛ 152line 148 didn't jump to line 152, because the condition on line 148 was never true
149 # This is a special case as we always provide handling for the
150 # options method in the base `View` class.
151 # Unlike the other explicitly defined actions, 'metadata' is implicit.
152 self.action = 'metadata'
153 else:
154 self.action = self.action_map.get(method)
155 return request
157 def reverse_action(self, url_name, *args, **kwargs):
158 """
159 Reverse the action for the given `url_name`.
160 """
161 url_name = '%s-%s' % (self.basename, url_name)
162 namespace = None
163 if self.request and self.request.resolver_match:
164 namespace = self.request.resolver_match.namespace
165 if namespace:
166 url_name = namespace + ':' + url_name
167 kwargs.setdefault('request', self.request)
169 return reverse(url_name, *args, **kwargs)
171 @classmethod
172 def get_extra_actions(cls):
173 """
174 Get the methods that are marked as an extra ViewSet `@action`.
175 """
176 return [_check_attr_name(method, name)
177 for name, method
178 in getmembers(cls, _is_extra_action)]
180 def get_extra_action_url_map(self):
181 """
182 Build a map of {names: urls} for the extra actions.
184 This method will noop if `detail` was not provided as a view initkwarg.
185 """
186 action_urls = OrderedDict()
188 # exit early if `detail` has not been provided
189 if self.detail is None:
190 return action_urls
192 # filter for the relevant extra actions
193 actions = [
194 action for action in self.get_extra_actions()
195 if action.detail == self.detail
196 ]
198 for action in actions:
199 try:
200 url_name = '%s-%s' % (self.basename, action.url_name)
201 url = reverse(url_name, self.args, self.kwargs, request=self.request)
202 view = self.__class__(**action.kwargs)
203 action_urls[view.get_view_name()] = url
204 except NoReverseMatch:
205 pass # URL requires additional arguments, ignore
207 return action_urls
210class ViewSet(ViewSetMixin, views.APIView):
211 """
212 The base ViewSet class does not provide any actions by default.
213 """
214 pass
217class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
218 """
219 The GenericViewSet class does not provide any actions by default,
220 but does include the base set of generic view behavior, such as
221 the `get_object` and `get_queryset` methods.
222 """
223 pass
226class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
227 mixins.ListModelMixin,
228 GenericViewSet):
229 """
230 A viewset that provides default `list()` and `retrieve()` actions.
231 """
232 pass
235class ModelViewSet(mixins.CreateModelMixin,
236 mixins.RetrieveModelMixin,
237 mixins.UpdateModelMixin,
238 mixins.DestroyModelMixin,
239 mixins.ListModelMixin,
240 GenericViewSet):
241 """
242 A viewset that provides default `create()`, `retrieve()`, `update()`,
243 `partial_update()`, `destroy()` and `list()` actions.
244 """
245 pass