mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-29 17:39:48 +03:00
Merge fd3f538e9f
into c2280e34ec
This commit is contained in:
commit
5792e05f6f
157
docs/api-guide/viewsets-routers.md
Normal file
157
docs/api-guide/viewsets-routers.md
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
<a class="github" href="routers.py"></a> <a class="github" href="viewsets.py"></a>
|
||||||
|
|
||||||
|
# ViewSets & Routers
|
||||||
|
|
||||||
|
> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code.
|
||||||
|
>
|
||||||
|
> — [Ruby on Rails Documentation][cite]
|
||||||
|
|
||||||
|
Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests.
|
||||||
|
|
||||||
|
Conversely, Django stops short of automatically generating URLs, and requires you to explicitly manage your URL configuration.
|
||||||
|
|
||||||
|
REST framework adds support for automatic URL routing, which provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs.
|
||||||
|
|
||||||
|
# ViewSets
|
||||||
|
|
||||||
|
Django REST framework allows you to combine the logic for a set of related views in a single class, called a `ViewSet`. In other frameworks you may also find conceptually similar implementations named something like 'Resources' or 'Controllers'.
|
||||||
|
|
||||||
|
A `ViewSet` class is simply **a type of class-based View, that does not provide any method handlers** such as `.get()` or `.post()`, and instead provides actions such as `.list()` and `.create()`.
|
||||||
|
|
||||||
|
The method handlers for a `ViewSet` are only bound to the corresponding actions at the point of finalizing the view, using the `.as_view()` method.
|
||||||
|
|
||||||
|
Typically, rather than exlicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Let's define a simple viewset that can be used to listing or retrieving all the users in the system.
|
||||||
|
|
||||||
|
class UserViewSet(ViewSet):
|
||||||
|
"""
|
||||||
|
A simple ViewSet that for listing or retrieving users.
|
||||||
|
"""
|
||||||
|
queryset = User.objects.all()
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
|
serializer = UserSerializer(self.queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def retrieve(self, request, pk=None):
|
||||||
|
user = get_object_or_404(self.queryset, pk=pk)
|
||||||
|
serializer = UserSerializer(user)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
If we need to, we can bind this viewset into two seperate views, like so:
|
||||||
|
|
||||||
|
user_list = UserViewSet.as_view({'get': 'list'})
|
||||||
|
user_detail = UserViewSet.as_view({'get': 'retrieve'})
|
||||||
|
|
||||||
|
Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated.
|
||||||
|
|
||||||
|
There are two main advantages of using a `ViewSet` class over using a `View` class.
|
||||||
|
|
||||||
|
* Repeated logic can be combined into a single class. In the above example, we only need to specify the `queryset` once, and it'll be used across multiple views.
|
||||||
|
* By using routers, we no longer need to deal with wiring up the URL conf ourselves.
|
||||||
|
|
||||||
|
Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout.
|
||||||
|
|
||||||
|
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
## ViewSet
|
||||||
|
|
||||||
|
The `ViewSet` class inherits from `APIView`. You can use any of the standard attributes such as `permission_classes`, `authentication_classes` in order to control the API policy on the viewset.
|
||||||
|
|
||||||
|
The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly.
|
||||||
|
|
||||||
|
## ModelViewSet
|
||||||
|
|
||||||
|
The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the
|
||||||
|
|
||||||
|
The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
|
||||||
|
|
||||||
|
class AccountViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
A simple ViewSet for viewing and editing accounts.
|
||||||
|
"""
|
||||||
|
queryset = Account.objects.all()
|
||||||
|
serializer_class = AccountSerializer
|
||||||
|
permission_classes = [IsAccountAdminOrReadOnly]
|
||||||
|
|
||||||
|
Note that you can use any of the standard attributes or method overrides provided by `GenericAPIView`. For example, to use a `ViewSet` that dynamically determines the queryset it should operate on, you might do something like this:
|
||||||
|
|
||||||
|
class AccountViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
A simple ViewSet for viewing and editing the accounts
|
||||||
|
associated with the user.
|
||||||
|
"""
|
||||||
|
serializer_class = AccountSerializer
|
||||||
|
permission_classes = [IsAccountAdminOrReadOnly]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return request.user.accounts.all()
|
||||||
|
|
||||||
|
Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes.
|
||||||
|
|
||||||
|
## ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
The `ReadOnlyModelViewSet` class also inherits from `GenericAPIView`. As with `ModelViewSet` it also includes implementations for various actions, but unlike `ModelViewSet` only provides the 'read-only' actions, `.list()` and `.retrieve()`.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
As with `ModelViewSet`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example:
|
||||||
|
|
||||||
|
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
A simple ViewSet for viewing accounts.
|
||||||
|
"""
|
||||||
|
queryset = Account.objects.all()
|
||||||
|
serializer_class = AccountSerializer
|
||||||
|
|
||||||
|
Again, as with `ModelViewSet`, you can use any of the standard attributes and method overrides available to `GenericAPIView`.
|
||||||
|
|
||||||
|
# Custom ViewSet base classes
|
||||||
|
|
||||||
|
Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes.
|
||||||
|
|
||||||
|
For example, the definition of `ModelViewSet` looks like this:
|
||||||
|
|
||||||
|
class ModelViewSet(mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
viewsets.ViewSetMixin,
|
||||||
|
generics.GenericAPIView):
|
||||||
|
"""
|
||||||
|
A viewset that provides actions for `create`, `retrieve`,
|
||||||
|
`update`, `destroy` and `list` actions.
|
||||||
|
|
||||||
|
To use it, override the class and set the `.queryset`
|
||||||
|
and `.serializer_class` attributes.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API.
|
||||||
|
|
||||||
|
For advanced usage, it's worth noting the that `ViewSetMixin` class can also be applied to the standard Django `View` class. Doing so allows you to use REST framework's automatic routing, but don't want to use it's permissions, authentication and other API policies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Routers
|
||||||
|
|
||||||
|
Routers provide a convenient and simple shortcut for wiring up your application's URLs.
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register('^/', APIRoot, 'api-root')
|
||||||
|
router.register('^users/', UserViewSet, 'user')
|
||||||
|
router.register('^groups/', GroupViewSet, 'group')
|
||||||
|
router.register('^accounts/', AccountViewSet, 'account')
|
||||||
|
|
||||||
|
urlpatterns = router.urlpatterns
|
||||||
|
|
||||||
|
[cite]: http://guides.rubyonrails.org/routing.html
|
|
@ -86,6 +86,7 @@ The tutorial will walk you through the building blocks that make up REST framewo
|
||||||
* [3 - Class based views][tut-3]
|
* [3 - Class based views][tut-3]
|
||||||
* [4 - Authentication & permissions][tut-4]
|
* [4 - Authentication & permissions][tut-4]
|
||||||
* [5 - Relationships & hyperlinked APIs][tut-5]
|
* [5 - Relationships & hyperlinked APIs][tut-5]
|
||||||
|
* [6 - ViewSets & Routers][tut-6]
|
||||||
|
|
||||||
## API Guide
|
## API Guide
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ The API guide is your complete reference manual to all the functionality provide
|
||||||
* [Responses][response]
|
* [Responses][response]
|
||||||
* [Views][views]
|
* [Views][views]
|
||||||
* [Generic views][generic-views]
|
* [Generic views][generic-views]
|
||||||
|
* [ViewSets and Routers][viewsets-routers]
|
||||||
* [Parsers][parsers]
|
* [Parsers][parsers]
|
||||||
* [Renderers][renderers]
|
* [Renderers][renderers]
|
||||||
* [Serializers][serializers]
|
* [Serializers][serializers]
|
||||||
|
@ -197,11 +199,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
[tut-3]: tutorial/3-class-based-views.md
|
[tut-3]: tutorial/3-class-based-views.md
|
||||||
[tut-4]: tutorial/4-authentication-and-permissions.md
|
[tut-4]: tutorial/4-authentication-and-permissions.md
|
||||||
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
||||||
|
[tut-6]: tutorial/6-viewsets-and-routers.md
|
||||||
|
|
||||||
[request]: api-guide/requests.md
|
[request]: api-guide/requests.md
|
||||||
[response]: api-guide/responses.md
|
[response]: api-guide/responses.md
|
||||||
[views]: api-guide/views.md
|
[views]: api-guide/views.md
|
||||||
[generic-views]: api-guide/generic-views.md
|
[generic-views]: api-guide/generic-views.md
|
||||||
|
[viewsets-routers]: api-guide/viewsets-routers.md
|
||||||
[parsers]: api-guide/parsers.md
|
[parsers]: api-guide/parsers.md
|
||||||
[renderers]: api-guide/renderers.md
|
[renderers]: api-guide/renderers.md
|
||||||
[serializers]: api-guide/serializers.md
|
[serializers]: api-guide/serializers.md
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
|
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
|
||||||
<li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
|
<li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
|
||||||
<li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
|
<li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
|
||||||
|
<li><a href="{{ base_url }}/tutorial/6-viewsets-and-routers{{ suffix }}">6 - ViewSets and Routers</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
<li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li>
|
<li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li>
|
||||||
<li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li>
|
<li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li>
|
||||||
<li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li>
|
<li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li>
|
||||||
|
<li><a href="{{ base_url }}/api-guide/viewsets-routers{{ suffix }}">ViewSets and Routers</a></li>
|
||||||
<li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li>
|
<li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li>
|
||||||
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
|
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
|
||||||
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
|
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
|
||||||
|
|
107
docs/tutorial/6-viewsets-and-routers.md
Normal file
107
docs/tutorial/6-viewsets-and-routers.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# Tutorial 6 - ViewSets & Routers
|
||||||
|
|
||||||
|
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modelling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
|
||||||
|
|
||||||
|
`ViewSet` classes are almost the same thing as `View` classes, except that they provide operations such as `read`, or `update`, and not method handlers such as `get` or `put`.
|
||||||
|
|
||||||
|
A `ViewSet` class is only bound to a set of method handlers at the last moment, when it is instantiated into a set of views, typically by using a `Router` class which handles the complexities of defining the URL conf for you.
|
||||||
|
|
||||||
|
## Refactoring to use ViewSets
|
||||||
|
|
||||||
|
Let's take our current set of views, and refactor them into view sets.
|
||||||
|
|
||||||
|
First of all let's refactor our `UserListView` and `UserDetailView` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""
|
||||||
|
This viewset automatically provides `list` and `detail` actions.
|
||||||
|
"""
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
|
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
|
||||||
|
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import link
|
||||||
|
|
||||||
|
class SnippetViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
|
`update` and `destroy` actions.
|
||||||
|
|
||||||
|
Additionally we provide an extra `highlight` action, by using the
|
||||||
|
`@link` decorator.
|
||||||
|
"""
|
||||||
|
queryset = Snippet.objects.all()
|
||||||
|
serializer_class = SnippetSerializer
|
||||||
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
|
||||||
|
IsOwnerOrReadOnly,)
|
||||||
|
|
||||||
|
@link(renderer_classes=[renderers.StaticHTMLRenderer])
|
||||||
|
def highlight(self, request, *args, **kwargs):
|
||||||
|
snippet = self.get_object()
|
||||||
|
return Response(snippet.highlighted)
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
Notice that we've used the `@link` decorator for the `highlight` method.
|
||||||
|
This decorator can be used to add custom endpoints, other than the standard `create`/`update`/`delete` endpoints.
|
||||||
|
|
||||||
|
The `@link` decorator will
|
||||||
|
|
||||||
|
## Binding ViewSets to URLs explicitly
|
||||||
|
|
||||||
|
The handler methods only get bound to the actions when we define the URLConf.
|
||||||
|
To see what's going on under the hood let's first explicitly create a set of views from our ViewSets.
|
||||||
|
|
||||||
|
In the `urls.py` file we first need to bind our `ViewSet` classes into a set of concrete views.
|
||||||
|
|
||||||
|
from snippets.resources import SnippetResource, UserResource
|
||||||
|
|
||||||
|
snippet_list = SnippetViewSet.as_view({'get': 'list', 'post': 'create'})
|
||||||
|
snippet_detail = SnippetViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})
|
||||||
|
snippet_highlight = SnippetViewSet.as_view({'get': 'highlight'})
|
||||||
|
user_list = UserViewSet.as_view({'get': 'list', 'post': 'create'})
|
||||||
|
user_detail = UserViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})
|
||||||
|
|
||||||
|
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
|
||||||
|
|
||||||
|
Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual.
|
||||||
|
|
||||||
|
urlpatterns = format_suffix_patterns(patterns('snippets.views',
|
||||||
|
url(r'^$', 'api_root'),
|
||||||
|
url(r'^snippets/$', snippet_list, name='snippet-list'),
|
||||||
|
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
|
||||||
|
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
|
||||||
|
url(r'^users/$', user_list, name='user-list'),
|
||||||
|
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
|
||||||
|
))
|
||||||
|
|
||||||
|
## Using Routers
|
||||||
|
|
||||||
|
Now that we're using `ViewSet` classes rather than `View` classes, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using a `Router` class. All we need to do is register the appropriate view sets with a router, and let it do the rest.
|
||||||
|
|
||||||
|
Here's our re-wired `urls.py` file.
|
||||||
|
|
||||||
|
from snippets import views
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
# Create a router and register our views and view sets with it.
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'^/', views.api_root)
|
||||||
|
router.register(r'^snippets/', views.SnippetViewSet, 'snippet')
|
||||||
|
router.register(r'^users/', views.UserViewSet, 'user')
|
||||||
|
|
||||||
|
# The urlconf is determined automatically by the router.
|
||||||
|
urlpatterns = router.urlpatterns
|
||||||
|
|
||||||
|
# Add format suffixes to all our URL patterns.
|
||||||
|
urlpatterns = format_suffix_patterns(urlpatterns)
|
||||||
|
|
||||||
|
## Trade-offs between views vs viewsets.
|
||||||
|
|
||||||
|
Using view sets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimises the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
|
||||||
|
|
||||||
|
That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using view sets is less explicit than building your views individually.
|
||||||
|
|
|
@ -47,10 +47,12 @@ path_list = [
|
||||||
'tutorial/3-class-based-views.md',
|
'tutorial/3-class-based-views.md',
|
||||||
'tutorial/4-authentication-and-permissions.md',
|
'tutorial/4-authentication-and-permissions.md',
|
||||||
'tutorial/5-relationships-and-hyperlinked-apis.md',
|
'tutorial/5-relationships-and-hyperlinked-apis.md',
|
||||||
|
'tutorial/6-viewsets-and-routers.md',
|
||||||
'api-guide/requests.md',
|
'api-guide/requests.md',
|
||||||
'api-guide/responses.md',
|
'api-guide/responses.md',
|
||||||
'api-guide/views.md',
|
'api-guide/views.md',
|
||||||
'api-guide/generic-views.md',
|
'api-guide/generic-views.md',
|
||||||
|
'api-guide/viewsets-routers.md',
|
||||||
'api-guide/parsers.md',
|
'api-guide/parsers.md',
|
||||||
'api-guide/renderers.md',
|
'api-guide/renderers.md',
|
||||||
'api-guide/serializers.md',
|
'api-guide/serializers.md',
|
||||||
|
|
|
@ -97,3 +97,25 @@ def permission_classes(permission_classes):
|
||||||
func.permission_classes = permission_classes
|
func.permission_classes = permission_classes
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def link(**kwargs):
|
||||||
|
"""
|
||||||
|
Used to mark a method on a ViewSet that should be routed for GET requests.
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
func.bind_to_method = 'get'
|
||||||
|
func.kwargs = kwargs
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def action(**kwargs):
|
||||||
|
"""
|
||||||
|
Used to mark a method on a ViewSet that should be routed for POST requests.
|
||||||
|
"""
|
||||||
|
def decorator(func):
|
||||||
|
func.bind_to_method = 'post'
|
||||||
|
func.kwargs = kwargs
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
|
@ -24,6 +24,7 @@ from rest_framework.settings import api_settings
|
||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
from rest_framework.utils.breadcrumbs import get_breadcrumbs
|
||||||
|
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||||
from rest_framework import exceptions, parsers, status, VERSION
|
from rest_framework import exceptions, parsers, status, VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
return GenericContentForm()
|
return GenericContentForm()
|
||||||
|
|
||||||
def get_name(self, view):
|
def get_name(self, view):
|
||||||
try:
|
return get_view_name(view.__class__)
|
||||||
return view.get_name()
|
|
||||||
except AttributeError:
|
|
||||||
return smart_text(view.__class__.__name__)
|
|
||||||
|
|
||||||
def get_description(self, view):
|
def get_description(self, view):
|
||||||
try:
|
return get_view_description(view.__class__, html=True)
|
||||||
return view.get_description(html=True)
|
|
||||||
except AttributeError:
|
|
||||||
return smart_text(view.__doc__ or '')
|
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
|
73
rest_framework/routers.py
Normal file
73
rest_framework/routers.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
from django.conf.urls import url, patterns
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRouter(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.registry = []
|
||||||
|
|
||||||
|
def register(self, prefix, viewset, base_name):
|
||||||
|
self.registry.append((prefix, viewset, base_name))
|
||||||
|
|
||||||
|
def get_urlpatterns(self):
|
||||||
|
raise NotImplemented('get_urlpatterns must be overridden')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def urlpatterns(self):
|
||||||
|
if not hasattr(self, '_urlpatterns'):
|
||||||
|
self._urlpatterns = patterns('', *self.get_urlpatterns())
|
||||||
|
return self._urlpatterns
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultRouter(BaseRouter):
|
||||||
|
route_list = [
|
||||||
|
(r'$', {'get': 'list', 'post': 'create'}, 'list'),
|
||||||
|
(r'(?P<pk>[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'),
|
||||||
|
]
|
||||||
|
extra_routes = r'(?P<pk>[^/]+)/%s/$'
|
||||||
|
name_format = '%s-%s'
|
||||||
|
|
||||||
|
def get_urlpatterns(self):
|
||||||
|
ret = []
|
||||||
|
for prefix, viewset, base_name in self.registry:
|
||||||
|
# Bind regular views
|
||||||
|
if not getattr(viewset, '_is_viewset', False):
|
||||||
|
regex = prefix
|
||||||
|
view = viewset
|
||||||
|
name = base_name
|
||||||
|
ret.append(url(regex, view, name=name))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Bind standard CRUD routes
|
||||||
|
for suffix, action_mapping, action_name in self.route_list:
|
||||||
|
|
||||||
|
# Only actions which actually exist on the viewset will be bound
|
||||||
|
bound_actions = {}
|
||||||
|
for method, action in action_mapping.items():
|
||||||
|
if hasattr(viewset, action):
|
||||||
|
bound_actions[method] = action
|
||||||
|
|
||||||
|
# Build the url pattern
|
||||||
|
regex = prefix + suffix
|
||||||
|
view = viewset.as_view(bound_actions, name_suffix=action_name)
|
||||||
|
name = self.name_format % (base_name, action_name)
|
||||||
|
ret.append(url(regex, view, name=name))
|
||||||
|
|
||||||
|
# Bind any extra `@action` or `@link` routes
|
||||||
|
for action_name in dir(viewset):
|
||||||
|
func = getattr(viewset, action_name)
|
||||||
|
http_method = getattr(func, 'bind_to_method', None)
|
||||||
|
|
||||||
|
# Skip if this is not an @action or @link method
|
||||||
|
if not http_method:
|
||||||
|
continue
|
||||||
|
|
||||||
|
suffix = self.extra_routes % action_name
|
||||||
|
|
||||||
|
# Build the url pattern
|
||||||
|
regex = prefix + suffix
|
||||||
|
view = viewset.as_view({http_method: action_name}, **func.kwargs)
|
||||||
|
name = self.name_format % (base_name, action_name)
|
||||||
|
ret.append(url(regex, view, name=name))
|
||||||
|
|
||||||
|
# Return a list of url patterns
|
||||||
|
return ret
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.compat import apply_markdown
|
from rest_framework.compat import apply_markdown
|
||||||
|
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||||
|
|
||||||
# We check that docstrings get nicely un-indented.
|
# We check that docstrings get nicely un-indented.
|
||||||
DESCRIPTION = """an example docstring
|
DESCRIPTION = """an example docstring
|
||||||
|
@ -49,22 +50,16 @@ MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2>
|
||||||
|
|
||||||
|
|
||||||
class TestViewNamesAndDescriptions(TestCase):
|
class TestViewNamesAndDescriptions(TestCase):
|
||||||
def test_resource_name_uses_classname_by_default(self):
|
def test_view_name_uses_class_name(self):
|
||||||
"""Ensure Resource names are based on the classname by default."""
|
"""
|
||||||
|
Ensure view names are based on the class name.
|
||||||
|
"""
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
pass
|
pass
|
||||||
self.assertEqual(MockView().get_name(), 'Mock')
|
self.assertEqual(get_view_name(MockView), 'Mock')
|
||||||
|
|
||||||
def test_resource_name_can_be_set_explicitly(self):
|
def test_view_description_uses_docstring(self):
|
||||||
"""Ensure Resource names can be set using the 'get_name' method."""
|
"""Ensure view descriptions are based on the docstring."""
|
||||||
example = 'Some Other Name'
|
|
||||||
class MockView(APIView):
|
|
||||||
def get_name(self):
|
|
||||||
return example
|
|
||||||
self.assertEqual(MockView().get_name(), example)
|
|
||||||
|
|
||||||
def test_resource_description_uses_docstring_by_default(self):
|
|
||||||
"""Ensure Resource names are based on the docstring by default."""
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
"""an example docstring
|
"""an example docstring
|
||||||
====================
|
====================
|
||||||
|
@ -81,44 +76,32 @@ class TestViewNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
# hash style header #"""
|
# hash style header #"""
|
||||||
|
|
||||||
self.assertEqual(MockView().get_description(), DESCRIPTION)
|
self.assertEqual(get_view_description(MockView), DESCRIPTION)
|
||||||
|
|
||||||
def test_resource_description_can_be_set_explicitly(self):
|
def test_view_description_supports_unicode(self):
|
||||||
"""Ensure Resource descriptions can be set using the 'get_description' method."""
|
"""
|
||||||
example = 'Some other description'
|
Unicode in docstrings should be respected.
|
||||||
|
"""
|
||||||
class MockView(APIView):
|
|
||||||
"""docstring"""
|
|
||||||
def get_description(self):
|
|
||||||
return example
|
|
||||||
self.assertEqual(MockView().get_description(), example)
|
|
||||||
|
|
||||||
def test_resource_description_supports_unicode(self):
|
|
||||||
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
"""Проверка"""
|
"""Проверка"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.assertEqual(MockView().get_description(), "Проверка")
|
self.assertEqual(get_view_description(MockView), "Проверка")
|
||||||
|
|
||||||
|
def test_view_description_can_be_empty(self):
|
||||||
def test_resource_description_does_not_require_docstring(self):
|
"""
|
||||||
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method."""
|
Ensure that if a view has no docstring,
|
||||||
example = 'Some other description'
|
then it's description is the empty string.
|
||||||
|
"""
|
||||||
class MockView(APIView):
|
|
||||||
def get_description(self):
|
|
||||||
return example
|
|
||||||
self.assertEqual(MockView().get_description(), example)
|
|
||||||
|
|
||||||
def test_resource_description_can_be_empty(self):
|
|
||||||
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string."""
|
|
||||||
class MockView(APIView):
|
class MockView(APIView):
|
||||||
pass
|
pass
|
||||||
self.assertEqual(MockView().get_description(), '')
|
self.assertEqual(get_view_description(MockView), '')
|
||||||
|
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""Ensure markdown to HTML works as expected"""
|
"""
|
||||||
|
Ensure markdown to HTML works as expected.
|
||||||
|
"""
|
||||||
if apply_markdown:
|
if apply_markdown:
|
||||||
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
|
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
|
||||||
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
|
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.urlresolvers import resolve, get_script_prefix
|
from django.core.urlresolvers import resolve, get_script_prefix
|
||||||
|
from rest_framework.utils.formatting import get_view_name
|
||||||
|
|
||||||
|
|
||||||
def get_breadcrumbs(url):
|
def get_breadcrumbs(url):
|
||||||
|
@ -16,11 +17,11 @@ def get_breadcrumbs(url):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
# Check if this is a REST framework view, and if so add it to the breadcrumbs
|
||||||
if isinstance(getattr(view, 'cls_instance', None), APIView):
|
if issubclass(getattr(view, 'cls', None), APIView):
|
||||||
# Don't list the same view twice in a row.
|
# Don't list the same view twice in a row.
|
||||||
# Probably an optional trailing slash.
|
# Probably an optional trailing slash.
|
||||||
if not seen or seen[-1] != view:
|
if not seen or seen[-1] != view:
|
||||||
breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url))
|
breadcrumbs_list.insert(0, (get_view_name(view.cls), prefix + url))
|
||||||
seen.append(view)
|
seen.append(view)
|
||||||
|
|
||||||
if url == '':
|
if url == '':
|
||||||
|
|
77
rest_framework/utils/formatting.py
Normal file
77
rest_framework/utils/formatting.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
"""
|
||||||
|
Utility functions to return a formatted name and description for a given view.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from rest_framework.compat import apply_markdown
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_trailing_string(content, trailing):
|
||||||
|
"""
|
||||||
|
Strip trailing component `trailing` from `content` if it exists.
|
||||||
|
Used when generating names from view classes.
|
||||||
|
"""
|
||||||
|
if content.endswith(trailing) and content != trailing:
|
||||||
|
return content[:-len(trailing)]
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_leading_indent(content):
|
||||||
|
"""
|
||||||
|
Remove leading indent from a block of text.
|
||||||
|
Used when generating descriptions from docstrings.
|
||||||
|
"""
|
||||||
|
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
||||||
|
for line in content.splitlines()[1:] if line.lstrip()]
|
||||||
|
|
||||||
|
# unindent the content if needed
|
||||||
|
if whitespace_counts:
|
||||||
|
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
||||||
|
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||||
|
content = content.strip('\n')
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def _camelcase_to_spaces(content):
|
||||||
|
"""
|
||||||
|
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||||
|
Used when generating names from view classes.
|
||||||
|
"""
|
||||||
|
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
|
||||||
|
content = re.sub(camelcase_boundry, ' \\1', content).strip()
|
||||||
|
return ' '.join(content.split('_')).title()
|
||||||
|
|
||||||
|
|
||||||
|
def get_view_name(cls):
|
||||||
|
"""
|
||||||
|
Return a formatted name for an `APIView` class or `@api_view` function.
|
||||||
|
"""
|
||||||
|
name = cls.__name__
|
||||||
|
name = _remove_trailing_string(name, 'View')
|
||||||
|
name = _remove_trailing_string(name, 'ViewSet')
|
||||||
|
return _camelcase_to_spaces(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_view_description(cls, html=False):
|
||||||
|
"""
|
||||||
|
Return a description for an `APIView` class or `@api_view` function.
|
||||||
|
"""
|
||||||
|
description = cls.__doc__ or ''
|
||||||
|
description = _remove_leading_indent(description)
|
||||||
|
if html:
|
||||||
|
return markup_description(description)
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
def markup_description(description):
|
||||||
|
"""
|
||||||
|
Apply HTML markup to the given description.
|
||||||
|
"""
|
||||||
|
if apply_markdown:
|
||||||
|
description = apply_markdown(description)
|
||||||
|
else:
|
||||||
|
description = escape(description).replace('\n', '<br />')
|
||||||
|
return mark_safe(description)
|
|
@ -4,51 +4,13 @@ Provides an APIView class that is used as the base of all class-based views.
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from rest_framework import status, exceptions
|
from rest_framework import status, exceptions
|
||||||
from rest_framework.compat import View, apply_markdown
|
from rest_framework.compat import View
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
import re
|
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||||
|
|
||||||
|
|
||||||
def _remove_trailing_string(content, trailing):
|
|
||||||
"""
|
|
||||||
Strip trailing component `trailing` from `content` if it exists.
|
|
||||||
Used when generating names from view classes.
|
|
||||||
"""
|
|
||||||
if content.endswith(trailing) and content != trailing:
|
|
||||||
return content[:-len(trailing)]
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_leading_indent(content):
|
|
||||||
"""
|
|
||||||
Remove leading indent from a block of text.
|
|
||||||
Used when generating descriptions from docstrings.
|
|
||||||
"""
|
|
||||||
whitespace_counts = [len(line) - len(line.lstrip(' '))
|
|
||||||
for line in content.splitlines()[1:] if line.lstrip()]
|
|
||||||
|
|
||||||
# unindent the content if needed
|
|
||||||
if whitespace_counts:
|
|
||||||
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
|
|
||||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
|
||||||
content = content.strip('\n')
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def _camelcase_to_spaces(content):
|
|
||||||
"""
|
|
||||||
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
|
||||||
Used when generating names from view classes.
|
|
||||||
"""
|
|
||||||
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
|
|
||||||
content = re.sub(camelcase_boundry, ' \\1', content).strip()
|
|
||||||
return ' '.join(content.split('_')).title()
|
|
||||||
|
|
||||||
|
|
||||||
class APIView(View):
|
class APIView(View):
|
||||||
|
@ -64,13 +26,13 @@ class APIView(View):
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
"""
|
"""
|
||||||
Override the default :meth:`as_view` to store an instance of the view
|
Store the original class on the view function.
|
||||||
as an attribute on the callable function. This allows us to discover
|
|
||||||
information about the view when we do URL reverse lookups.
|
This allows us to discover information about the view when we do URL
|
||||||
|
reverse lookups. Used for breadcrumb generation.
|
||||||
"""
|
"""
|
||||||
# TODO: deprecate?
|
|
||||||
view = super(APIView, cls).as_view(**initkwargs)
|
view = super(APIView, cls).as_view(**initkwargs)
|
||||||
view.cls_instance = cls(**initkwargs)
|
view.cls = cls
|
||||||
return view
|
return view
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -90,43 +52,10 @@ class APIView(View):
|
||||||
'Vary': 'Accept'
|
'Vary': 'Accept'
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
"""
|
|
||||||
Return the resource or view class name for use as this view's name.
|
|
||||||
Override to customize.
|
|
||||||
"""
|
|
||||||
# TODO: deprecate?
|
|
||||||
name = self.__class__.__name__
|
|
||||||
name = _remove_trailing_string(name, 'View')
|
|
||||||
return _camelcase_to_spaces(name)
|
|
||||||
|
|
||||||
def get_description(self, html=False):
|
|
||||||
"""
|
|
||||||
Return the resource or view docstring for use as this view's description.
|
|
||||||
Override to customize.
|
|
||||||
"""
|
|
||||||
# TODO: deprecate?
|
|
||||||
description = self.__doc__ or ''
|
|
||||||
description = _remove_leading_indent(description)
|
|
||||||
if html:
|
|
||||||
return self.markup_description(description)
|
|
||||||
return description
|
|
||||||
|
|
||||||
def markup_description(self, description):
|
|
||||||
"""
|
|
||||||
Apply HTML markup to the description of this view.
|
|
||||||
"""
|
|
||||||
# TODO: deprecate?
|
|
||||||
if apply_markdown:
|
|
||||||
description = apply_markdown(description)
|
|
||||||
else:
|
|
||||||
description = escape(description).replace('\n', '<br />')
|
|
||||||
return mark_safe(description)
|
|
||||||
|
|
||||||
def metadata(self, request):
|
def metadata(self, request):
|
||||||
return {
|
return {
|
||||||
'name': self.get_name(),
|
'name': get_view_name(self.__class__),
|
||||||
'description': self.get_description(),
|
'description': get_view_description(self.__class__),
|
||||||
'renders': [renderer.media_type for renderer in self.renderer_classes],
|
'renders': [renderer.media_type for renderer in self.renderer_classes],
|
||||||
'parses': [parser.media_type for parser in self.parser_classes],
|
'parses': [parser.media_type for parser in self.parser_classes],
|
||||||
}
|
}
|
||||||
|
|
89
rest_framework/viewsets.py
Normal file
89
rest_framework/viewsets.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
from functools import update_wrapper
|
||||||
|
from django.utils.decorators import classonlymethod
|
||||||
|
from rest_framework import views, generics, mixins
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSetMixin(object):
|
||||||
|
"""
|
||||||
|
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'})
|
||||||
|
"""
|
||||||
|
_is_viewset = True
|
||||||
|
|
||||||
|
@classonlymethod
|
||||||
|
def as_view(cls, actions=None, name_suffix=None, **initkwargs):
|
||||||
|
"""
|
||||||
|
Main entry point for a request-response process.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
# 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))
|
||||||
|
|
||||||
|
def view(request, *args, **kwargs):
|
||||||
|
self = cls(**initkwargs)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Patch this in as it's otherwise only present from 1.5 onwards
|
||||||
|
if hasattr(self, 'get') and not hasattr(self, 'head'):
|
||||||
|
self.head = self.get
|
||||||
|
|
||||||
|
# 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=())
|
||||||
|
|
||||||
|
view.cls = cls
|
||||||
|
return view
|
||||||
|
|
||||||
|
|
||||||
|
class ViewSet(ViewSetMixin, views.APIView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView
|
||||||
|
# is a bit weird given the diamond inheritence, but it will work for now.
|
||||||
|
# There's some implementation clean up that can happen later.
|
||||||
|
class ModelViewSet(mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
ViewSetMixin,
|
||||||
|
generics.MultipleObjectAPIView,
|
||||||
|
generics.SingleObjectAPIView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
ViewSetMixin,
|
||||||
|
generics.MultipleObjectAPIView,
|
||||||
|
generics.SingleObjectAPIView):
|
||||||
|
pass
|
Loading…
Reference in New Issue
Block a user