123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- """
- ViewSets are essentially just a type of class based view, that doesn't provide
- any method handlers, such as `get()`, `post()`, etc... but instead has actions,
- such as `list()`, `retrieve()`, `create()`, etc...
- Actions are only bound to methods at the point of instantiating the views.
- user_list = UserViewSet.as_view({'get': 'list'})
- user_detail = UserViewSet.as_view({'get': 'retrieve'})
- Typically, rather than instantiate views from viewsets directly, you'll
- register the viewset with a router and let the URL conf be determined
- automatically.
- router = DefaultRouter()
- router.register(r'users', UserViewSet, 'user')
- urlpatterns = router.urls
- """
- from collections import OrderedDict
- from functools import update_wrapper
- from inspect import getmembers
- from django.urls import NoReverseMatch
- from django.utils.decorators import classonlymethod
- from django.views.decorators.csrf import csrf_exempt
- from rest_framework import generics, mixins, views
- from rest_framework.decorators import MethodMapper
- from rest_framework.reverse import reverse
- def _is_extra_action(attr):
- return hasattr(attr, 'mapping') and isinstance(attr.mapping, MethodMapper)
- def _check_attr_name(func, name):
- assert func.__name__ == name, (
- 'Expected function (`{func.__name__}`) to match its attribute name '
- '(`{name}`). If using a decorator, ensure the inner function is '
- 'decorated with `functools.wraps`, or that `{func.__name__}.__name__` '
- 'is otherwise set to `{name}`.').format(func=func, name=name)
- return func
- class ViewSetMixin:
- """
- This is the magic.
- Overrides `.as_view()` so that it takes an `actions` keyword that performs
- the binding of HTTP methods to actions on the Resource.
- For example, to create a concrete view binding the 'GET' and 'POST' methods
- to the 'list' and 'create' actions...
- view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
- """
- @classonlymethod
- def as_view(cls, actions=None, **initkwargs):
- """
- Because of the way class based views create a closure around the
- instantiated view, we need to totally reimplement `.as_view`,
- and slightly modify the view function that is created and returned.
- """
- # The name and description initkwargs may be explicitly overridden for
- # certain route configurations. eg, names of extra actions.
- cls.name = None
- cls.description = None
- # The suffix initkwarg is reserved for displaying the viewset type.
- # This initkwarg should have no effect if the name is provided.
- # eg. 'List' or 'Instance'.
- cls.suffix = None
- # The detail initkwarg is reserved for introspecting the viewset type.
- cls.detail = None
- # Setting a basename allows a view to reverse its action urls. This
- # value is provided by the router through the initkwargs.
- cls.basename = None
- # actions must not be empty
- if not actions:
- raise TypeError("The `actions` argument must be provided when "
- "calling `.as_view()` on a ViewSet. For example "
- "`.as_view({'get': 'list'})`")
- # sanitize keyword arguments
- for key in initkwargs:
- if key in cls.http_method_names:
- raise TypeError("You tried to pass in the %s method name as a "
- "keyword argument to %s(). Don't do that."
- % (key, cls.__name__))
- if not hasattr(cls, key):
- raise TypeError("%s() received an invalid keyword %r" % (
- cls.__name__, key))
- # name and suffix are mutually exclusive
- if 'name' in initkwargs and 'suffix' in initkwargs:
- raise TypeError("%s() received both `name` and `suffix`, which are "
- "mutually exclusive arguments." % (cls.__name__))
- def view(request, *args, **kwargs):
- self = cls(**initkwargs)
- if 'get' in actions and 'head' not in actions:
- actions['head'] = actions['get']
- # We also store the mapping of request methods to actions,
- # so that we can later set the action attribute.
- # eg. `self.action = 'list'` on an incoming GET request.
- self.action_map = actions
- # Bind methods to actions
- # This is the bit that's different to a standard view
- for method, action in actions.items():
- handler = getattr(self, action)
- setattr(self, method, handler)
- self.request = request
- self.args = args
- self.kwargs = kwargs
- # And continue as usual
- return self.dispatch(request, *args, **kwargs)
- # take name and docstring from class
- update_wrapper(view, cls, updated=())
- # and possible attributes set by decorators
- # like csrf_exempt from dispatch
- update_wrapper(view, cls.dispatch, assigned=())
- # We need to set these on the view function, so that breadcrumb
- # generation can pick out these bits of information from a
- # resolved URL.
- view.cls = cls
- view.initkwargs = initkwargs
- view.actions = actions
- return csrf_exempt(view)
- def initialize_request(self, request, *args, **kwargs):
- """
- Set the `.action` attribute on the view, depending on the request method.
- """
- request = super().initialize_request(request, *args, **kwargs)
- method = request.method.lower()
- if method == 'options':
- # This is a special case as we always provide handling for the
- # options method in the base `View` class.
- # Unlike the other explicitly defined actions, 'metadata' is implicit.
- self.action = 'metadata'
- else:
- self.action = self.action_map.get(method)
- return request
- def reverse_action(self, url_name, *args, **kwargs):
- """
- Reverse the action for the given `url_name`.
- """
- url_name = '%s-%s' % (self.basename, url_name)
- namespace = None
- if self.request and self.request.resolver_match:
- namespace = self.request.resolver_match.namespace
- if namespace:
- url_name = namespace + ':' + url_name
- kwargs.setdefault('request', self.request)
- return reverse(url_name, *args, **kwargs)
- @classmethod
- def get_extra_actions(cls):
- """
- Get the methods that are marked as an extra ViewSet `@action`.
- """
- return [_check_attr_name(method, name)
- for name, method
- in getmembers(cls, _is_extra_action)]
- def get_extra_action_url_map(self):
- """
- Build a map of {names: urls} for the extra actions.
- This method will noop if `detail` was not provided as a view initkwarg.
- """
- action_urls = OrderedDict()
- # exit early if `detail` has not been provided
- if self.detail is None:
- return action_urls
- # filter for the relevant extra actions
- actions = [
- action for action in self.get_extra_actions()
- if action.detail == self.detail
- ]
- for action in actions:
- try:
- url_name = '%s-%s' % (self.basename, action.url_name)
- url = reverse(url_name, self.args, self.kwargs, request=self.request)
- view = self.__class__(**action.kwargs)
- action_urls[view.get_view_name()] = url
- except NoReverseMatch:
- pass # URL requires additional arguments, ignore
- return action_urls
- class ViewSet(ViewSetMixin, views.APIView):
- """
- The base ViewSet class does not provide any actions by default.
- """
- pass
- class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
- """
- The GenericViewSet class does not provide any actions by default,
- but does include the base set of generic view behavior, such as
- the `get_object` and `get_queryset` methods.
- """
- pass
- class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
- mixins.ListModelMixin,
- GenericViewSet):
- """
- A viewset that provides default `list()` and `retrieve()` actions.
- """
- pass
- class ModelViewSet(mixins.CreateModelMixin,
- mixins.RetrieveModelMixin,
- mixins.UpdateModelMixin,
- mixins.DestroyModelMixin,
- mixins.ListModelMixin,
- GenericViewSet):
- """
- A viewset that provides default `create()`, `retrieve()`, `update()`,
- `partial_update()`, `destroy()` and `list()` actions.
- """
- pass
|