Commit e84bf214 by Craig Blaszczyk

fix merge

parents 0632e946 4f423030
...@@ -12,11 +12,13 @@ Andrew Straw <astraw> ...@@ -12,11 +12,13 @@ Andrew Straw <astraw>
Zeth <zeth> Zeth <zeth>
Fernando Zunino <fzunino> Fernando Zunino <fzunino>
Jens Alm <ulmus> Jens Alm <ulmus>
Craig Blaszczyk <jakul> Craig Blaszczyk <jakul>
Garcia Solero <garciasolero> Garcia Solero <garciasolero>
Tom Drummond <devioustree> Tom Drummond <devioustree>
Danilo Bargen <gwrtheyrn> Danilo Bargen <gwrtheyrn>
Andrew McCloud <amccloud> Andrew McCloud <amccloud>
Thomas Steinacher <thomasst>
Meurig Freeman <meurig>
THANKS TO: THANKS TO:
......
""" """
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 # cStringIO only if it's available
......
""" """
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`. classes that can be added to a `View`.
""" """
from django.contrib.auth.models import AnonymousUser 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.db.models.fields.related import ForeignKey
from django.http import HttpResponse from django.http import HttpResponse
from djangorestframework import status from djangorestframework import status
from djangorestframework.parsers import FormParser, MultiPartParser
from djangorestframework.renderers import BaseRenderer from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource from djangorestframework.resources import Resource, FormResource, ModelResource
from djangorestframework.response import Response, ErrorResponse from djangorestframework.response import Response, ErrorResponse
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
from decimal import Decimal
import re
from StringIO import StringIO from StringIO import StringIO
...@@ -52,7 +49,7 @@ class RequestMixin(object): ...@@ -52,7 +49,7 @@ class RequestMixin(object):
""" """
The set of request parsers that the view can handle. The set of request parsers that the view can handle.
Should be a tuple/list of classes as described in the :mod:`parsers` module. Should be a tuple/list of classes as described in the :mod:`parsers` module.
""" """
parsers = () parsers = ()
...@@ -158,7 +155,7 @@ class RequestMixin(object): ...@@ -158,7 +155,7 @@ class RequestMixin(object):
# We only need to use form overloading on form POST requests. # 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): if not self._USE_FORM_OVERLOADING or self._method != 'POST' or not is_form_media_type(self._content_type):
return return
# At this point we're committed to parsing the request as form data. # At this point we're committed to parsing the request as form data.
self._data = data = self.request.POST.copy() self._data = data = self.request.POST.copy()
self._files = self.request.FILES self._files = self.request.FILES
...@@ -203,12 +200,12 @@ class RequestMixin(object): ...@@ -203,12 +200,12 @@ class RequestMixin(object):
""" """
return [parser.media_type for parser in self.parsers] return [parser.media_type for parser in self.parsers]
@property @property
def _default_parser(self): def _default_parser(self):
""" """
Return the view's default parser class. Return the view's default parser class.
""" """
return self.parsers[0] return self.parsers[0]
...@@ -218,7 +215,7 @@ class RequestMixin(object): ...@@ -218,7 +215,7 @@ class RequestMixin(object):
class ResponseMixin(object): class ResponseMixin(object):
""" """
Adds behavior for pluggable `Renderers` to a :class:`views.View` class. Adds behavior for pluggable `Renderers` to a :class:`views.View` class.
Default behavior is to use standard HTTP Accept header content negotiation. 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. 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. Ignores Accept headers from Internet Explorer user agents and uses a sensible browser Accept header instead.
...@@ -229,8 +226,8 @@ class ResponseMixin(object): ...@@ -229,8 +226,8 @@ class ResponseMixin(object):
""" """
The set of response renderers that the view can handle. 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 = () renderers = ()
...@@ -253,7 +250,7 @@ class ResponseMixin(object): ...@@ -253,7 +250,7 @@ class ResponseMixin(object):
# Set the media type of the response # Set the media type of the response
# Note that the renderer *could* override it in .render() if required. # Note that the renderer *could* override it in .render() if required.
response.media_type = renderer.media_type response.media_type = renderer.media_type
# Serialize the response content # Serialize the response content
if response.has_content_body: if response.has_content_body:
content = renderer.render(response.cleaned_content, media_type) content = renderer.render(response.cleaned_content, media_type)
...@@ -317,7 +314,7 @@ class ResponseMixin(object): ...@@ -317,7 +314,7 @@ class ResponseMixin(object):
Return an list of all the media types that this view can render. Return an list of all the media types that this view can render.
""" """
return [renderer.media_type for renderer in self.renderers] return [renderer.media_type for renderer in self.renderers]
@property @property
def _rendered_formats(self): def _rendered_formats(self):
""" """
...@@ -339,18 +336,18 @@ class AuthMixin(object): ...@@ -339,18 +336,18 @@ class AuthMixin(object):
""" """
Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class. 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. 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 = () authentication = ()
""" """
The set of permissions that will be enforced on this view. 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 = () permissions = ()
...@@ -359,7 +356,7 @@ class AuthMixin(object): ...@@ -359,7 +356,7 @@ class AuthMixin(object):
def user(self): def user(self):
""" """
Returns the :obj:`user` for the current request, as determined by the set of 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'): if not hasattr(self, '_user'):
self._user = self._authenticate() self._user = self._authenticate()
...@@ -451,7 +448,10 @@ class ResourceMixin(object): ...@@ -451,7 +448,10 @@ class ResourceMixin(object):
return self._resource.filter_response(obj) return self._resource.filter_response(obj)
def get_bound_form(self, content=None, method=None): 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): ...@@ -538,13 +538,13 @@ class CreateModelMixin(object):
for fieldname in m2m_data: for fieldname in m2m_data:
manager = getattr(instance, fieldname) manager = getattr(instance, fieldname)
if hasattr(manager, 'add'): if hasattr(manager, 'add'):
manager.add(*m2m_data[fieldname][1]) manager.add(*m2m_data[fieldname][1])
else: else:
data = {} data = {}
data[manager.source_field_name] = instance data[manager.source_field_name] = instance
for related_item in m2m_data[fieldname][1]: for related_item in m2m_data[fieldname][1]:
data[m2m_data[fieldname][0]] = related_item data[m2m_data[fieldname][0]] = related_item
manager.through(**data).save() manager.through(**data).save()
...@@ -561,11 +561,11 @@ class UpdateModelMixin(object): ...@@ -561,11 +561,11 @@ class UpdateModelMixin(object):
""" """
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
model = self.resource.model 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: try:
if args: 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) self.model_instance = model.objects.get(pk=args[-1], **kwargs)
else: else:
# Otherwise assume the kwargs uniquely identify the model # Otherwise assume the kwargs uniquely identify the model
...@@ -637,3 +637,93 @@ class ListModelMixin(object): ...@@ -637,3 +637,93 @@ class ListModelMixin(object):
return queryset.filter(**kwargs) 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
""" """
The :mod:`permissions` module bundles a set of permission classes that are used 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 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. class to your view by setting your View's :attr:`permissions` class attribute.
""" """
...@@ -40,7 +40,7 @@ class BasePermission(object): ...@@ -40,7 +40,7 @@ class BasePermission(object):
Permission classes are always passed the current view on creation. Permission classes are always passed the current view on creation.
""" """
self.view = view self.view = view
def check_permission(self, auth): def check_permission(self, auth):
""" """
Should simply return, or raise an :exc:`response.ErrorResponse`. Should simply return, or raise an :exc:`response.ErrorResponse`.
...@@ -64,7 +64,7 @@ class IsAuthenticated(BasePermission): ...@@ -64,7 +64,7 @@ class IsAuthenticated(BasePermission):
def check_permission(self, user): def check_permission(self, user):
if not user.is_authenticated(): if not user.is_authenticated():
raise _403_FORBIDDEN_RESPONSE raise _403_FORBIDDEN_RESPONSE
class IsAdminUser(BasePermission): class IsAdminUser(BasePermission):
...@@ -82,7 +82,7 @@ class IsUserOrIsAnonReadOnly(BasePermission): ...@@ -82,7 +82,7 @@ class IsUserOrIsAnonReadOnly(BasePermission):
The request is authenticated as a user, or is a read-only request. 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 if (not user.is_authenticated() and
self.view.method != 'GET' and self.view.method != 'GET' and
self.view.method != 'HEAD'): self.view.method != 'HEAD'):
...@@ -100,7 +100,7 @@ class BaseThrottle(BasePermission): ...@@ -100,7 +100,7 @@ class BaseThrottle(BasePermission):
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
Previous request information used for throttling is stored in the cache. Previous request information used for throttling is stored in the cache.
""" """
attr_name = 'throttle' attr_name = 'throttle'
default = '0/sec' default = '0/sec'
...@@ -109,7 +109,7 @@ class BaseThrottle(BasePermission): ...@@ -109,7 +109,7 @@ class BaseThrottle(BasePermission):
def get_cache_key(self): def get_cache_key(self):
""" """
Should return a unique cache-key which can be used for throttling. Should return a unique cache-key which can be used for throttling.
Muse be overridden. Must be overridden.
""" """
pass pass
...@@ -123,7 +123,7 @@ class BaseThrottle(BasePermission): ...@@ -123,7 +123,7 @@ class BaseThrottle(BasePermission):
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
self.auth = auth self.auth = auth
self.check_throttle() self.check_throttle()
def check_throttle(self): def check_throttle(self):
""" """
Implement the check to see if the request should be throttled. Implement the check to see if the request should be throttled.
...@@ -134,7 +134,7 @@ class BaseThrottle(BasePermission): ...@@ -134,7 +134,7 @@ class BaseThrottle(BasePermission):
self.key = self.get_cache_key() self.key = self.get_cache_key()
self.history = cache.get(self.key, []) self.history = cache.get(self.key, [])
self.now = self.timer() self.now = self.timer()
# Drop any requests from the history which have now passed the # Drop any requests from the history which have now passed the
# throttle duration # throttle duration
while self.history and self.history[-1] <= self.now - self.duration: while self.history and self.history[-1] <= self.now - self.duration:
...@@ -153,7 +153,7 @@ class BaseThrottle(BasePermission): ...@@ -153,7 +153,7 @@ class BaseThrottle(BasePermission):
cache.set(self.key, self.history, self.duration) cache.set(self.key, self.history, self.duration)
header = 'status=SUCCESS; next=%s sec' % self.next() header = 'status=SUCCESS; next=%s sec' % self.next()
self.view.add_header('X-Throttle', header) self.view.add_header('X-Throttle', header)
def throttle_failure(self): def throttle_failure(self):
""" """
Called when a request to the API has failed due to throttling. Called when a request to the API has failed due to throttling.
...@@ -162,7 +162,7 @@ class BaseThrottle(BasePermission): ...@@ -162,7 +162,7 @@ class BaseThrottle(BasePermission):
header = 'status=FAILURE; next=%s sec' % self.next() header = 'status=FAILURE; next=%s sec' % self.next()
self.view.add_header('X-Throttle', header) self.view.add_header('X-Throttle', header)
raise _503_SERVICE_UNAVAILABLE raise _503_SERVICE_UNAVAILABLE
def next(self): def next(self):
""" """
Returns the recommended next request time in seconds. Returns the recommended next request time in seconds.
...@@ -205,7 +205,7 @@ class PerViewThrottling(BaseThrottle): ...@@ -205,7 +205,7 @@ class PerViewThrottling(BaseThrottle):
def get_cache_key(self): def get_cache_key(self):
return 'throttle_view_%s' % self.view.__class__.__name__ return 'throttle_view_%s' % self.view.__class__.__name__
class PerResourceThrottling(BaseThrottle): class PerResourceThrottling(BaseThrottle):
""" """
Limits the rate of API calls that may be used against all views on Limits the rate of API calls that may be used against all views on
......
...@@ -106,7 +106,8 @@ class Serializer(object): ...@@ -106,7 +106,8 @@ class Serializer(object):
def __init__(self, depth=None, stack=[], **kwargs): def __init__(self, depth=None, stack=[], **kwargs):
self.depth = depth or self.depth if depth is not None:
self.depth = depth
self.stack = stack self.stack = stack
......
...@@ -18,7 +18,7 @@ class MockView(View): ...@@ -18,7 +18,7 @@ class MockView(View):
def post(self, request): def post(self, request):
if request.POST.get('example') is not None: if request.POST.get('example') is not None:
return Response(status.OK) return Response(status.OK)
return Response(status.INTERNAL_SERVER_ERROR) return Response(status.INTERNAL_SERVER_ERROR)
urlpatterns = patterns('', urlpatterns = patterns('',
...@@ -103,104 +103,104 @@ class TestContentParsing(TestCase): ...@@ -103,104 +103,104 @@ class TestContentParsing(TestCase):
view.request = self.req.post('/', form_data) view.request = self.req.post('/', form_data)
view.parsers = (PlainTextParser,) view.parsers = (PlainTextParser,)
self.assertEqual(view.DATA, content) self.assertEqual(view.DATA, content)
def test_accessing_post_after_data_form(self): def test_accessing_post_after_data_form(self):
"""Ensures request.POST can be accessed after request.DATA in form request""" """Ensures request.POST can be accessed after request.DATA in form request"""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view = RequestMixin() view = RequestMixin()
view.parsers = (FormParser, MultiPartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data) view.request = self.req.post('/', data=form_data)
self.assertEqual(view.DATA.items(), form_data.items()) self.assertEqual(view.DATA.items(), form_data.items())
self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.request.POST.items(), form_data.items())
def test_accessing_post_after_data_for_json(self): # def test_accessing_post_after_data_for_json(self):
"""Ensures request.POST can be accessed after request.DATA in json request""" # """Ensures request.POST can be accessed after request.DATA in json request"""
from django.utils import simplejson as json # from django.utils import simplejson as json
data = {'qwerty': 'uiop'} # data = {'qwerty': 'uiop'}
content = json.dumps(data) # content = json.dumps(data)
content_type = 'application/json' # content_type = 'application/json'
view = RequestMixin() # view = RequestMixin()
view.parsers = (JSONParser,) # view.parsers = (JSONParser,)
view.request = self.req.post('/', content, content_type=content_type) # view.request = self.req.post('/', content, content_type=content_type)
self.assertEqual(view.DATA.items(), data.items()) # self.assertEqual(view.DATA.items(), data.items())
self.assertEqual(view.request.POST.items(), []) # self.assertEqual(view.request.POST.items(), [])
def test_accessing_post_after_data_for_overloaded_json(self): def test_accessing_post_after_data_for_overloaded_json(self):
"""Ensures request.POST can be accessed after request.DATA in overloaded json request""" """Ensures request.POST can be accessed after request.DATA in overloaded json request"""
from django.utils import simplejson as json from django.utils import simplejson as json
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
view = RequestMixin() view = RequestMixin()
view.parsers = (JSONParser,) view.parsers = (JSONParser,)
form_data = {view._CONTENT_PARAM: content, form_data = {view._CONTENT_PARAM: content,
view._CONTENTTYPE_PARAM: content_type} view._CONTENTTYPE_PARAM: content_type}
view.request = self.req.post('/', data=form_data) view.request = self.req.post('/', data=form_data)
self.assertEqual(view.DATA.items(), data.items()) self.assertEqual(view.DATA.items(), data.items())
self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.request.POST.items(), form_data.items())
def test_accessing_data_after_post_form(self): def test_accessing_data_after_post_form(self):
"""Ensures request.DATA can be accessed after request.POST in form request""" """Ensures request.DATA can be accessed after request.POST in form request"""
form_data = {'qwerty': 'uiop'} form_data = {'qwerty': 'uiop'}
view = RequestMixin() view = RequestMixin()
view.parsers = (FormParser, MultiPartParser) view.parsers = (FormParser, MultiPartParser)
view.request = self.req.post('/', data=form_data) view.request = self.req.post('/', data=form_data)
self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.request.POST.items(), form_data.items())
self.assertEqual(view.DATA.items(), form_data.items()) self.assertEqual(view.DATA.items(), form_data.items())
def test_accessing_data_after_post_for_json(self): def test_accessing_data_after_post_for_json(self):
"""Ensures request.DATA can be accessed after request.POST in json request""" """Ensures request.DATA can be accessed after request.POST in json request"""
from django.utils import simplejson as json from django.utils import simplejson as json
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
view = RequestMixin() view = RequestMixin()
view.parsers = (JSONParser,) view.parsers = (JSONParser,)
view.request = self.req.post('/', content, content_type=content_type) view.request = self.req.post('/', content, content_type=content_type)
post_items = view.request.POST.items() post_items = view.request.POST.items()
self.assertEqual(len(post_items), 1) self.assertEqual(len(post_items), 1)
self.assertEqual(len(post_items[0]), 2) self.assertEqual(len(post_items[0]), 2)
self.assertEqual(post_items[0][0], content) self.assertEqual(post_items[0][0], content)
self.assertEqual(view.DATA.items(), data.items()) self.assertEqual(view.DATA.items(), data.items())
def test_accessing_data_after_post_for_overloaded_json(self): def test_accessing_data_after_post_for_overloaded_json(self):
"""Ensures request.DATA can be accessed after request.POST in overloaded json request""" """Ensures request.DATA can be accessed after request.POST in overloaded json request"""
from django.utils import simplejson as json from django.utils import simplejson as json
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
content = json.dumps(data) content = json.dumps(data)
content_type = 'application/json' content_type = 'application/json'
view = RequestMixin() view = RequestMixin()
view.parsers = (JSONParser,) view.parsers = (JSONParser,)
form_data = {view._CONTENT_PARAM: content, form_data = {view._CONTENT_PARAM: content,
view._CONTENTTYPE_PARAM: content_type} view._CONTENTTYPE_PARAM: content_type}
view.request = self.req.post('/', data=form_data) view.request = self.req.post('/', data=form_data)
self.assertEqual(view.request.POST.items(), form_data.items()) self.assertEqual(view.request.POST.items(), form_data.items())
self.assertEqual(view.DATA.items(), data.items()) self.assertEqual(view.DATA.items(), data.items())
class TestContentParsingWithAuthentication(TestCase): class TestContentParsingWithAuthentication(TestCase):
urls = 'djangorestframework.tests.content' urls = 'djangorestframework.tests.content'
def setUp(self): def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True) self.csrf_client = Client(enforce_csrf_checks=True)
self.username = 'john' self.username = 'john'
...@@ -208,25 +208,25 @@ class TestContentParsingWithAuthentication(TestCase): ...@@ -208,25 +208,25 @@ class TestContentParsingWithAuthentication(TestCase):
self.password = 'password' self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password) self.user = User.objects.create_user(self.username, self.email, self.password)
self.req = RequestFactory() self.req = RequestFactory()
def test_user_logged_in_authentication_has_post_when_not_logged_in(self): 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""" """Ensures request.POST exists after UserLoggedInAuthentication when user doesn't log in"""
content = {'example': 'example'} 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) response = self.client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed") self.assertEqual(status.OK, response.status_code, "POST data is malformed")
response = self.csrf_client.post('/', content) response = self.csrf_client.post('/', content)
self.assertEqual(status.OK, response.status_code, "POST data is malformed") 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")
"""Tests for the status module""" """Tests for the mixin module"""
from django.test import TestCase from django.test import TestCase
from django.utils import simplejson as json
from djangorestframework import status from djangorestframework import status
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from django.contrib.auth.models import Group, User 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.resources import ModelResource
from djangorestframework.response import Response
from djangorestframework.tests.models import CustomUser from djangorestframework.tests.models import CustomUser
from djangorestframework.views import View
class TestModelCreation(TestCase): class TestModelCreation(TestCase):
"""Tests on CreateModelMixin""" """Tests on CreateModelMixin"""
def setUp(self): def setUp(self):
...@@ -25,23 +28,26 @@ class TestModelCreation(TestCase): ...@@ -25,23 +28,26 @@ class TestModelCreation(TestCase):
mixin = CreateModelMixin() mixin = CreateModelMixin()
mixin.resource = GroupResource mixin.resource = GroupResource
mixin.CONTENT = form_data mixin.CONTENT = form_data
response = mixin.post(request) response = mixin.post(request)
self.assertEquals(1, Group.objects.count()) self.assertEquals(1, Group.objects.count())
self.assertEquals('foo', response.cleaned_content.name) self.assertEquals('foo', response.cleaned_content.name)
def test_creation_with_m2m_relation(self): def test_creation_with_m2m_relation(self):
class UserResource(ModelResource): class UserResource(ModelResource):
model = User model = User
def url(self, instance): def url(self, instance):
return "/users/%i" % instance.id return "/users/%i" % instance.id
group = Group(name='foo') group = Group(name='foo')
group.save() 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) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [group] cleaned_data['groups'] = [group]
...@@ -53,18 +59,18 @@ class TestModelCreation(TestCase): ...@@ -53,18 +59,18 @@ class TestModelCreation(TestCase):
self.assertEquals(1, User.objects.count()) self.assertEquals(1, User.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count()) self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo', response.cleaned_content.groups.all()[0].name) self.assertEquals('foo', response.cleaned_content.groups.all()[0].name)
def test_creation_with_m2m_relation_through(self): def test_creation_with_m2m_relation_through(self):
""" """
Tests creation where the m2m relation uses a through table Tests creation where the m2m relation uses a through table
""" """
class UserResource(ModelResource): class UserResource(ModelResource):
model = CustomUser model = CustomUser
def url(self, instance): def url(self, instance):
return "/customusers/%i" % instance.id return "/customusers/%i" % instance.id
form_data = {'username': 'bar0', 'groups': []} form_data = {'username': 'bar0', 'groups': []}
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [] cleaned_data['groups'] = []
...@@ -74,12 +80,12 @@ class TestModelCreation(TestCase): ...@@ -74,12 +80,12 @@ class TestModelCreation(TestCase):
response = mixin.post(request) response = mixin.post(request)
self.assertEquals(1, CustomUser.objects.count()) 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 = Group(name='foo1')
group.save() group.save()
form_data = {'username': 'bar1', 'groups': [group.id]} form_data = {'username': 'bar1', 'groups': [group.id]}
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [group] cleaned_data['groups'] = [group]
...@@ -91,12 +97,11 @@ class TestModelCreation(TestCase): ...@@ -91,12 +97,11 @@ class TestModelCreation(TestCase):
self.assertEquals(2, CustomUser.objects.count()) self.assertEquals(2, CustomUser.objects.count())
self.assertEquals(1, response.cleaned_content.groups.count()) self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name) self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
group2 = Group(name='foo2') group2 = Group(name='foo2')
group2.save() group2.save()
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]} form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [group, group2] cleaned_data['groups'] = [group, group2]
...@@ -109,5 +114,124 @@ class TestModelCreation(TestCase): ...@@ -109,5 +114,124 @@ class TestModelCreation(TestCase):
self.assertEquals(2, response.cleaned_content.groups.count()) self.assertEquals(2, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name) self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
self.assertEquals('foo2', response.cleaned_content.groups.all()[1].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: ...@@ -228,6 +228,7 @@ if YAMLRenderer:
(data, files) = parser.parse(StringIO(content)) (data, files) = parser.parse(StringIO(content))
self.assertEquals(obj, data) self.assertEquals(obj, data)
class XMLRendererTestCase(TestCase): class XMLRendererTestCase(TestCase):
""" """
...@@ -288,4 +289,4 @@ class XMLRendererTestCase(TestCase): ...@@ -288,4 +289,4 @@ class XMLRendererTestCase(TestCase):
def assertXMLContains(self, xml, string): def assertXMLContains(self, xml, string):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>')) self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</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))
\ No newline at end of file
from django.conf.urls.defaults import patterns, url from django.conf.urls.defaults import patterns, url
from django.test import TestCase from django.test import TestCase
from django.test import Client 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', urlpatterns = patterns('djangorestframework.utils.staticviews',
url(r'^robots.txt$', 'deny_robots'), url(r'^robots.txt$', 'deny_robots'),
url(r'^favicon.ico$', 'favicon'), url(r'^favicon.ico$', 'favicon'),
url(r'^accounts/login$', 'api_login'), url(r'^accounts/login$', 'api_login'),
url(r'^accounts/logout$', 'api_logout'), 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',
})
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 ViewTests(TestCase):
class ExtraViewsTests(TestCase):
"""Test the extra views djangorestframework provides""" """Test the extra views djangorestframework provides"""
urls = 'djangorestframework.tests.views' urls = 'djangorestframework.tests.views'
...@@ -39,5 +131,5 @@ class ViewTests(TestCase): ...@@ -39,5 +131,5 @@ class ViewTests(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html') self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
# TODO: Add login/logout behaviour tests # TODO: Add login/logout behaviour tests
...@@ -5,7 +5,7 @@ be subclassing in your implementation. ...@@ -5,7 +5,7 @@ be subclassing in your implementation.
By setting or modifying class attributes on your view, you change it's predefined behaviour. 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.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
...@@ -13,6 +13,7 @@ from djangorestframework.compat import View as DjangoView ...@@ -13,6 +13,7 @@ from djangorestframework.compat import View as DjangoView
from djangorestframework.response import Response, ErrorResponse from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import * from djangorestframework.mixins import *
from djangorestframework import resources, renderers, parsers, authentication, permissions, status from djangorestframework import resources, renderers, parsers, authentication, permissions, status
from djangorestframework.utils.description import get_name, get_description
__all__ = ( __all__ = (
...@@ -41,7 +42,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -41,7 +42,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
List of renderers the resource can serialize the response with, ordered by preference. List of renderers the resource can serialize the response with, ordered by preference.
""" """
renderers = renderers.DEFAULT_RENDERERS renderers = renderers.DEFAULT_RENDERERS
""" """
List of parsers the resource can parse the request with. List of parsers the resource can parse the request with.
""" """
...@@ -52,19 +53,19 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -52,19 +53,19 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
authentication = ( authentication.UserLoggedInAuthentication, authentication = ( authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication ) authentication.BasicAuthentication )
""" """
List of all permissions that must be checked. List of all permissions that must be checked.
""" """
permissions = ( permissions.FullAnonAccess, ) permissions = ( permissions.FullAnonAccess, )
@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 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 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 = super(View, cls).as_view(**initkwargs)
view.cls_instance = cls(**initkwargs) view.cls_instance = cls(**initkwargs)
...@@ -81,7 +82,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -81,7 +82,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
def http_method_not_allowed(self, request, *args, **kwargs): 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, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
...@@ -98,7 +99,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -98,7 +99,7 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
def add_header(self, field, value): 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 self.headers[field] = value
...@@ -113,12 +114,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -113,12 +114,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
self.headers = {} self.headers = {}
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here. # 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()) prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix) set_script_prefix(prefix + orig_prefix)
try: try:
self.initial(request, *args, **kwargs) self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions # Authenticate and check request has the relevant permissions
self._check_permissions() self._check_permissions()
...@@ -140,23 +142,45 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): ...@@ -140,23 +142,45 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
else: else:
response = Response(status.HTTP_204_NO_CONTENT) response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types) if request.method == 'OPTIONS':
response.cleaned_content = self.filter_response(response.raw_content) # 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: except ErrorResponse, exc:
response = exc.response response = exc.response
# Always add these headers. # Always add these headers.
# #
# TODO - this isn't actually the correct way to set the vary header, # 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['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept' response.headers['Vary'] = 'Authenticate, Accept'
# merge with headers possibly set at some point in the view # merge with headers possibly set at some point in the view
response.headers.update(self.headers) 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): class ModelView(View):
...@@ -174,11 +198,11 @@ class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteM ...@@ -174,11 +198,11 @@ class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteM
class ListModelView(ListModelMixin, ModelView): class ListModelView(ListModelMixin, ModelView):
""" """
A view which provides default operations for list, against a model in the database. A view which provides default operations for list, against a model in the database.
""" """
_suffix = 'List' _suffix = 'List'
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
""" """
A view which provides default operations for list and create, against a model in the database. A view which provides default operations for list and create, against a model in the database.
""" """
_suffix = 'List' _suffix = 'List'
...@@ -11,7 +11,7 @@ Introduction ...@@ -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. 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: Features:
...@@ -26,10 +26,10 @@ Features: ...@@ -26,10 +26,10 @@ Features:
Resources 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>`_. * 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>`_. * 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!) * 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 ...@@ -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.resources import ModelResource
from djangorestframework.views import ListOrCreateModelView, InstanceModelView from djangorestframework.views import ListOrCreateModelView, InstanceModelView
from myapp.models import MyModel from myapp.models import MyModel
class MyResource(ModelResource): class MyResource(ModelResource):
model = MyModel model = MyModel
...@@ -91,7 +91,7 @@ Django REST framework comes with two "getting started" examples. ...@@ -91,7 +91,7 @@ Django REST framework comes with two "getting started" examples.
#. :ref:`views` #. :ref:`views`
#. :ref:`modelviews` #. :ref:`modelviews`
Examples Examples
-------- --------
...@@ -143,7 +143,7 @@ Examples Reference ...@@ -143,7 +143,7 @@ Examples Reference
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
examples/views examples/views
examples/modelviews examples/modelviews
examples/objectstore examples/objectstore
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment