viewsets.py 8.7 KB

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