mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-31 16:07:38 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			249 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| 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.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 = {}
 | |
| 
 | |
|         # 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
 |