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

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... 

5 

6Actions are only bound to methods at the point of instantiating the views. 

7 

8 user_list = UserViewSet.as_view({'get': 'list'}) 

9 user_detail = UserViewSet.as_view({'get': 'retrieve'}) 

10 

11Typically, rather than instantiate views from viewsets directly, you'll 

12register the viewset with a router and let the URL conf be determined 

13automatically. 

14 

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 

22 

23from django.urls import NoReverseMatch 

24from django.utils.decorators import classonlymethod 

25from django.views.decorators.csrf import csrf_exempt 

26 

27from rest_framework import generics, mixins, views 

28from rest_framework.decorators import MethodMapper 

29from rest_framework.reverse import reverse 

30 

31 

32def _is_extra_action(attr): 

33 return hasattr(attr, 'mapping') and isinstance(attr.mapping, MethodMapper) 

34 

35 

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 

43 

44 

45class ViewSetMixin: 

46 """ 

47 This is the magic. 

48 

49 Overrides `.as_view()` so that it takes an `actions` keyword that performs 

50 the binding of HTTP methods to actions on the Resource. 

51 

52 For example, to create a concrete view binding the 'GET' and 'POST' methods 

53 to the 'list' and 'create' actions... 

54 

55 view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) 

56 """ 

57 

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 

69 

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 

74 

75 # The detail initkwarg is reserved for introspecting the viewset type. 

76 cls.detail = None 

77 

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 

81 

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'})`") 

87 

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)) 

97 

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__)) 

102 

103 def view(request, *args, **kwargs): 

104 self = cls(**initkwargs) 

105 

106 if 'get' in actions and 'head' not in actions: 

107 actions['head'] = actions['get'] 

108 

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 

113 

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) 

119 

120 self.request = request 

121 self.args = args 

122 self.kwargs = kwargs 

123 

124 # And continue as usual 

125 return self.dispatch(request, *args, **kwargs) 

126 

127 # take name and docstring from class 

128 update_wrapper(view, cls, updated=()) 

129 

130 # and possible attributes set by decorators 

131 # like csrf_exempt from dispatch 

132 update_wrapper(view, cls.dispatch, assigned=()) 

133 

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) 

141 

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 

156 

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) 

168 

169 return reverse(url_name, *args, **kwargs) 

170 

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)] 

179 

180 def get_extra_action_url_map(self): 

181 """ 

182 Build a map of {names: urls} for the extra actions. 

183 

184 This method will noop if `detail` was not provided as a view initkwarg. 

185 """ 

186 action_urls = OrderedDict() 

187 

188 # exit early if `detail` has not been provided 

189 if self.detail is None: 

190 return action_urls 

191 

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 ] 

197 

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 

206 

207 return action_urls 

208 

209 

210class ViewSet(ViewSetMixin, views.APIView): 

211 """ 

212 The base ViewSet class does not provide any actions by default. 

213 """ 

214 pass 

215 

216 

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 

224 

225 

226class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, 

227 mixins.ListModelMixin, 

228 GenericViewSet): 

229 """ 

230 A viewset that provides default `list()` and `retrieve()` actions. 

231 """ 

232 pass 

233 

234 

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