fix merge

This commit is contained in:
Craig Blaszczyk 2011-12-11 18:30:43 +00:00
commit e84bf2140c
11 changed files with 478 additions and 144 deletions

View File

@ -17,6 +17,8 @@ Garcia Solero <garciasolero>
Tom Drummond <devioustree>
Danilo Bargen <gwrtheyrn>
Andrew McCloud <amccloud>
Thomas Steinacher <thomasst>
Meurig Freeman <meurig>
THANKS TO:

View File

@ -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

View File

@ -4,20 +4,17 @@ 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
@ -451,7 +448,10 @@ class ResourceMixin(object):
return self._resource.filter_response(obj)
def get_bound_form(self, content=None, method=None):
if hasattr(self._resource, 'get_bound_form'):
return self._resource.get_bound_form(content, method=method)
else:
return None
@ -565,7 +565,7 @@ class UpdateModelMixin(object):
# 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

View File

@ -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

View File

@ -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

View File

@ -114,21 +114,21 @@ class TestContentParsing(TestCase):
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
# 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'
# data = {'qwerty': 'uiop'}
# content = json.dumps(data)
# content_type = 'application/json'
view = RequestMixin()
view.parsers = (JSONParser,)
# view = RequestMixin()
# 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.request.POST.items(), [])
# 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"""
@ -219,14 +219,14 @@ class TestContentParsingWithAuthentication(TestCase):
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'}
# 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.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")
# response = self.csrf_client.post('/', content)
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")

View File

@ -1,11 +1,14 @@
"""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):
@ -30,7 +33,6 @@ class TestModelCreation(TestCase):
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
@ -41,7 +43,11 @@ class TestModelCreation(TestCase):
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]
@ -92,7 +98,6 @@ class TestModelCreation(TestCase):
self.assertEquals(1, response.cleaned_content.groups.count())
self.assertEquals('foo1', response.cleaned_content.groups.all()[0].name)
group2 = Group(name='foo2')
group2.save()
@ -111,3 +116,122 @@ class TestModelCreation(TestCase):
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)

View File

@ -229,6 +229,7 @@ if YAMLRenderer:
self.assertEquals(obj, data)
class XMLRendererTestCase(TestCase):
"""
Tests specific to the JSON Renderer

View File

@ -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'
class ViewTests(TestCase):
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 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

View File

@ -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__ = (
@ -113,8 +114,9 @@ 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)
@ -140,6 +142,11 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
else:
response = Response(status.HTTP_204_NO_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)
@ -156,8 +163,25 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
# merge with headers possibly set at some point in the view
response.headers.update(self.headers)
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):
"""

View File

@ -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!)