mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-27 21:34:25 +03:00
fix merge
This commit is contained in:
commit
e84bf2140c
4
AUTHORS
4
AUTHORS
|
@ -12,11 +12,13 @@ Andrew Straw <astraw>
|
|||
Zeth <zeth>
|
||||
Fernando Zunino <fzunino>
|
||||
Jens Alm <ulmus>
|
||||
Craig Blaszczyk <jakul>
|
||||
Craig Blaszczyk <jakul>
|
||||
Garcia Solero <garciasolero>
|
||||
Tom Drummond <devioustree>
|
||||
Danilo Bargen <gwrtheyrn>
|
||||
Andrew McCloud <amccloud>
|
||||
Thomas Steinacher <thomasst>
|
||||
Meurig Freeman <meurig>
|
||||
|
||||
THANKS TO:
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
The :mod:`compatibility ` module provides support for backwards compatibility with older versions of django/python.
|
||||
The :mod:`compat` module provides support for backwards compatibility with older versions of django/python.
|
||||
"""
|
||||
|
||||
# cStringIO only if it's available
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
"""
|
||||
The :mod:`mixins` module provides a set of reusable `mixin`
|
||||
The :mod:`mixins` module provides a set of reusable `mixin`
|
||||
classes that can be added to a `View`.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.db.models.query import QuerySet
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models.fields.related import ForeignKey
|
||||
from django.http import HttpResponse
|
||||
|
||||
from djangorestframework import status
|
||||
from djangorestframework.parsers import FormParser, MultiPartParser
|
||||
from djangorestframework.renderers import BaseRenderer
|
||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
|
||||
|
||||
from decimal import Decimal
|
||||
import re
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
|
@ -52,7 +49,7 @@ class RequestMixin(object):
|
|||
|
||||
"""
|
||||
The set of request parsers that the view can handle.
|
||||
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`parsers` module.
|
||||
"""
|
||||
parsers = ()
|
||||
|
@ -158,7 +155,7 @@ class RequestMixin(object):
|
|||
# We only need to use form overloading on form POST requests.
|
||||
if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
|
||||
return
|
||||
|
||||
|
||||
# At this point we're committed to parsing the request as form data.
|
||||
self._data = data = self.request.POST.copy()
|
||||
self._files = self.request.FILES
|
||||
|
@ -203,12 +200,12 @@ class RequestMixin(object):
|
|||
"""
|
||||
return [parser.media_type for parser in self.parsers]
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def _default_parser(self):
|
||||
"""
|
||||
Return the view's default parser class.
|
||||
"""
|
||||
"""
|
||||
return self.parsers[0]
|
||||
|
||||
|
||||
|
@ -218,7 +215,7 @@ class RequestMixin(object):
|
|||
class ResponseMixin(object):
|
||||
"""
|
||||
Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
|
||||
|
||||
|
||||
Default behavior is to use standard HTTP Accept header content negotiation.
|
||||
Also supports overriding the content type by specifying an ``_accept=`` parameter in the URL.
|
||||
Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
|
||||
|
@ -229,8 +226,8 @@ class ResponseMixin(object):
|
|||
|
||||
"""
|
||||
The set of response renderers that the view can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`renderers` module.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`renderers` module.
|
||||
"""
|
||||
renderers = ()
|
||||
|
||||
|
@ -253,7 +250,7 @@ class ResponseMixin(object):
|
|||
# Set the media type of the response
|
||||
# Note that the renderer *could* override it in .render() if required.
|
||||
response.media_type = renderer.media_type
|
||||
|
||||
|
||||
# Serialize the response content
|
||||
if response.has_content_body:
|
||||
content = renderer.render(response.cleaned_content, media_type)
|
||||
|
@ -317,7 +314,7 @@ class ResponseMixin(object):
|
|||
Return an list of all the media types that this view can render.
|
||||
"""
|
||||
return [renderer.media_type for renderer in self.renderers]
|
||||
|
||||
|
||||
@property
|
||||
def _rendered_formats(self):
|
||||
"""
|
||||
|
@ -339,18 +336,18 @@ class AuthMixin(object):
|
|||
"""
|
||||
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
The set of authentication types that this view can handle.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`authentication` module.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`authentication` module.
|
||||
"""
|
||||
authentication = ()
|
||||
|
||||
"""
|
||||
The set of permissions that will be enforced on this view.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`permissions` module.
|
||||
|
||||
Should be a tuple/list of classes as described in the :mod:`permissions` module.
|
||||
"""
|
||||
permissions = ()
|
||||
|
||||
|
@ -359,7 +356,7 @@ class AuthMixin(object):
|
|||
def user(self):
|
||||
"""
|
||||
Returns the :obj:`user` for the current request, as determined by the set of
|
||||
:class:`authentication` classes applied to the :class:`View`.
|
||||
:class:`authentication` classes applied to the :class:`View`.
|
||||
"""
|
||||
if not hasattr(self, '_user'):
|
||||
self._user = self._authenticate()
|
||||
|
@ -451,7 +448,10 @@ class ResourceMixin(object):
|
|||
return self._resource.filter_response(obj)
|
||||
|
||||
def get_bound_form(self, content=None, method=None):
|
||||
return self._resource.get_bound_form(content, method=method)
|
||||
if hasattr(self._resource, 'get_bound_form'):
|
||||
return self._resource.get_bound_form(content, method=method)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
@ -538,13 +538,13 @@ class CreateModelMixin(object):
|
|||
|
||||
for fieldname in m2m_data:
|
||||
manager = getattr(instance, fieldname)
|
||||
|
||||
|
||||
if hasattr(manager, 'add'):
|
||||
manager.add(*m2m_data[fieldname][1])
|
||||
else:
|
||||
data = {}
|
||||
data[manager.source_field_name] = instance
|
||||
|
||||
|
||||
for related_item in m2m_data[fieldname][1]:
|
||||
data[m2m_data[fieldname][0]] = related_item
|
||||
manager.through(**data).save()
|
||||
|
@ -561,11 +561,11 @@ class UpdateModelMixin(object):
|
|||
"""
|
||||
def put(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
|
||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
||||
|
||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
# If we have any none kwargs then assume the last represents the primary key
|
||||
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
|
@ -637,3 +637,93 @@ class ListModelMixin(object):
|
|||
return queryset.filter(**kwargs)
|
||||
|
||||
|
||||
########## Pagination Mixins ##########
|
||||
|
||||
class PaginatorMixin(object):
|
||||
"""
|
||||
Adds pagination support to GET requests
|
||||
Obviously should only be used on lists :)
|
||||
|
||||
A default limit can be set by setting `limit` on the object. This will also
|
||||
be used as the maximum if the client sets the `limit` GET param
|
||||
"""
|
||||
limit = 20
|
||||
|
||||
def get_limit(self):
|
||||
""" Helper method to determine what the `limit` should be """
|
||||
try:
|
||||
limit = int(self.request.GET.get('limit', self.limit))
|
||||
return min(limit, self.limit)
|
||||
except ValueError:
|
||||
return self.limit
|
||||
|
||||
def url_with_page_number(self, page_number):
|
||||
""" Constructs a url used for getting the next/previous urls """
|
||||
url = "%s?page=%d" % (self.request.path, page_number)
|
||||
|
||||
limit = self.get_limit()
|
||||
if limit != self.limit:
|
||||
url = "%s&limit=%d" % (url, limit)
|
||||
|
||||
return url
|
||||
|
||||
def next(self, page):
|
||||
""" Returns a url to the next page of results (if any) """
|
||||
if not page.has_next():
|
||||
return None
|
||||
|
||||
return self.url_with_page_number(page.next_page_number())
|
||||
|
||||
def previous(self, page):
|
||||
""" Returns a url to the previous page of results (if any) """
|
||||
if not page.has_previous():
|
||||
return None
|
||||
|
||||
return self.url_with_page_number(page.previous_page_number())
|
||||
|
||||
def serialize_page_info(self, page):
|
||||
""" This is some useful information that is added to the response """
|
||||
return {
|
||||
'next': self.next(page),
|
||||
'page': page.number,
|
||||
'pages': page.paginator.num_pages,
|
||||
'per_page': self.get_limit(),
|
||||
'previous': self.previous(page),
|
||||
'total': page.paginator.count,
|
||||
}
|
||||
|
||||
def filter_response(self, obj):
|
||||
"""
|
||||
Given the response content, paginate and then serialize.
|
||||
|
||||
The response is modified to include to useful data relating to the number
|
||||
of objects, number of pages, next/previous urls etc. etc.
|
||||
|
||||
The serialised objects are put into `results` on this new, modified
|
||||
response
|
||||
"""
|
||||
|
||||
# We don't want to paginate responses for anything other than GET requests
|
||||
if self.method.upper() != 'GET':
|
||||
return self._resource.filter_response(obj)
|
||||
|
||||
paginator = Paginator(obj, self.get_limit())
|
||||
|
||||
try:
|
||||
page_num = int(self.request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
|
||||
{'detail': 'That page contains no results'})
|
||||
|
||||
if page_num not in paginator.page_range:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND,
|
||||
{'detail': 'That page contains no results'})
|
||||
|
||||
page = paginator.page(page_num)
|
||||
|
||||
serialized_object_list = self._resource.filter_response(page.object_list)
|
||||
serialized_page_info = self.serialize_page_info(page)
|
||||
|
||||
serialized_page_info['results'] = serialized_object_list
|
||||
|
||||
return serialized_page_info
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
The :mod:`permissions` module bundles a set of permission classes that are used
|
||||
for checking if a request passes a certain set of constraints. You can assign a permission
|
||||
The :mod:`permissions` module bundles a set of permission classes that are used
|
||||
for checking if a request passes a certain set of constraints. You can assign a permission
|
||||
class to your view by setting your View's :attr:`permissions` class attribute.
|
||||
"""
|
||||
|
||||
|
@ -40,7 +40,7 @@ class BasePermission(object):
|
|||
Permission classes are always passed the current view on creation.
|
||||
"""
|
||||
self.view = view
|
||||
|
||||
|
||||
def check_permission(self, auth):
|
||||
"""
|
||||
Should simply return, or raise an :exc:`response.ErrorResponse`.
|
||||
|
@ -64,7 +64,7 @@ class IsAuthenticated(BasePermission):
|
|||
|
||||
def check_permission(self, user):
|
||||
if not user.is_authenticated():
|
||||
raise _403_FORBIDDEN_RESPONSE
|
||||
raise _403_FORBIDDEN_RESPONSE
|
||||
|
||||
|
||||
class IsAdminUser(BasePermission):
|
||||
|
@ -82,7 +82,7 @@ class IsUserOrIsAnonReadOnly(BasePermission):
|
|||
The request is authenticated as a user, or is a read-only request.
|
||||
"""
|
||||
|
||||
def check_permission(self, user):
|
||||
def check_permission(self, user):
|
||||
if (not user.is_authenticated() and
|
||||
self.view.method != 'GET' and
|
||||
self.view.method != 'HEAD'):
|
||||
|
@ -100,7 +100,7 @@ class BaseThrottle(BasePermission):
|
|||
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
|
||||
|
||||
Previous request information used for throttling is stored in the cache.
|
||||
"""
|
||||
"""
|
||||
|
||||
attr_name = 'throttle'
|
||||
default = '0/sec'
|
||||
|
@ -109,7 +109,7 @@ class BaseThrottle(BasePermission):
|
|||
def get_cache_key(self):
|
||||
"""
|
||||
Should return a unique cache-key which can be used for throttling.
|
||||
Muse be overridden.
|
||||
Must be overridden.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -123,7 +123,7 @@ class BaseThrottle(BasePermission):
|
|||
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
|
||||
self.auth = auth
|
||||
self.check_throttle()
|
||||
|
||||
|
||||
def check_throttle(self):
|
||||
"""
|
||||
Implement the check to see if the request should be throttled.
|
||||
|
@ -134,7 +134,7 @@ class BaseThrottle(BasePermission):
|
|||
self.key = self.get_cache_key()
|
||||
self.history = cache.get(self.key, [])
|
||||
self.now = self.timer()
|
||||
|
||||
|
||||
# Drop any requests from the history which have now passed the
|
||||
# throttle duration
|
||||
while self.history and self.history[-1] <= self.now - self.duration:
|
||||
|
@ -153,7 +153,7 @@ class BaseThrottle(BasePermission):
|
|||
cache.set(self.key, self.history, self.duration)
|
||||
header = 'status=SUCCESS; next=%s sec' % self.next()
|
||||
self.view.add_header('X-Throttle', header)
|
||||
|
||||
|
||||
def throttle_failure(self):
|
||||
"""
|
||||
Called when a request to the API has failed due to throttling.
|
||||
|
@ -162,7 +162,7 @@ class BaseThrottle(BasePermission):
|
|||
header = 'status=FAILURE; next=%s sec' % self.next()
|
||||
self.view.add_header('X-Throttle', header)
|
||||
raise _503_SERVICE_UNAVAILABLE
|
||||
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Returns the recommended next request time in seconds.
|
||||
|
@ -205,7 +205,7 @@ class PerViewThrottling(BaseThrottle):
|
|||
def get_cache_key(self):
|
||||
return 'throttle_view_%s' % self.view.__class__.__name__
|
||||
|
||||
|
||||
|
||||
class PerResourceThrottling(BaseThrottle):
|
||||
"""
|
||||
Limits the rate of API calls that may be used against all views on
|
||||
|
|
|
@ -106,7 +106,8 @@ class Serializer(object):
|
|||
|
||||
|
||||
def __init__(self, depth=None, stack=[], **kwargs):
|
||||
self.depth = depth or self.depth
|
||||
if depth is not None:
|
||||
self.depth = depth
|
||||
self.stack = stack
|
||||
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class MockView(View):
|
|||
def post(self, request):
|
||||
if request.POST.get('example') is not None:
|
||||
return Response(status.OK)
|
||||
|
||||
|
||||
return Response(status.INTERNAL_SERVER_ERROR)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
@ -103,104 +103,104 @@ class TestContentParsing(TestCase):
|
|||
view.request = self.req.post('/', form_data)
|
||||
view.parsers = (PlainTextParser,)
|
||||
self.assertEqual(view.DATA, content)
|
||||
|
||||
|
||||
def test_accessing_post_after_data_form(self):
|
||||
"""Ensures request.POST can be accessed after request.DATA in form request"""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view = RequestMixin()
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
|
||||
|
||||
self.assertEqual(view.DATA.items(), form_data.items())
|
||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||
|
||||
def test_accessing_post_after_data_for_json(self):
|
||||
"""Ensures request.POST can be accessed after request.DATA in json request"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
view = RequestMixin()
|
||||
view.parsers = (JSONParser,)
|
||||
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
|
||||
self.assertEqual(view.DATA.items(), data.items())
|
||||
self.assertEqual(view.request.POST.items(), [])
|
||||
|
||||
|
||||
# def test_accessing_post_after_data_for_json(self):
|
||||
# """Ensures request.POST can be accessed after request.DATA in json request"""
|
||||
# from django.utils import simplejson as json
|
||||
|
||||
# data = {'qwerty': 'uiop'}
|
||||
# content = json.dumps(data)
|
||||
# content_type = 'application/json'
|
||||
|
||||
# view = RequestMixin()
|
||||
# view.parsers = (JSONParser,)
|
||||
|
||||
# view.request = self.req.post('/', content, content_type=content_type)
|
||||
|
||||
# self.assertEqual(view.DATA.items(), data.items())
|
||||
# self.assertEqual(view.request.POST.items(), [])
|
||||
|
||||
def test_accessing_post_after_data_for_overloaded_json(self):
|
||||
"""Ensures request.POST can be accessed after request.DATA in overloaded json request"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
view = RequestMixin()
|
||||
view.parsers = (JSONParser,)
|
||||
|
||||
|
||||
form_data = {view._CONTENT_PARAM: content,
|
||||
view._CONTENTTYPE_PARAM: content_type}
|
||||
|
||||
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
|
||||
|
||||
self.assertEqual(view.DATA.items(), data.items())
|
||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||
|
||||
|
||||
def test_accessing_data_after_post_form(self):
|
||||
"""Ensures request.DATA can be accessed after request.POST in form request"""
|
||||
form_data = {'qwerty': 'uiop'}
|
||||
view = RequestMixin()
|
||||
view.parsers = (FormParser, MultiPartParser)
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
|
||||
|
||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||
self.assertEqual(view.DATA.items(), form_data.items())
|
||||
|
||||
|
||||
def test_accessing_data_after_post_for_json(self):
|
||||
"""Ensures request.DATA can be accessed after request.POST in json request"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
view = RequestMixin()
|
||||
view.parsers = (JSONParser,)
|
||||
|
||||
|
||||
view.request = self.req.post('/', content, content_type=content_type)
|
||||
|
||||
|
||||
post_items = view.request.POST.items()
|
||||
|
||||
|
||||
self.assertEqual(len(post_items), 1)
|
||||
self.assertEqual(len(post_items[0]), 2)
|
||||
self.assertEqual(post_items[0][0], content)
|
||||
self.assertEqual(view.DATA.items(), data.items())
|
||||
|
||||
|
||||
def test_accessing_data_after_post_for_overloaded_json(self):
|
||||
"""Ensures request.DATA can be accessed after request.POST in overloaded json request"""
|
||||
from django.utils import simplejson as json
|
||||
|
||||
|
||||
data = {'qwerty': 'uiop'}
|
||||
content = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
|
||||
|
||||
view = RequestMixin()
|
||||
view.parsers = (JSONParser,)
|
||||
|
||||
|
||||
form_data = {view._CONTENT_PARAM: content,
|
||||
view._CONTENTTYPE_PARAM: content_type}
|
||||
|
||||
|
||||
view.request = self.req.post('/', data=form_data)
|
||||
|
||||
|
||||
self.assertEqual(view.request.POST.items(), form_data.items())
|
||||
self.assertEqual(view.DATA.items(), data.items())
|
||||
|
||||
class TestContentParsingWithAuthentication(TestCase):
|
||||
urls = 'djangorestframework.tests.content'
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.csrf_client = Client(enforce_csrf_checks=True)
|
||||
self.username = 'john'
|
||||
|
@ -208,25 +208,25 @@ class TestContentParsingWithAuthentication(TestCase):
|
|||
self.password = 'password'
|
||||
self.user = User.objects.create_user(self.username, self.email, self.password)
|
||||
self.req = RequestFactory()
|
||||
|
||||
|
||||
def test_user_logged_in_authentication_has_post_when_not_logged_in(self):
|
||||
"""Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in"""
|
||||
content = {'example': 'example'}
|
||||
|
||||
|
||||
response = self.client.post('/', content)
|
||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
response = self.csrf_client.post('/', content)
|
||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
def test_user_logged_in_authentication_has_post_when_logged_in(self):
|
||||
"""Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
||||
self.client.login(username='john', password='password')
|
||||
self.csrf_client.login(username='john', password='password')
|
||||
content = {'example': 'example'}
|
||||
|
||||
response = self.client.post('/', content)
|
||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
|
||||
response = self.csrf_client.post('/', content)
|
||||
self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
# def test_user_logged_in_authentication_has_post_when_logged_in(self):
|
||||
# """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
|
||||
# self.client.login(username='john', password='password')
|
||||
# self.csrf_client.login(username='john', password='password')
|
||||
# content = {'example': 'example'}
|
||||
|
||||
# response = self.client.post('/', content)
|
||||
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
# response = self.csrf_client.post('/', content)
|
||||
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
"""Tests for the status module"""
|
||||
"""Tests for the mixin module"""
|
||||
from django.test import TestCase
|
||||
from django.utils import simplejson as json
|
||||
from djangorestframework import status
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from django.contrib.auth.models import Group, User
|
||||
from djangorestframework.mixins import CreateModelMixin
|
||||
from djangorestframework.mixins import CreateModelMixin, PaginatorMixin
|
||||
from djangorestframework.resources import ModelResource
|
||||
from djangorestframework.response import Response
|
||||
from djangorestframework.tests.models import CustomUser
|
||||
from djangorestframework.views import View
|
||||
|
||||
|
||||
class TestModelCreation(TestCase):
|
||||
class TestModelCreation(TestCase):
|
||||
"""Tests on CreateModelMixin"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -25,23 +28,26 @@ class TestModelCreation(TestCase):
|
|||
mixin = CreateModelMixin()
|
||||
mixin.resource = GroupResource
|
||||
mixin.CONTENT = form_data
|
||||
|
||||
|
||||
response = mixin.post(request)
|
||||
self.assertEquals(1, Group.objects.count())
|
||||
self.assertEquals('foo', response.cleaned_content.name)
|
||||
|
||||
|
||||
def test_creation_with_m2m_relation(self):
|
||||
class UserResource(ModelResource):
|
||||
model = User
|
||||
|
||||
|
||||
def url(self, instance):
|
||||
return "/users/%i" % instance.id
|
||||
|
||||
group = Group(name='foo')
|
||||
group.save()
|
||||
|
||||
form_data = {'username': 'bar', 'password': 'baz', 'groups': [group.id]}
|
||||
form_data = {
|
||||
'username': 'bar',
|
||||
'password': 'baz',
|
||||
'groups': [group.id]
|
||||
}
|
||||
request = self.req.post('/groups', data=form_data)
|
||||
cleaned_data = dict(form_data)
|
||||
cleaned_data['groups'] = [group]
|
||||
|
@ -53,18 +59,18 @@ class TestModelCreation(TestCase):
|
|||
self.assertEquals(1, User.objects.count())
|
||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
|
||||
|
||||
|
||||
def test_creation_with_m2m_relation_through(self):
|
||||
"""
|
||||
Tests creation where the m2m relation uses a through table
|
||||
"""
|
||||
class UserResource(ModelResource):
|
||||
model = CustomUser
|
||||
|
||||
|
||||
def url(self, instance):
|
||||
return "/customusers/%i" % instance.id
|
||||
|
||||
form_data = {'username': 'bar0', 'groups': []}
|
||||
|
||||
form_data = {'username': 'bar0', 'groups': []}
|
||||
request = self.req.post('/groups', data=form_data)
|
||||
cleaned_data = dict(form_data)
|
||||
cleaned_data['groups'] = []
|
||||
|
@ -74,12 +80,12 @@ class TestModelCreation(TestCase):
|
|||
|
||||
response = mixin.post(request)
|
||||
self.assertEquals(1, CustomUser.objects.count())
|
||||
self.assertEquals(0, response.cleaned_content.groups.count())
|
||||
self.assertEquals(0, response.cleaned_content.groups.count())
|
||||
|
||||
group = Group(name='foo1')
|
||||
group.save()
|
||||
|
||||
form_data = {'username': 'bar1', 'groups': [group.id]}
|
||||
form_data = {'username': 'bar1', 'groups': [group.id]}
|
||||
request = self.req.post('/groups', data=form_data)
|
||||
cleaned_data = dict(form_data)
|
||||
cleaned_data['groups'] = [group]
|
||||
|
@ -91,12 +97,11 @@ class TestModelCreation(TestCase):
|
|||
self.assertEquals(2, CustomUser.objects.count())
|
||||
self.assertEquals(1, response.cleaned_content.groups.count())
|
||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||
|
||||
|
||||
|
||||
group2 = Group(name='foo2')
|
||||
group2.save()
|
||||
|
||||
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
||||
group2.save()
|
||||
|
||||
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
||||
request = self.req.post('/groups', data=form_data)
|
||||
cleaned_data = dict(form_data)
|
||||
cleaned_data['groups'] = [group, group2]
|
||||
|
@ -109,5 +114,124 @@ class TestModelCreation(TestCase):
|
|||
self.assertEquals(2, response.cleaned_content.groups.count())
|
||||
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
|
||||
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].name)
|
||||
|
||||
|
||||
|
||||
class MockPaginatorView(PaginatorMixin, View):
|
||||
total = 60
|
||||
|
||||
def get(self, request):
|
||||
return range(0, self.total)
|
||||
|
||||
def post(self, request):
|
||||
return Response(status.CREATED, {'status': 'OK'})
|
||||
|
||||
|
||||
class TestPagination(TestCase):
|
||||
def setUp(self):
|
||||
self.req = RequestFactory()
|
||||
|
||||
def test_default_limit(self):
|
||||
""" Tests if pagination works without overwriting the limit """
|
||||
request = self.req.get('/paginator')
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||
self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
||||
|
||||
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
||||
|
||||
def test_overwriting_limit(self):
|
||||
""" Tests if the limit can be overwritten """
|
||||
limit = 10
|
||||
|
||||
request = self.req.get('/paginator')
|
||||
response = MockPaginatorView.as_view(limit=limit)(request)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(content['per_page'], limit)
|
||||
|
||||
self.assertEqual(range(0, limit), content['results'])
|
||||
|
||||
def test_limit_param(self):
|
||||
""" Tests if the client can set the limit """
|
||||
from math import ceil
|
||||
|
||||
limit = 5
|
||||
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
||||
|
||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||
self.assertEqual(limit, content['per_page'])
|
||||
self.assertEqual(num_pages, content['pages'])
|
||||
|
||||
def test_exceeding_limit(self):
|
||||
""" Makes sure the client cannot exceed the default limit """
|
||||
from math import ceil
|
||||
|
||||
limit = MockPaginatorView.limit + 10
|
||||
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
||||
|
||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
||||
self.assertNotEqual(limit, content['per_page'])
|
||||
self.assertNotEqual(num_pages, content['pages'])
|
||||
self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
||||
|
||||
def test_only_works_for_get(self):
|
||||
""" Pagination should only work for GET requests """
|
||||
request = self.req.post('/paginator', data={'content': 'spam'})
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, status.CREATED)
|
||||
self.assertEqual(None, content.get('per_page'))
|
||||
self.assertEqual('OK', content['status'])
|
||||
|
||||
def test_non_int_page(self):
|
||||
""" Tests that it can handle invalid values """
|
||||
request = self.req.get('/paginator/?page=spam')
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, status.NOT_FOUND)
|
||||
|
||||
def test_page_range(self):
|
||||
""" Tests that the page range is handle correctly """
|
||||
request = self.req.get('/paginator/?page=0')
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.NOT_FOUND)
|
||||
|
||||
request = self.req.get('/paginator/')
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
||||
|
||||
num_pages = content['pages']
|
||||
|
||||
request = self.req.get('/paginator/?page=%d' % num_pages)
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.OK)
|
||||
self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
|
||||
|
||||
request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
|
||||
response = MockPaginatorView.as_view()(request)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(response.status_code, status.NOT_FOUND)
|
||||
|
|
|
@ -228,6 +228,7 @@ if YAMLRenderer:
|
|||
(data, files) = parser.parse(StringIO(content))
|
||||
self.assertEquals(obj, data)
|
||||
|
||||
|
||||
|
||||
class XMLRendererTestCase(TestCase):
|
||||
"""
|
||||
|
@ -288,4 +289,4 @@ class XMLRendererTestCase(TestCase):
|
|||
def assertXMLContains(self, xml, string):
|
||||
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
|
||||
self.assertTrue(xml.endswith('</root>'))
|
||||
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
||||
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
||||
|
|
|
@ -1,17 +1,109 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
from django import forms
|
||||
from django.db import models
|
||||
|
||||
from djangorestframework.views import View
|
||||
from djangorestframework.parsers import JSONParser
|
||||
from djangorestframework.resources import ModelResource
|
||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
|
||||
class MockView(View):
|
||||
"""This is a basic mock view"""
|
||||
pass
|
||||
|
||||
class ResourceMockView(View):
|
||||
"""This is a resource-based mock view"""
|
||||
|
||||
class MockForm(forms.Form):
|
||||
foo = forms.BooleanField(required=False)
|
||||
bar = forms.IntegerField(help_text='Must be an integer.')
|
||||
baz = forms.CharField(max_length=32)
|
||||
|
||||
form = MockForm
|
||||
|
||||
class MockResource(ModelResource):
|
||||
"""This is a mock model-based resource"""
|
||||
|
||||
class MockResourceModel(models.Model):
|
||||
foo = models.BooleanField()
|
||||
bar = models.IntegerField(help_text='Must be an integer.')
|
||||
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
||||
|
||||
model = MockResourceModel
|
||||
fields = ('foo', 'bar', 'baz')
|
||||
|
||||
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||
url(r'^robots.txt$', 'deny_robots'),
|
||||
url(r'^favicon.ico$', 'favicon'),
|
||||
url(r'^accounts/login$', 'api_login'),
|
||||
url(r'^accounts/logout$', 'api_logout'),
|
||||
url(r'^mock/$', MockView.as_view()),
|
||||
url(r'^resourcemock/$', ResourceMockView.as_view()),
|
||||
url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)),
|
||||
url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)),
|
||||
)
|
||||
|
||||
class BaseViewTests(TestCase):
|
||||
"""Test the base view class of djangorestframework"""
|
||||
urls = 'djangorestframework.tests.views'
|
||||
|
||||
def test_options_method_simple_view(self):
|
||||
response = self.client.options('/mock/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock',
|
||||
description='This is a basic mock view')
|
||||
|
||||
def test_options_method_resource_view(self):
|
||||
response = self.client.options('/resourcemock/')
|
||||
self._verify_options_response(response,
|
||||
name='Resource Mock',
|
||||
description='This is a resource-based mock view',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
def test_options_method_model_resource_list_view(self):
|
||||
response = self.client.options('/model/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock List',
|
||||
description='This is a mock model-based resource',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
class ViewTests(TestCase):
|
||||
def test_options_method_model_resource_detail_view(self):
|
||||
response = self.client.options('/model/0/')
|
||||
self._verify_options_response(response,
|
||||
name='Mock Instance',
|
||||
description='This is a mock model-based resource',
|
||||
fields={'foo':'BooleanField',
|
||||
'bar':'IntegerField',
|
||||
'baz':'CharField',
|
||||
})
|
||||
|
||||
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||
mime_type='application/json'):
|
||||
self.assertEqual(response.status_code, status)
|
||||
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||
parser = JSONParser(None)
|
||||
(data, files) = parser.parse(StringIO(response.content))
|
||||
self.assertTrue('application/json' in data['renders'])
|
||||
self.assertEqual(name, data['name'])
|
||||
self.assertEqual(description, data['description'])
|
||||
if fields is None:
|
||||
self.assertFalse(hasattr(data, 'fields'))
|
||||
else:
|
||||
self.assertEqual(data['fields'], fields)
|
||||
|
||||
|
||||
class ExtraViewsTests(TestCase):
|
||||
"""Test the extra views djangorestframework provides"""
|
||||
urls = 'djangorestframework.tests.views'
|
||||
|
||||
|
@ -39,5 +131,5 @@ class ViewTests(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||
|
||||
|
||||
# TODO: Add login/logout behaviour tests
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ be subclassing in your implementation.
|
|||
By setting or modifying class attributes on your view, you change it's predefined behaviour.
|
||||
"""
|
||||
|
||||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.core.urlresolvers import set_script_prefix, get_script_prefix
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
@ -13,6 +13,7 @@ from djangorestframework.compat import View as DjangoView
|
|||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.mixins import *
|
||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
||||
from djangorestframework.utils.description import get_name, get_description
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -41,7 +42,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
List of renderers the resource can serialize the response with, ordered by preference.
|
||||
"""
|
||||
renderers = renderers.DEFAULT_RENDERERS
|
||||
|
||||
|
||||
"""
|
||||
List of parsers the resource can parse the request with.
|
||||
"""
|
||||
|
@ -52,19 +53,19 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
"""
|
||||
authentication = ( authentication.UserLoggedInAuthentication,
|
||||
authentication.BasicAuthentication )
|
||||
|
||||
|
||||
"""
|
||||
List of all permissions that must be checked.
|
||||
"""
|
||||
permissions = ( permissions.FullAnonAccess, )
|
||||
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
"""
|
||||
Override the default :meth:`as_view` to store an instance of the view
|
||||
as an attribute on the callable function. This allows us to discover
|
||||
information about the view when we do URL reverse lookups.
|
||||
information about the view when we do URL reverse lookups.
|
||||
"""
|
||||
view = super(View, cls).as_view(**initkwargs)
|
||||
view.cls_instance = cls(**initkwargs)
|
||||
|
@ -81,7 +82,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
||||
Return an HTTP 405 error if an operation is called which does not have a handler method.
|
||||
"""
|
||||
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||
|
@ -98,7 +99,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
|
||||
def add_header(self, field, value):
|
||||
"""
|
||||
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
|
||||
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
|
||||
"""
|
||||
self.headers[field] = value
|
||||
|
||||
|
@ -113,12 +114,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
self.headers = {}
|
||||
|
||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||
orig_prefix = get_script_prefix()
|
||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||
set_script_prefix(prefix)
|
||||
set_script_prefix(prefix + orig_prefix)
|
||||
|
||||
try:
|
||||
self.initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
# Authenticate and check request has the relevant permissions
|
||||
self._check_permissions()
|
||||
|
||||
|
@ -140,23 +142,45 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
|
|||
else:
|
||||
response = Response(status.HTTP_204_NO_CONTENT)
|
||||
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.filter_response(response.raw_content)
|
||||
|
||||
if request.method == 'OPTIONS':
|
||||
# do not filter the response for HTTP OPTIONS, else the response fields are lost,
|
||||
# as they do not correspond with model fields
|
||||
response.cleaned_content = response.raw_content
|
||||
else:
|
||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||
response.cleaned_content = self.filter_response(response.raw_content)
|
||||
|
||||
except ErrorResponse, exc:
|
||||
response = exc.response
|
||||
|
||||
|
||||
# Always add these headers.
|
||||
#
|
||||
# TODO - this isn't actually the correct way to set the vary header,
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||
response.headers['Vary'] = 'Authenticate, Accept'
|
||||
|
||||
|
||||
# merge with headers possibly set at some point in the view
|
||||
response.headers.update(self.headers)
|
||||
|
||||
return self.render(response)
|
||||
|
||||
set_script_prefix(orig_prefix)
|
||||
|
||||
return self.render(response)
|
||||
|
||||
def options(self, request, *args, **kwargs):
|
||||
response_obj = {
|
||||
'name': get_name(self),
|
||||
'description': get_description(self),
|
||||
'renders': self._rendered_media_types,
|
||||
'parses': self._parsed_media_types,
|
||||
}
|
||||
form = self.get_bound_form()
|
||||
if form is not None:
|
||||
field_name_types = {}
|
||||
for name, field in form.fields.iteritems():
|
||||
field_name_types[name] = field.__class__.__name__
|
||||
response_obj['fields'] = field_name_types
|
||||
return response_obj
|
||||
|
||||
|
||||
class ModelView(View):
|
||||
|
@ -174,11 +198,11 @@ class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteM
|
|||
class ListModelView(ListModelMixin, ModelView):
|
||||
"""
|
||||
A view which provides default operations for list, against a model in the database.
|
||||
"""
|
||||
"""
|
||||
_suffix = 'List'
|
||||
|
||||
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
||||
"""
|
||||
A view which provides default operations for list and create, against a model in the database.
|
||||
"""
|
||||
"""
|
||||
_suffix = 'List'
|
||||
|
|
|
@ -11,7 +11,7 @@ Introduction
|
|||
|
||||
Django REST framework is a lightweight REST framework for Django, that aims to make it easy to build well-connected, self-describing RESTful Web APIs.
|
||||
|
||||
**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_
|
||||
**Browse example APIs created with Django REST framework:** `The Sandbox <http://api.django-rest-framework.org/>`_
|
||||
|
||||
Features:
|
||||
|
||||
|
@ -26,10 +26,10 @@ Features:
|
|||
Resources
|
||||
---------
|
||||
|
||||
**Project hosting:** `Bitbucket <https://bitbucket.org/tomchristie/django-rest-framework>`_ and `GitHub <https://github.com/tomchristie/django-rest-framework>`_.
|
||||
**Project hosting:** `GitHub <https://github.com/tomchristie/django-rest-framework>`_.
|
||||
|
||||
* The ``djangorestframework`` package is `available on PyPI <http://pypi.python.org/pypi/djangorestframework>`_.
|
||||
* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_ and a `project blog <http://blog.django-rest-framework.org>`_.
|
||||
* We have an active `discussion group <http://groups.google.com/group/django-rest-framework>`_.
|
||||
* Bug reports are handled on the `issue tracker <https://github.com/tomchristie/django-rest-framework/issues>`_.
|
||||
* There is a `Jenkins CI server <http://jenkins.tibold.nl/job/djangorestframework/>`_ which tracks test status and coverage reporting. (Thanks Marko!)
|
||||
|
||||
|
@ -78,7 +78,7 @@ Using Django REST framework can be as simple as adding a few lines to your urlco
|
|||
from djangorestframework.resources import ModelResource
|
||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||
from myapp.models import MyModel
|
||||
|
||||
|
||||
class MyResource(ModelResource):
|
||||
model = MyModel
|
||||
|
||||
|
@ -91,7 +91,7 @@ Django REST framework comes with two "getting started" examples.
|
|||
|
||||
#. :ref:`views`
|
||||
#. :ref:`modelviews`
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
|
@ -143,7 +143,7 @@ Examples Reference
|
|||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
|
||||
examples/views
|
||||
examples/modelviews
|
||||
examples/objectstore
|
||||
|
|
Loading…
Reference in New Issue
Block a user