viewsets.py 7.9 KB

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