Updates
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
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 functools import update_wrapper
|
||||
from inspect import getmembers
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
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
|
||||
|
||||
# Exempt from Django's LoginRequiredMiddleware. Users should set
|
||||
# DEFAULT_PERMISSION_CLASSES to 'rest_framework.permissions.IsAuthenticated' instead
|
||||
if DJANGO_VERSION >= (5, 1):
|
||||
view.login_required = False
|
||||
|
||||
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 = {}
|
||||
|
||||
# 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)
|
||||
namespace = self.request.resolver_match.namespace
|
||||
if namespace:
|
||||
url_name = '%s:%s' % (namespace, 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
|
||||
Reference in New Issue
Block a user