From 9094f93d188859f5db9198a170bbb65d5b9e9286 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 11:21:50 +0100 Subject: Sanitise JSON error messages --- rest_framework/tests/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 3746d7c8..43365e07 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,3 +1,4 @@ +import copy from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status @@ -27,6 +28,17 @@ def basic_view(request): return {'method': 'PUT', 'data': request.DATA} +def sanitise_json_error(error_dict): + """ + Exact contents of JSON error messages depend on the installed version + of json. + """ + ret = copy.copy(error_dict) + chop = len('JSON parse error - No JSON object could be decoded') + ret['detail'] = ret['detail'][:chop] + return ret + + class ClassBasedViewIntegrationTests(TestCase): def setUp(self): self.view = BasicView.as_view() @@ -38,7 +50,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -53,7 +65,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) class FunctionBasedViewIntegrationTests(TestCase): @@ -67,7 +79,7 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -82,4 +94,4 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) -- cgit v1.2.3 From 7608cf1193fd555f31eb9a3d98c6f258720f8022 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 13 Oct 2012 15:07:43 +0100 Subject: Improve documentation for Requests --- rest_framework/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/request.py b/rest_framework/request.py index 0a57d376..7267b368 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -96,7 +96,7 @@ class Request(object): """ Returns the content type header. - This should be used instead of ``request.META.get('HTTP_CONTENT_TYPE')``, + This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`, as it allows the content type to be overridden by using a hidden form field on a form POST request. """ -- cgit v1.2.3 From 551c86c43a71f7dee5cce68c5142714301f6196f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 14 Oct 2012 22:43:07 +0100 Subject: Documentation for parsers --- rest_framework/parsers.py | 17 ----------------- rest_framework/tests/request.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 18 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 5325a64b..672f6a16 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -98,23 +98,6 @@ class YAMLParser(BaseParser): raise ParseError('YAML parse error - %s' % unicode(exc)) -class PlainTextParser(BaseParser): - """ - Plain text parser. - """ - - media_type = 'text/plain' - - def parse_stream(self, stream, **opts): - """ - Returns a 2-tuple of `(data, files)`. - - `data` will simply be a string representing the body of the request. - `files` will always be `None`. - """ - return stream.read() - - class FormParser(BaseParser): """ Parser for form data. diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 7b24b036..f00ee85f 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -10,9 +10,9 @@ from rest_framework import status from rest_framework.authentication import SessionAuthentication from django.test.client import RequestFactory from rest_framework.parsers import ( + BaseParser, FormParser, MultiPartParser, - PlainTextParser, JSONParser ) from rest_framework.request import Request @@ -24,6 +24,19 @@ from rest_framework.views import APIView factory = RequestFactory() +class PlainTextParser(BaseParser): + media_type = 'text/plain' + + def parse_stream(self, stream, **opts): + """ + Returns a 2-tuple of `(data, files)`. + + `data` will simply be a string representing the body of the request. + `files` will always be `None`. + """ + return stream.read() + + class TestMethodOverloading(TestCase): def test_method(self): """ -- cgit v1.2.3 From 241be38340dcea9a49ce741ba844171ce02db2bd Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 09:14:01 +0200 Subject: Added TextField to recognized fields --- rest_framework/fields.py | 7 +++++++ rest_framework/serializers.py | 1 + 2 files changed, 8 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d..5d13bd55 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -409,6 +409,13 @@ class BooleanField(WritableField): return False raise ValidationError(self.error_messages['invalid'] % value) +class TextField(WritableField): + type_name = 'TextField' + + def from_native(self, value): + if isinstance(value, basestring) or value is None: + return value + return smart_unicode(value) class CharField(WritableField): type_name = 'CharField' diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 06330017..0f19956e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -381,6 +381,7 @@ class ModelSerializer(Serializer): models.DateField: DateField, models.EmailField: EmailField, models.CharField: CharField, + models.TextField: TextField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, } -- cgit v1.2.3 From 36cc56bc9d49832ca990ba8568903966f46a2938 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 10:06:50 +0200 Subject: Added tests for TextField --- rest_framework/serializers.py | 2 +- rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f19956e..5afbced2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -381,7 +381,7 @@ class ModelSerializer(Serializer): models.DateField: DateField, models.EmailField: EmailField, models.CharField: CharField, - models.TextField: TextField, + models.TextField: TextField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, } diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 256987ad..7208d6a5 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,6 +28,26 @@ class CommentSerializer(serializers.Serializer): return instance +class LongText(object): + def __init__(self, content): + self.content = content + + def __eq__(self, other): + return all([getattr(self, attr) == getattr(other, attr) + for attr in ('content',)]) + + +class LongTextSerializer(serializers.Serializer): + content = serializers.TextField() + + def restore_object(self, data, instance=None): + if instance is None: + return LongText(**data) + for key, val in data.items(): + setattr(instance, key, val) + return instance + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -82,6 +102,7 @@ class ValidationTests(TestCase): 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) } + self.long_text = LongText('test test test test') def test_create(self): serializer = CommentSerializer(self.data) @@ -102,6 +123,14 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + def test_update_long_text(self): + data = { + 'content' : 'Lorem ipsum dolor sit amet.' + } + serializer = LongTextSerializer(data, self.long_text) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(data['content'], self.long_text.content) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From de4604be0ab64da2d7da0a7054197278e566ced2 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 11:47:56 +0200 Subject: Support for request-based queryset limits on ListModelMixin ListModelMixin uses the get_queryset from the MultipleObjectMixin. This method can be overridden on the View class to return a different queryset, but get_queryset doesn't accept a request parameter in. This commit adds the limit_list hook to override if you want to limit the queryset based on request-information such as the logged in user. --- rest_framework/mixins.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 29153e18..2834c882 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -32,8 +32,15 @@ class ListModelMixin(object): """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." + def limit_list(self, request, queryset): + """ + Override this method to limit the queryset based on information in the request, such as the logged in user. + Should return the limited queryset, defaults to no limits. + """ + return queryset + def list(self, request, *args, **kwargs): - self.object_list = self.get_queryset() + self.object_list = self.limit_list(request, self.get_queryset()) # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. -- cgit v1.2.3 From afbc9684f2108f6fd0ad4ef0ab4c5d19953c1561 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 11:49:56 +0200 Subject: Revert "Support for request-based queryset limits on ListModelMixin" This reverts commit de4604be0ab64da2d7da0a7054197278e566ced2. --- rest_framework/mixins.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 2834c882..29153e18 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -32,15 +32,8 @@ class ListModelMixin(object): """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." - def limit_list(self, request, queryset): - """ - Override this method to limit the queryset based on information in the request, such as the logged in user. - Should return the limited queryset, defaults to no limits. - """ - return queryset - def list(self, request, *args, **kwargs): - self.object_list = self.limit_list(request, self.get_queryset()) + self.object_list = self.get_queryset() # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. -- cgit v1.2.3 From c94272650915eef368cdc5d157644884c3eecccb Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 13:46:44 +0200 Subject: Added docs, integer fields and refactored models.TextField to use CharField I realized that per the django forms, there is no need for a separate TextField, an unlimited CharField is perfectly good. Also added default field for the different IntegerField types --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5afbced2..13f8cde2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -377,11 +377,14 @@ class ModelSerializer(Serializer): field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, + models.PositiveIntegerField: IntegerField, + models.SmallIntegerField: IntegerField, + models.PositiveSmallIntegerField: IntegerField, models.DateTimeField: DateTimeField, models.DateField: DateField, models.EmailField: EmailField, models.CharField: CharField, - models.TextField: TextField, + models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, } -- cgit v1.2.3 From 9f3ff0105ad3486e6cbb9c284d0c3ecda7b47e96 Mon Sep 17 00:00:00 2001 From: Jens Alm Date: Mon, 15 Oct 2012 14:09:29 +0200 Subject: Removed serializer.TextField and related tests --- rest_framework/fields.py | 7 ------- rest_framework/tests/serializer.py | 29 ----------------------------- 2 files changed, 36 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5d13bd55..bb9a523d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -409,13 +409,6 @@ class BooleanField(WritableField): return False raise ValidationError(self.error_messages['invalid'] % value) -class TextField(WritableField): - type_name = 'TextField' - - def from_native(self, value): - if isinstance(value, basestring) or value is None: - return value - return smart_unicode(value) class CharField(WritableField): type_name = 'CharField' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 7208d6a5..256987ad 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,26 +28,6 @@ class CommentSerializer(serializers.Serializer): return instance -class LongText(object): - def __init__(self, content): - self.content = content - - def __eq__(self, other): - return all([getattr(self, attr) == getattr(other, attr) - for attr in ('content',)]) - - -class LongTextSerializer(serializers.Serializer): - content = serializers.TextField() - - def restore_object(self, data, instance=None): - if instance is None: - return LongText(**data) - for key, val in data.items(): - setattr(instance, key, val) - return instance - - class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -102,7 +82,6 @@ class ValidationTests(TestCase): 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) } - self.long_text = LongText('test test test test') def test_create(self): serializer = CommentSerializer(self.data) @@ -123,14 +102,6 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) - def test_update_long_text(self): - data = { - 'content' : 'Lorem ipsum dolor sit amet.' - } - serializer = LongTextSerializer(data, self.long_text) - self.assertEquals(serializer.is_valid(), True) - self.assertEquals(data['content'], self.long_text.content) - class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 9c1fba3483b7e81da0744464dcf23a5f12711de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 13:27:50 +0100 Subject: Tweak parsers to take parser_context --- rest_framework/authentication.py | 40 +++++++++----------------------------- rest_framework/parsers.py | 21 ++++++++++---------- rest_framework/permissions.py | 15 +++++++------- rest_framework/renderers.py | 4 ++-- rest_framework/request.py | 29 ++++++++++++++++++++++----- rest_framework/resources.py | 1 + rest_framework/settings.py | 2 +- rest_framework/tests/request.py | 2 +- rest_framework/utils/mediatypes.py | 26 ------------------------- rest_framework/views.py | 22 +++++++++++++++------ 10 files changed, 73 insertions(+), 89 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index ee5bd2f2..d7624708 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -1,10 +1,9 @@ """ -The :mod:`authentication` module provides a set of pluggable authentication classes. - -Authentication behavior is provided by mixing the :class:`mixins.RequestMixin` class into a :class:`View` class. +Provides a set of pluggable authentication policies. """ from django.contrib.auth import authenticate +from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError from rest_framework.compat import CsrfViewMiddleware from rest_framework.authtoken.models import Token import base64 @@ -17,25 +16,14 @@ class BaseAuthentication(object): def authenticate(self, request): """ - Authenticate the :obj:`request` and return a :obj:`User` or :const:`None`. [*]_ - - .. [*] The authentication context *will* typically be a :obj:`User`, - but it need not be. It can be any user-like object so long as the - permissions classes (see the :mod:`permissions` module) on the view can - handle the object and use it to determine if the request has the required - permissions or not. - - This can be an important distinction if you're implementing some token - based authentication mechanism, where the authentication context - may be more involved than simply mapping to a :obj:`User`. + Authenticate the request and return a two-tuple of (user, token). """ - return None + raise NotImplementedError(".authenticate() must be overridden.") class BasicAuthentication(BaseAuthentication): """ - Base class for HTTP Basic authentication. - Subclasses should implement `.authenticate_credentials()`. + HTTP Basic authentication against username/password. """ def authenticate(self, request): @@ -43,8 +31,6 @@ class BasicAuthentication(BaseAuthentication): Returns a `User` if a correct username and password have been supplied using HTTP Basic authentication. Otherwise returns `None`. """ - from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError - if 'HTTP_AUTHORIZATION' in request.META: auth = request.META['HTTP_AUTHORIZATION'].split() if len(auth) == 2 and auth[0].lower() == "basic": @@ -54,21 +40,13 @@ class BasicAuthentication(BaseAuthentication): return None try: - userid, password = smart_unicode(auth_parts[0]), smart_unicode(auth_parts[2]) + userid = smart_unicode(auth_parts[0]) + password = smart_unicode(auth_parts[2]) except DjangoUnicodeDecodeError: return None return self.authenticate_credentials(userid, password) - def authenticate_credentials(self, userid, password): - """ - Given the Basic authentication userid and password, authenticate - and return a user instance. - """ - raise NotImplementedError('.authenticate_credentials() must be overridden') - - -class UserBasicAuthentication(BasicAuthentication): def authenticate_credentials(self, userid, password): """ Authenticate the userid and password against username and password. @@ -85,8 +63,8 @@ class SessionAuthentication(BaseAuthentication): def authenticate(self, request): """ - Returns a :obj:`User` if the request session currently has a logged in user. - Otherwise returns :const:`None`. + Returns a `User` if the request session currently has a logged in user. + Otherwise returns `None`. """ # Get the underlying HttpRequest object diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 672f6a16..048b71e1 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -38,7 +38,7 @@ class BaseParser(object): media_type = None - def parse(self, string_or_stream, **opts): + def parse(self, string_or_stream, parser_context=None): """ The main entry point to parsers. This is a light wrapper around `parse_stream`, that instead handles both string and stream objects. @@ -47,9 +47,9 @@ class BaseParser(object): stream = BytesIO(string_or_stream) else: stream = string_or_stream - return self.parse_stream(stream, **opts) + return self.parse_stream(stream, parser_context) - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Given a stream to read from, return the deserialized output. Should return parsed data, or a DataAndFiles object consisting of the @@ -65,7 +65,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -85,7 +85,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -105,7 +105,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -123,15 +123,16 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a DataAndFiles object. `.data` will be a `QueryDict` containing all the form parameters. `.files` will be a `QueryDict` containing all the form files. """ - meta = opts['meta'] - upload_handlers = opts['upload_handlers'] + parser_context = parser_context or {} + meta = parser_context['meta'] + upload_handlers = parser_context['upload_handlers'] try: parser = DjangoMultiPartParser(meta, stream, upload_handlers) data, files = parser.parse() @@ -147,7 +148,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 13ea39ea..6f848cee 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -1,8 +1,5 @@ """ -The :mod:`permissions` module bundles a set of permission classes that are used -for checking if a request passes a certain set of constraints. - -Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` class into a :class:`View` class. +Provides a set of pluggable permission policies. """ @@ -16,7 +13,7 @@ class BasePermission(object): def has_permission(self, request, view, obj=None): """ - Should simply return, or raise an :exc:`response.ImmediateResponse`. + Return `True` if permission is granted, `False` otherwise. """ raise NotImplementedError(".has_permission() must be overridden.") @@ -64,7 +61,8 @@ class DjangoModelPermissions(BasePermission): It ensures that the user is authenticated, and has the appropriate `add`/`change`/`delete` permissions on the model. - This permission should only be used on views with a `ModelResource`. + This permission will only be applied against view classes that + provide a `.model` attribute, such as the generic class-based views. """ # Map methods into required permission codes. @@ -92,7 +90,10 @@ class DjangoModelPermissions(BasePermission): return [perm % kwargs for perm in self.perms_map[method]] def has_permission(self, request, view, obj=None): - model_cls = view.model + model_cls = getattr(view, 'model', None) + if not model_cls: + return True + perms = self.get_required_permissions(request.method, model_cls) if (request.user and diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e5e4134b..2a3b0b6c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -7,6 +7,7 @@ and providing forms and links depending on the allowed methods, renderers and pa """ import string from django import forms +from django.http.multipartparser import parse_header from django.template import RequestContext, loader from django.utils import simplejson as json from rest_framework.compat import yaml @@ -16,7 +17,6 @@ from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.mediatypes import get_media_type_params from rest_framework import VERSION from rest_framework import serializers, parsers @@ -58,7 +58,7 @@ class JSONRenderer(BaseRenderer): if accepted_media_type: # If the media type looks like 'application/json; indent=4', # then pretty print the result. - params = get_media_type_params(accepted_media_type) + base_media_type, params = parse_header(accepted_media_type) indent = params.get('indent', indent) try: indent = max(min(int(indent), 8), 0) diff --git a/rest_framework/request.py b/rest_framework/request.py index 7267b368..6f9cf09a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -11,9 +11,18 @@ The wrapped request then offers a richer API, in particular : """ from StringIO import StringIO +from django.http.multipartparser import parse_header from rest_framework import exceptions from rest_framework.settings import api_settings -from rest_framework.utils.mediatypes import is_form_media_type + + +def is_form_media_type(media_type): + """ + Return True if the media type is a valid form media type. + """ + base_media_type, params = parse_header(media_type) + return base_media_type == 'application/x-www-form-urlencoded' or \ + base_media_type == 'multipart/form-data' class Empty(object): @@ -35,7 +44,8 @@ def clone_request(request, method): """ ret = Request(request._request, request.parsers, - request.authenticators) + request.authenticators, + request.parser_context) ret._data = request._data ret._files = request._files ret._content_type = request._content_type @@ -65,20 +75,30 @@ class Request(object): _CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE def __init__(self, request, parsers=None, authenticators=None, - negotiator=None): + negotiator=None, parser_context=None): self._request = request self.parsers = parsers or () self.authenticators = authenticators or () self.negotiator = negotiator or self._default_negotiator() + self.parser_context = parser_context self._data = Empty self._files = Empty self._method = Empty self._content_type = Empty self._stream = Empty + if self.parser_context is None: + self.parser_context = self._default_parser_context(request) + def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION() + def _default_parser_context(self, request): + return { + 'upload_handlers': request.upload_handlers, + 'meta': request.META, + } + @property def method(self): """ @@ -253,8 +273,7 @@ class Request(object): if not parser: raise exceptions.UnsupportedMediaType(self.content_type) - parsed = parser.parse(self.stream, meta=self.META, - upload_handlers=self.upload_handlers) + parsed = parser.parse(self.stream, self.parser_context) # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. try: diff --git a/rest_framework/resources.py b/rest_framework/resources.py index bb3d581f..dd8a5471 100644 --- a/rest_framework/resources.py +++ b/rest_framework/resources.py @@ -70,6 +70,7 @@ class Resource(ResourceMixin, views.APIView): ##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### class ModelResource(ResourceMixin, views.APIView): + # TODO: Actually delegation won't work root_class = generics.ListCreateAPIView detail_class = generics.RetrieveUpdateDestroyAPIView diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 5ebe7ba5..8bbb2f75 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -35,7 +35,7 @@ DEFAULTS = { ), 'DEFAULT_AUTHENTICATION': ( 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.UserBasicAuthentication' + 'rest_framework.authentication.BasicAuthentication' ), 'DEFAULT_PERMISSIONS': (), 'DEFAULT_THROTTLES': (), diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f00ee85f..f90bebf4 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse_stream(self, stream, **opts): + def parse_stream(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index 5eba7fb2..ee7f3a54 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -25,32 +25,6 @@ def media_type_matches(lhs, rhs): return lhs.match(rhs) -def is_form_media_type(media_type): - """ - Return True if the media type is a valid form media type as defined by the HTML4 spec. - (NB. HTML5 also adds text/plain to the list of valid form media types, but we don't support this here) - """ - media_type = _MediaType(media_type) - return media_type.full_type == 'application/x-www-form-urlencoded' or \ - media_type.full_type == 'multipart/form-data' - - -def add_media_type_param(media_type, key, val): - """ - Add a key, value parameter to a media type string, and return the new media type string. - """ - media_type = _MediaType(media_type) - media_type.params[key] = val - return str(media_type) - - -def get_media_type_params(media_type): - """ - Return a dictionary of the parameters on the given media type. - """ - return _MediaType(media_type).params - - def order_by_precedence(media_type_lst): """ Returns a list of sets of media type strings, ordered by precedence. diff --git a/rest_framework/views.py b/rest_framework/views.py index b3f36085..92d4445f 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -1,8 +1,5 @@ """ -The :mod:`views` module provides the Views you will most probably -be subclassing in your implementation. - -By setting or modifying class attributes on your view, you change it's predefined behaviour. +Provides an APIView class that is used as the base of all class-based views. """ import re @@ -159,9 +156,19 @@ class APIView(View): """ raise exceptions.Throttled(wait) + def get_parser_context(self, request): + """ + Returns a dict that is passed through to Parser.parse_stream(), + as the `parser_context` keyword argument. + """ + return { + 'upload_handlers': request.upload_handlers, + 'meta': request.META, + } + def get_renderer_context(self): """ - Returns a dict that is passed through to the Renderer.render(), + Returns a dict that is passed through to Renderer.render(), as the `renderer_context` keyword argument. """ # Note: Additionally 'response' will also be set on the context, @@ -253,10 +260,13 @@ class APIView(View): """ Returns the initial request object. """ + parser_context = self.get_parser_context(request) + return Request(request, parsers=self.get_parsers(), authenticators=self.get_authenticators(), - negotiator=self.get_content_negotiator()) + negotiator=self.get_content_negotiator(), + parser_context=parser_context) def initial(self, request, *args, **kwargs): """ -- cgit v1.2.3 From 3c8f01b985396c9bfe802f0d1e25bbb59ea2a1a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 14:03:36 +0100 Subject: Explicit CSRF failure message. Fixes #60. --- rest_framework/authentication.py | 22 +++++++++++++++++----- rest_framework/renderers.py | 7 +++++-- rest_framework/views.py | 6 +++--- 3 files changed, 25 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index d7624708..30c78ebc 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -4,6 +4,7 @@ Provides a set of pluggable authentication policies. from django.contrib.auth import authenticate from django.utils.encoding import smart_unicode, DjangoUnicodeDecodeError +from rest_framework import exceptions from rest_framework.compat import CsrfViewMiddleware from rest_framework.authtoken.models import Token import base64 @@ -71,12 +72,23 @@ class SessionAuthentication(BaseAuthentication): http_request = request._request user = getattr(http_request, 'user', None) - if user and user.is_active: - # Enforce CSRF validation for session based authentication. - resp = CsrfViewMiddleware().process_view(http_request, None, (), {}) + # Unauthenticated, CSRF validation not required + if not user or not user.is_active: + return - if resp is None: # csrf passed - return (user, None) + # Enforce CSRF validation for session based authentication. + class CSRFCheck(CsrfViewMiddleware): + def _reject(self, request, reason): + # Return the failure reason instead of an HttpResponse + return reason + + reason = CSRFCheck().process_view(http_request, None, (), {}) + if reason: + # CSRF failed, bail with explicit error message + raise exceptions.PermissionDenied('CSRF Failed: %s' % reason) + + # CSRF passed with authenticated user + return (user, None) class TokenAuthentication(BaseAuthentication): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2a3b0b6c..94d253c9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -235,8 +235,11 @@ class BrowsableAPIRenderer(BaseRenderer): return # Cannot use form overloading request = clone_request(request, method) - if not view.has_permission(request): - return # Don't have permission + try: + if not view.has_permission(request): + return # Don't have permission + except: + return # Don't have permission and exception explicitly raise if method == 'DELETE' or method == 'OPTIONS': return True # Don't actually need to return a form diff --git a/rest_framework/views.py b/rest_framework/views.py index 92d4445f..62fc92f9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -156,14 +156,14 @@ class APIView(View): """ raise exceptions.Throttled(wait) - def get_parser_context(self, request): + def get_parser_context(self, http_request): """ Returns a dict that is passed through to Parser.parse_stream(), as the `parser_context` keyword argument. """ return { - 'upload_handlers': request.upload_handlers, - 'meta': request.META, + 'upload_handlers': http_request.upload_handlers, + 'meta': http_request.META, } def get_renderer_context(self): -- cgit v1.2.3 From 520a183cc68f5dc2b581456f50b32d3f031ff4af Mon Sep 17 00:00:00 2001 From: eofs Date: Wed, 17 Oct 2012 10:41:23 +0300 Subject: Typo in class name --- rest_framework/throttling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 566c277d..6e7a0b72 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -22,7 +22,7 @@ class BaseThrottle(object): return None -class SimpleRateThottle(BaseThrottle): +class SimpleRateThrottle(BaseThrottle): """ A simple cache implementation, that only requires `.get_cache_key()` to be overridden. @@ -133,7 +133,7 @@ class SimpleRateThottle(BaseThrottle): return remaining_duration / float(available_requests) -class AnonRateThrottle(SimpleRateThottle): +class AnonRateThrottle(SimpleRateThrottle): """ Limits the rate of API calls that may be made by a anonymous users. @@ -153,7 +153,7 @@ class AnonRateThrottle(SimpleRateThottle): } -class UserRateThrottle(SimpleRateThottle): +class UserRateThrottle(SimpleRateThrottle): """ Limits the rate of API calls that may be made by a given user. @@ -175,7 +175,7 @@ class UserRateThrottle(SimpleRateThottle): } -class ScopedRateThrottle(SimpleRateThottle): +class ScopedRateThrottle(SimpleRateThrottle): """ Limits the rate of API calls by different amounts for various parts of the API. Any view that has the `throttle_scope` property set will be -- cgit v1.2.3 From 38673c35d4aa5487e175ac7c917c66c45ddb6ba4 Mon Sep 17 00:00:00 2001 From: Rob Dobson Date: Wed, 17 Oct 2012 19:12:34 +0100 Subject: Make default field check safe for boolean values whereby 'False' may be an acceptable default value --- rest_framework/serializers.py | 2 +- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 13f8cde2..6724bbdf 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -393,7 +393,7 @@ class ModelSerializer(Serializer): except KeyError: ret = ModelField(model_field=model_field) - if model_field.default: + if model_field.default is not None: ret.required = False return ret diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6a758f0c..75dab2f7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -91,3 +91,7 @@ class Comment(RESTFrameworkModel): email = models.EmailField() content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + +class ActionItem(RESTFrameworkModel): + title = models.CharField(max_length=200) + done = models.BooleanField(default=False) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 256987ad..610ed85f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -28,6 +28,10 @@ class CommentSerializer(serializers.Serializer): return instance +class ActionItemSerializer(serializers.ModelSerializer): + class Meta: + model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -81,7 +85,9 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } + self.actionitem = ActionItem('Some to do item', + ) def test_create(self): serializer = CommentSerializer(self.data) @@ -102,6 +108,17 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'email': [u'This field is required.']}) + def test_missing_bool_with_default(self): + """Make sure that a boolean value with a 'False' value is not + mistaken for not having a default.""" + data = { + 'title':'Some action item', + #No 'done' value. + } + serializer = ActionItemSerializer(data, instance=self.actionitem) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.errors, {}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 99d48f90030d174ef80498b48f56af6489865f0d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:07:56 +0100 Subject: Drop .parse_string_or_stream() - keep API minimal. --- rest_framework/parsers.py | 28 ++++++++-------------------- rest_framework/tests/request.py | 2 +- rest_framework/views.py | 2 +- 3 files changed, 10 insertions(+), 22 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 048b71e1..6287b842 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -21,7 +21,6 @@ from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError import datetime import decimal -from io import BytesIO class DataAndFiles(object): @@ -33,29 +32,18 @@ class DataAndFiles(object): class BaseParser(object): """ All parsers should extend `BaseParser`, specifying a `media_type` - attribute, and overriding the `.parse_stream()` method. + attribute, and overriding the `.parse()` method. """ media_type = None - def parse(self, string_or_stream, parser_context=None): - """ - The main entry point to parsers. This is a light wrapper around - `parse_stream`, that instead handles both string and stream objects. - """ - if isinstance(string_or_stream, basestring): - stream = BytesIO(string_or_stream) - else: - stream = string_or_stream - return self.parse_stream(stream, parser_context) - - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Given a stream to read from, return the deserialized output. Should return parsed data, or a DataAndFiles object consisting of the parsed data and files. """ - raise NotImplementedError(".parse_stream() must be overridden.") + raise NotImplementedError(".parse() must be overridden.") class JSONParser(BaseParser): @@ -65,7 +53,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -85,7 +73,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -105,7 +93,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -123,7 +111,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a DataAndFiles object. @@ -148,7 +136,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f90bebf4..f698e845 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse_stream(self, stream, parser_context=None): + def parse(self, stream, parser_context=None): """ Returns a 2-tuple of `(data, files)`. diff --git a/rest_framework/views.py b/rest_framework/views.py index 62fc92f9..1be2593c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -158,7 +158,7 @@ class APIView(View): def get_parser_context(self, http_request): """ - Returns a dict that is passed through to Parser.parse_stream(), + Returns a dict that is passed through to Parser.parse(), as the `parser_context` keyword argument. """ return { -- cgit v1.2.3 From 4231995fbd80e45991975ab81d9e570a9f4b72d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:19:59 +0100 Subject: parser_context includes `view`, `request`, `args`, `kwargs`. (Not `meta` and `upload_handlers`) Consistency with renderer API. --- rest_framework/parsers.py | 6 ++++-- rest_framework/request.py | 9 ++------- rest_framework/views.py | 15 +++++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 6287b842..7e13c3d8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -119,8 +119,10 @@ class MultiPartParser(BaseParser): `.files` will be a `QueryDict` containing all the form files. """ parser_context = parser_context or {} - meta = parser_context['meta'] - upload_handlers = parser_context['upload_handlers'] + request = parser_context['request'] + meta = request.META + upload_handlers = request.upload_handlers + try: parser = DjangoMultiPartParser(meta, stream, upload_handlers) data, files = parser.parse() diff --git a/rest_framework/request.py b/rest_framework/request.py index 6f9cf09a..d739d27d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -88,17 +88,12 @@ class Request(object): self._stream = Empty if self.parser_context is None: - self.parser_context = self._default_parser_context(request) + self.parser_context = {} + self.parser_context['request'] = self def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION() - def _default_parser_context(self, request): - return { - 'upload_handlers': request.upload_handlers, - 'meta': request.META, - } - @property def method(self): """ diff --git a/rest_framework/views.py b/rest_framework/views.py index 1be2593c..066c0bb9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -161,9 +161,12 @@ class APIView(View): Returns a dict that is passed through to Parser.parse(), as the `parser_context` keyword argument. """ + # Note: Additionally `request` will also be added to the context + # by the Request object. return { - 'upload_handlers': http_request.upload_handlers, - 'meta': http_request.META, + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}) } def get_renderer_context(self): @@ -171,13 +174,13 @@ class APIView(View): Returns a dict that is passed through to Renderer.render(), as the `renderer_context` keyword argument. """ - # Note: Additionally 'response' will also be set on the context, + # Note: Additionally 'response' will also be added to the context, # by the Response object. return { 'view': self, - 'request': self.request, - 'args': self.args, - 'kwargs': self.kwargs + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}), + 'request': getattr(self, 'request', None) } # API policy instantiation methods -- cgit v1.2.3 From fb56f215ae50da0aebe99e05036ece259fd3e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:39:07 +0100 Subject: Added `media_type` to `.parse()` - Consistency with renderer API. --- rest_framework/parsers.py | 28 +++++++++++----------------- rest_framework/renderers.py | 13 +++++++------ rest_framework/request.py | 12 ++++++++---- rest_framework/tests/request.py | 2 +- 4 files changed, 27 insertions(+), 28 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 7e13c3d8..4841676c 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -1,14 +1,8 @@ """ -Django supports parsing the content of an HTTP request, but only for form POST requests. -That behavior is sufficient for dealing with standard HTML forms, but it doesn't map well -to general HTTP requests. +Parsers are used to parse the content of incoming HTTP requests. -We need a method to be able to: - -1.) Determine the parsed content on a request for methods other than POST (eg typically also PUT) - -2.) Determine the parsed content on a request for media types other than application/x-www-form-urlencoded - and multipart/form-data. (eg also handle multipart/json) +They give us a generic way of being able to handle various media types +on the request, such as form content or json encoded data. """ from django.http import QueryDict @@ -37,10 +31,10 @@ class BaseParser(object): media_type = None - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ - Given a stream to read from, return the deserialized output. - Should return parsed data, or a DataAndFiles object consisting of the + Given a stream to read from, return the parsed representation. + Should return parsed data, or a `DataAndFiles` object consisting of the parsed data and files. """ raise NotImplementedError(".parse() must be overridden.") @@ -53,7 +47,7 @@ class JSONParser(BaseParser): media_type = 'application/json' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -73,7 +67,7 @@ class YAMLParser(BaseParser): media_type = 'application/yaml' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -93,7 +87,7 @@ class FormParser(BaseParser): media_type = 'application/x-www-form-urlencoded' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. @@ -111,7 +105,7 @@ class MultiPartParser(BaseParser): media_type = 'multipart/form-data' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a DataAndFiles object. @@ -138,7 +132,7 @@ class XMLParser(BaseParser): media_type = 'application/xml' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): try: tree = ET.parse(stream) except (ExpatError, ETParseError, ValueError), exc: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 94d253c9..23fd961b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1,9 +1,10 @@ """ -Renderers are used to serialize a View's output into specific media types. +Renderers are used to serialize a response into specific media types. -Django REST framework also provides HTML and PlainText renderers that help self-document the API, -by serializing the output along with documentation regarding the View, output status and headers, -and providing forms and links depending on the allowed methods, renderers and parsers on the View. +They give us a generic way of being able to handle various media types +on the response, such as JSON encoded data or HTML output. + +REST framework also provides an HTML renderer the renders the browseable API. """ import string from django import forms @@ -23,8 +24,8 @@ from rest_framework import serializers, parsers class BaseRenderer(object): """ - All renderers must extend this class, set the :attr:`media_type` attribute, - and override the :meth:`render` method. + All renderers should extend this class, setting the `media_type` + and `format` attributes, and override the `.render()` method. """ media_type = None diff --git a/rest_framework/request.py b/rest_framework/request.py index d739d27d..b9d55de4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -260,15 +260,19 @@ class Request(object): May raise an `UnsupportedMediaType`, or `ParseError` exception. """ - if self.stream is None or self.content_type is None: + stream = self.stream + media_type = self.content_type + + if stream is None or media_type is None: return (None, None) - parser = self.negotiator.select_parser(self.parsers, self.content_type) + parser = self.negotiator.select_parser(self.parsers, media_type) if not parser: - raise exceptions.UnsupportedMediaType(self.content_type) + raise exceptions.UnsupportedMediaType(media_type) + + parsed = parser.parse(stream, media_type, self.parser_context) - parsed = parser.parse(self.stream, self.parser_context) # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. try: diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index f698e845..ff48f3fa 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -27,7 +27,7 @@ factory = RequestFactory() class PlainTextParser(BaseParser): media_type = 'text/plain' - def parse(self, stream, parser_context=None): + def parse(self, stream, media_type=None, parser_context=None): """ Returns a 2-tuple of `(data, files)`. -- cgit v1.2.3 From e126b615420fed12af58675cb4bb52e749b006bd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:58:18 +0100 Subject: Negotiation API finalized. .select_renderers and .select_parsers --- rest_framework/negotiation.py | 33 +++++++++++---------------------- rest_framework/request.py | 2 +- rest_framework/tests/negotiation.py | 10 +++++----- rest_framework/views.py | 8 +++++++- 4 files changed, 24 insertions(+), 29 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 8b22f669..444f8056 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -4,45 +4,34 @@ from rest_framework.utils.mediatypes import order_by_precedence, media_type_matc class BaseContentNegotiation(object): - def negotiate(self, request, renderers, format=None, force=False): - raise NotImplementedError('.negotiate() must be implemented') + def select_parser(self, request, parsers): + raise NotImplementedError('.select_parser() must be implemented') + def select_renderer(self, request, renderers, format_suffix=None): + raise NotImplementedError('.select_renderer() must be implemented') -class DefaultContentNegotiation(object): + +class DefaultContentNegotiation(BaseContentNegotiation): settings = api_settings - def select_parser(self, parsers, media_type): + def select_parser(self, request, parsers): """ Given a list of parsers and a media type, return the appropriate parser to handle the incoming request. """ for parser in parsers: - if media_type_matches(parser.media_type, media_type): + if media_type_matches(parser.media_type, request.content_type): return parser return None - def negotiate(self, request, renderers, format=None, force=False): + def select_renderer(self, request, renderers, format_suffix=None): """ Given a request and a list of renderers, return a two-tuple of: (renderer, media type). - - If force is set, then suppress exceptions, and forcibly return a - fallback renderer and media_type. - """ - try: - return self.unforced_negotiate(request, renderers, format) - except (exceptions.InvalidFormat, exceptions.NotAcceptable): - if force: - return (renderers[0], renderers[0].media_type) - raise - - def unforced_negotiate(self, request, renderers, format=None): - """ - As `.negotiate()`, but does not take the optional `force` agument, - or suppress exceptions. """ # Allow URL style format override. eg. "?format=json - format = format or request.GET.get(self.settings.URL_FORMAT_OVERRIDE) + format_query_param = self.settings.URL_FORMAT_OVERRIDE + format = format_suffix or request.GET.get(format_query_param) if format: renderers = self.filter_renderers(renderers, format) diff --git a/rest_framework/request.py b/rest_framework/request.py index b9d55de4..b212680f 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -266,7 +266,7 @@ class Request(object): if stream is None or media_type is None: return (None, None) - parser = self.negotiator.select_parser(self.parsers, media_type) + parser = self.negotiator.select_parser(self, self.parsers) if not parser: raise exceptions.UnsupportedMediaType(media_type) diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index d8265b43..e06354ea 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -18,20 +18,20 @@ class TestAcceptedMediaType(TestCase): self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.negotiator = DefaultContentNegotiation() - def negotiate(self, request): - return self.negotiator.negotiate(request, self.renderers) + def select_renderer(self, request): + return self.negotiator.select_renderer(request, self.renderers) def test_client_without_accept_use_renderer(self): request = factory.get('/') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_underspecifies_accept_use_renderer(self): request = factory.get('/', HTTP_ACCEPT='*/*') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json') def test_client_overspecifies_accept_use_client(self): request = factory.get('/', HTTP_ACCEPT='application/json; indent=8') - accepted_renderer, accepted_media_type = self.negotiate(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEquals(accepted_media_type, 'application/json; indent=8') diff --git a/rest_framework/views.py b/rest_framework/views.py index 066c0bb9..357d8939 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -238,7 +238,13 @@ class APIView(View): """ renderers = self.get_renderers() conneg = self.get_content_negotiator() - return conneg.negotiate(request, renderers, self.format_kwarg, force) + + try: + return conneg.select_renderer(request, renderers, self.format_kwarg) + except: + if force: + return (renderers[0], renderers[0].media_type) + raise def has_permission(self, request, obj=None): """ -- cgit v1.2.3 From fed235dd0135c3eb98bb218a51f01ace5ddd3782 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 23:09:11 +0100 Subject: Make settings consistent with corrosponding view attributes --- rest_framework/generics.py | 4 ++-- rest_framework/request.py | 2 +- rest_framework/settings.py | 43 +++++++++++++++++++++++-------------------- rest_framework/views.py | 12 ++++++------ 4 files changed, 32 insertions(+), 29 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 59739d01..18c1033d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -15,7 +15,7 @@ class BaseView(views.APIView): Base class for all other generic views. """ serializer_class = None - model_serializer_class = api_settings.MODEL_SERIALIZER + model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS def get_serializer_context(self): """ @@ -56,7 +56,7 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): Base class for generic views onto a queryset. """ - pagination_serializer_class = api_settings.PAGINATION_SERIALIZER + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS paginate_by = api_settings.PAGINATE_BY def get_pagination_serializer_class(self): diff --git a/rest_framework/request.py b/rest_framework/request.py index b212680f..5870be82 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -92,7 +92,7 @@ class Request(object): self.parser_context['request'] = self def _default_negotiator(self): - return api_settings.DEFAULT_CONTENT_NEGOTIATION() + return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() @property def method(self): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8bbb2f75..3c508294 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -3,11 +3,11 @@ Settings for REST framework are all namespaced in the REST_FRAMEWORK setting. For example your project's `settings.py` file might look like this: REST_FRAMEWORK = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.YAMLRenderer', ) - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.YAMLParser', ) @@ -24,30 +24,33 @@ from django.utils import importlib USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) DEFAULTS = { - 'DEFAULT_RENDERERS': ( + 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ), - 'DEFAULT_PARSERS': ( + 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser' ), - 'DEFAULT_AUTHENTICATION': ( + 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication' ), - 'DEFAULT_PERMISSIONS': (), - 'DEFAULT_THROTTLES': (), - 'DEFAULT_CONTENT_NEGOTIATION': + 'DEFAULT_PERMISSION_CLASSES': (), + 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', + + 'DEFAULT_MODEL_SERIALIZER_CLASS': + 'rest_framework.serializers.ModelSerializer', + 'DEFAULT_PAGINATION_SERIALIZER_CLASS': + 'rest_framework.pagination.PaginationSerializer', + 'DEFAULT_THROTTLE_RATES': { 'user': None, 'anon': None, }, - - 'MODEL_SERIALIZER': 'rest_framework.serializers.ModelSerializer', - 'PAGINATION_SERIALIZER': 'rest_framework.pagination.PaginationSerializer', 'PAGINATE_BY': None, 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', @@ -65,14 +68,14 @@ DEFAULTS = { # List of settings that may be in string import notation. IMPORT_STRINGS = ( - 'DEFAULT_RENDERERS', - 'DEFAULT_PARSERS', - 'DEFAULT_AUTHENTICATION', - 'DEFAULT_PERMISSIONS', - 'DEFAULT_THROTTLES', - 'DEFAULT_CONTENT_NEGOTIATION', - 'MODEL_SERIALIZER', - 'PAGINATION_SERIALIZER', + 'DEFAULT_RENDERER_CLASSES', + 'DEFAULT_PARSER_CLASSES', + 'DEFAULT_AUTHENTICATION_CLASSES', + 'DEFAULT_PERMISSION_CLASSES', + 'DEFAULT_THROTTLE_CLASSES', + 'DEFAULT_CONTENT_NEGOTIATION_CLASS', + 'DEFAULT_MODEL_SERIALIZER_CLASS', + 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', ) @@ -111,7 +114,7 @@ class APISettings(object): For example: from rest_framework.settings import api_settings - print api_settings.DEFAULT_RENDERERS + print api_settings.DEFAULT_RENDERER_CLASSES Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. diff --git a/rest_framework/views.py b/rest_framework/views.py index 357d8939..c721be3c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -54,12 +54,12 @@ def _camelcase_to_spaces(content): class APIView(View): settings = api_settings - renderer_classes = api_settings.DEFAULT_RENDERERS - parser_classes = api_settings.DEFAULT_PARSERS - authentication_classes = api_settings.DEFAULT_AUTHENTICATION - throttle_classes = api_settings.DEFAULT_THROTTLES - permission_classes = api_settings.DEFAULT_PERMISSIONS - content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS @classmethod def as_view(cls, **initkwargs): -- cgit v1.2.3 From d1746e2f3c3c2250ffdf7b71f2a77df3edccea61 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 22:02:44 +0100 Subject: Allow callables in dotted notation like Field(source='foo.bar') --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d..663a168d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -70,6 +70,8 @@ class Field(object): value = obj for component in self.source.split('.'): value = getattr(value, component) + if is_simple_callable(value): + value = value() else: value = getattr(obj, field_name) return self.to_native(value) -- cgit v1.2.3 From c341799344ab394322a589ce44328f245910e651 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 22:19:54 +0100 Subject: Apply readonly on RelatedField --- rest_framework/fields.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 663a168d..162eed26 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -224,6 +224,9 @@ class RelatedField(WritableField): return self.to_native(value) def field_from_native(self, data, field_name, into): + if self.readonly: + return + value = data.get(field_name) into[(self.source or field_name) + '_id'] = self.from_native(value) -- cgit v1.2.3 From d70e387f106c269d5d8c447c77ba26bdb1aafc8f Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Thu, 18 Oct 2012 23:45:16 +0100 Subject: Added tests for dotted notation in serializer field source --- rest_framework/tests/serializer.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 610ed85f..bd1f07da 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,6 +4,11 @@ from rest_framework import serializers from rest_framework.tests.models import * +class SubComment(object): + def __init__(self, sub_comment): + self.sub_comment = sub_comment + + class Comment(object): def __init__(self, email, content, created): self.email = email @@ -13,13 +18,18 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) + + def get_sub_comment(self): + sub_comment = SubComment('And Merry Christmas!') + return sub_comment class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() - + sub_comment = serializers.Field(source='get_sub_comment.sub_comment') + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,7 +52,14 @@ class BasicTests(TestCase): self.data = { 'email': 'tom@example.com', 'content': 'Happy new year!', - 'created': datetime.datetime(2012, 1, 1) + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'This wont change' + } + self.expected = { + 'email': 'tom@example.com', + 'content': 'Happy new year!', + 'created': datetime.datetime(2012, 1, 1), + 'sub_comment': 'And Merry Christmas!' } def test_empty(self): @@ -50,14 +67,14 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None + 'created': None, + 'sub_comment': '' } self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) - expected = self.data - self.assertEquals(serializer.data, expected) + serializer = CommentSerializer(instance=self.comment) + self.assertEquals(serializer.data, self.expected) def test_create(self): serializer = CommentSerializer(self.data) @@ -65,6 +82,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertFalse(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') def test_update(self): serializer = CommentSerializer(self.data, instance=self.comment) @@ -72,6 +90,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.object, expected) self.assertTrue(serializer.object is expected) + self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') class ValidationTests(TestCase): -- cgit v1.2.3 From 643d3491a65237fef6932ef8833472c243ad7ee8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 23:48:52 +0100 Subject: First pass at pastebin tutorial --- rest_framework/fields.py | 80 ++++++++++++++++++++++++++++++++++++++++----- rest_framework/renderers.py | 13 ++++++-- 2 files changed, 82 insertions(+), 11 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d..14422b27 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -7,6 +7,7 @@ from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve from django.conf import settings +from django.forms import widgets from django.utils.encoding import is_protected_type, smart_unicode from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse @@ -105,10 +106,14 @@ class WritableField(Field): 'required': _('This field is required.'), 'invalid': _('Invalid value.'), } + widget = widgets.TextInput def __init__(self, source=None, readonly=False, required=None, - validators=[], error_messages=None): + validators=[], error_messages=None, widget=None, + help_text=None, initial=None): + super(WritableField, self).__init__(source=source) + self.readonly = readonly if required is None: self.required = not(readonly) @@ -124,6 +129,15 @@ class WritableField(Field): self.validators = self.default_validators + validators + # These attributes are ony used for HTML forms. + self.initial = initial + self.help_text = help_text and smart_unicode(help_text) or '' + + widget = widget or self.widget + if isinstance(widget, type): + widget = widget() + self.widget = widget + def validate(self, value): if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) @@ -157,9 +171,12 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if self.required: - raise ValidationError(self.error_messages['required']) - return + if getattr(self, 'missing_value', None) is not None: + native = self.missing_value + else: + if self.required: + raise ValidationError(self.error_messages['required']) + return value = self.from_native(native) if self.source == '*': @@ -394,20 +411,19 @@ class HyperlinkedIdentityField(Field): class BooleanField(WritableField): type_name = 'BooleanField' + widget = widgets.CheckboxInput default_error_messages = { 'invalid': _(u"'%s' value must be either True or False."), } + empty = False + missing_value = False # Fill in missing value not supplied by html form def from_native(self, value): - if value in (True, False): - # if value is 1 or 0 than it's equal to True or False, but we want - # to return a true bool for semantic reasons. - return bool(value) if value in ('t', 'True', '1'): return True if value in ('f', 'False', '0'): return False - raise ValidationError(self.error_messages['invalid'] % value) + return bool(value) class CharField(WritableField): @@ -427,6 +443,52 @@ class CharField(WritableField): return smart_unicode(value) +class ChoiceField(WritableField): + type_name = 'ChoiceField' + widget = widgets.Select + default_error_messages = { + 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), + } + + def __init__(self, choices=(), *args, **kwargs): + super(ChoiceField, self).__init__(*args, **kwargs) + self.choices = choices + + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def validate(self, value): + """ + Validates that the input is in self.choices. + """ + super(ChoiceField, self).validate(value) + if value and not self.valid_value(value): + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + + def valid_value(self, value): + """ + Check to see if the provided value is a valid choice. + """ + for k, v in self.choices: + if isinstance(v, (list, tuple)): + # This is an optgroup, so look inside the group for options + for k2, v2 in v: + if value == smart_unicode(k2): + return True + else: + if value == smart_unicode(k): + return True + return False + + class EmailField(CharField): type_name = 'EmailField' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..936bec36 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -279,13 +279,22 @@ class BrowsableAPIRenderer(BaseRenderer): continue kwargs = {} + kwargs['required'] = v.required if getattr(v, 'queryset', None): - kwargs['queryset'] = getattr(v, 'queryset', None) + kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): + kwargs['widget'] = v.widget + if getattr(v, 'initial', None): + kwargs['initial'] = v.initial + if getattr(v, 'help_text', None): + kwargs['help_text'] = v.help_text + kwargs['label'] = k + print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: - fields[k] = forms.CharField() + fields[k] = forms.CharField(**kwargs) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From dab177e29e45b657bb43705979c8e601d5a1b31b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:20:54 +0100 Subject: Drop help_text --- rest_framework/fields.py | 3 +-- rest_framework/renderers.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 14422b27..0990eadc 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -110,7 +110,7 @@ class WritableField(Field): def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - help_text=None, initial=None): + initial=None): super(WritableField, self).__init__(source=source) @@ -131,7 +131,6 @@ class WritableField(Field): # These attributes are ony used for HTML forms. self.initial = initial - self.help_text = help_text and smart_unicode(help_text) or '' widget = widget or self.widget if isinstance(widget, type): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 936bec36..de2276ad 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -286,8 +286,6 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs['widget'] = v.widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial - if getattr(v, 'help_text', None): - kwargs['help_text'] = v.help_text kwargs['label'] = k print kwargs -- cgit v1.2.3 From a7390fe7044c4f10d15fdbe9de9e594d0ffa2e05 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:47:01 +0100 Subject: Fix up widget choices --- rest_framework/renderers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index de2276ad..ba5489bc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,6 +6,7 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer the renders the browseable API. """ +import copy import string from django import forms from django.http.multipartparser import parse_header @@ -283,11 +284,19 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset if getattr(v, 'widget', None): - kwargs['widget'] = v.widget + widget = copy.deepcopy(v.widget) + # If choices have friendly readable names, + # then add in the identities too + if getattr(widget, 'choices', None): + choices = widget.choices + if any([ident != desc for (ident, desc) in choices]): + choices = [(ident, "%s (%s)" % (desc, ident)) + for (ident, desc) in choices] + widget.choices = choices + kwargs['widget'] = widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial kwargs['label'] = k - print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) -- cgit v1.2.3 From efabd2bb1b762fbdee2b48fa3a6ccb8f23c7e8dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 19:59:46 +0100 Subject: docs, docs, docs, docs, docs, docs, docs --- rest_framework/urlpatterns.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 386c78a2..316ccd19 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -2,26 +2,23 @@ from django.conf.urls.defaults import url from rest_framework.settings import api_settings -def format_suffix_patterns(urlpatterns, suffix_required=False, - suffix_kwarg=None, allowed=None): +def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): """ Supplement existing urlpatterns with corrosponding patterns that also include a '.format' suffix. Retains urlpattern ordering. + urlpatterns: + A list of URL patterns. + suffix_required: If `True`, only suffixed URLs will be generated, and non-suffixed URLs will not be used. Defaults to `False`. - suffix_kwarg: - The name of the kwarg that will be passed to the view. - Defaults to 'format'. - allowed: An optional tuple/list of allowed suffixes. eg ['json', 'api'] Defaults to `None`, which allows any suffix. - """ - suffix_kwarg = suffix_kwarg or api_settings.FORMAT_SUFFIX_KWARG + suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG if allowed: if len(allowed) == 1: allowed_pattern = allowed[0] -- cgit v1.2.3 From 71a93930fd4df7a1f5f92c67633b813a26a5e938 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 16:34:07 +0200 Subject: Fixing spelling errors. --- rest_framework/throttling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 6e7a0b72..6860e6b9 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -60,7 +60,7 @@ class SimpleRateThrottle(BaseThrottle): Determine the string representation of the allowed request rate. """ if not getattr(self, 'scope', None): - msg = ("You must set either `.scope` or `.rate` for '%s' thottle" % + msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % self.__class__.__name__) raise exceptions.ConfigurationError(msg) @@ -137,7 +137,7 @@ class AnonRateThrottle(SimpleRateThrottle): """ Limits the rate of API calls that may be made by a anonymous users. - The IP address of the request will be used as the unqiue cache key. + The IP address of the request will be used as the unique cache key. """ scope = 'anon' -- cgit v1.2.3 From 93f1aa4f69df85add114c9730a01b50d013a844a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:41:05 +0100 Subject: Remove `initial` kwarg, add `default`. --- rest_framework/fields.py | 18 +++++++++++------- rest_framework/renderers.py | 8 ++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0990eadc..29940946 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -107,10 +107,11 @@ class WritableField(Field): 'invalid': _('Invalid value.'), } widget = widgets.TextInput + default = None def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - initial=None): + default=None): super(WritableField, self).__init__(source=source) @@ -128,10 +129,9 @@ class WritableField(Field): self.error_messages = messages self.validators = self.default_validators + validators + self.default = default or self.default - # These attributes are ony used for HTML forms. - self.initial = initial - + # Widgets are ony used for HTML forms. widget = widget or self.widget if isinstance(widget, type): widget = widget() @@ -170,8 +170,8 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if getattr(self, 'missing_value', None) is not None: - native = self.missing_value + if self.default is not None: + native = self.default else: if self.required: raise ValidationError(self.error_messages['required']) @@ -415,7 +415,11 @@ class BooleanField(WritableField): 'invalid': _(u"'%s' value must be either True or False."), } empty = False - missing_value = False # Fill in missing value not supplied by html form + + # Note: we set default to `False` in order to fill in missing value not + # supplied by html form. TODO: Fix so that only html form input gets + # this behavior. + default = False def from_native(self, value): if value in ('t', 'True', '1'): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba5489bc..b2dbffd2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -281,8 +281,10 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs = {} kwargs['required'] = v.required + if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): widget = copy.deepcopy(v.widget) # If choices have friendly readable names, @@ -294,8 +296,10 @@ class BrowsableAPIRenderer(BaseRenderer): for (ident, desc) in choices] widget.choices = choices kwargs['widget'] = widget - if getattr(v, 'initial', None): - kwargs['initial'] = v.initial + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + kwargs['label'] = k try: -- cgit v1.2.3 From c30712a5c8e89c7d3e235c72867288a4cb5c8c85 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 22:23:54 +0200 Subject: Remove redundant check if method=='DELETE' --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..ba07f6cd 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = forms.CharField() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + if obj: data = serializer.data form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From ab1a12bfecf49061cb31dff05add26d96078771e Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 23:04:12 +0200 Subject: Refactoring BrowsableAPIRenderer --- rest_framework/renderers.py | 52 +++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 23 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba07f6cd..43175861 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -223,11 +223,9 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def get_form(self, view, method, request): + def show_form_for_method(self, view, method, request): """ - Get a form, possibly bound to either the input or output data. - In the absence on of the Resource having an associated form then - provide a form that can be used to submit arbitrary content. + Returns True if a form should be shown for this method. """ if not method in view.allowed_methods: return # Not a valid method @@ -241,20 +239,9 @@ class BrowsableAPIRenderer(BaseRenderer): return # Don't have permission except: return # Don't have permission and exception explicitly raise + return True - if method == 'DELETE' or method == 'OPTIONS': - return True # Don't actually need to return a form - - if (not getattr(view, 'get_serializer', None) or - not parsers.FormParser in getattr(view, 'parser_classes')): - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) - - ##### - # TODO: This is a little bit of a hack. Actually we'd like to remove - # this and just render serializer fields to html directly. - - # We need to map our Fields to Django's Fields. + def serializer_to_form_fields(self, serializer): field_mapping = { serializers.FloatField: forms.FloatField, serializers.IntegerField: forms.IntegerField, @@ -267,13 +254,7 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField } - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - - serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): if getattr(v, 'readonly', True): continue @@ -286,6 +267,31 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: fields[k] = forms.CharField() + return fields + + def get_form(self, view, method, request): + """ + Get a form, possibly bound to either the input or output data. + In the absence on of the Resource having an associated form then + provide a form that can be used to submit arbitrary content. + """ + if not self.show_form_for_method(view, method, request): + return + + if method == 'DELETE' or method == 'OPTIONS': + return True # Don't actually need to return a form + + if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: + media_types = [parser.media_type for parser in view.parser_classes] + return self.get_generic_content_form(media_types) + + # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python + obj, data = None, None + if getattr(view, 'object', None): + obj = view.object + + serializer = view.get_serializer(instance=obj) + fields = self.serializer_to_form_fields(serializer) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj: -- cgit v1.2.3 From 45d4622f090f8d81a04b4d3e888017419676bbc0 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 22 Oct 2012 15:12:25 +0100 Subject: Fix serialization of reverse relationships --- rest_framework/serializers.py | 23 +++++++++++++---------- rest_framework/tests/models.py | 11 +++++++++++ rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6724bbdf..221cbf2f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -247,6 +247,19 @@ class BaseSerializer(Field): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def field_to_native(self, obj, field_name): + """ + Override default so that we can apply ModelSerializer as a nested + field to relationships. + """ + obj = getattr(obj, self.source or field_name) + + # If the object has an "all" method, assume it's a relationship + if is_simple_callable(getattr(obj, 'all', None)): + return [self.to_native(item) for item in obj.all()] + + return self.to_native(obj) + @property def errors(self): """ @@ -295,16 +308,6 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - def field_to_native(self, obj, field_name): - """ - Override default so that we can apply ModelSerializer as a nested - field to relationships. - """ - obj = getattr(obj, self.source or field_name) - if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'): - return [self.to_native(item) for item in obj.all()] - return self.to_native(obj) - def default_fields(self, serialize, obj=None, data=None, nested=False): """ Return all the fields that should be serialized for the model. diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 75dab2f7..8e721737 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel): content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) done = models.BooleanField(default=False) + + +# Models for reverse relations +class BlogPost(RESTFrameworkModel): + title = models.CharField(max_length=100) + + +class BlogPostComment(RESTFrameworkModel): + text = models.TextField() + blog_post = models.ForeignKey(BlogPost) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd1f07da..2dfc04e1 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(len(self.objects.all()), 1) self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') + + +class ManyRelatedTests(TestCase): + def setUp(self): + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + comments = BlogPostCommentSerializer(source='blogpostcomment_set') + + self.serializer_class = BlogPostSerializer + + def test_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + serializer = self.serializer_class(instance=post) + expected = { + 'title': 'Test blog post', + 'comments': [ + {'text': 'I hate this blog post'}, + {'text': 'I love this blog post'} + ] + } + + self.assertEqual(serializer.data, expected) -- cgit v1.2.3 From c7a0d52fd7e22fbc4a01ff900bd3b2c1215e984d Mon Sep 17 00:00:00 2001 From: Ian Strachan Date: Mon, 22 Oct 2012 22:24:26 +0100 Subject: #314 Fix for manytomany field being required in the payload even though the field is specified as readonly in the serializer --- rest_framework/fields.py | 3 +++ rest_framework/tests/models.py | 5 ++++ rest_framework/tests/serializer.py | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f610d6aa..6ed37823 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -256,6 +256,9 @@ class ManyRelatedMixin(object): return [self.to_native(item) for item in value.all()] def field_from_native(self, data, field_name, into): + if self.readonly: + return + try: # Form data value = data.getlist(self.source or field_name) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 8e721737..97cd0849 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,7 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) + +class ReadOnlyManyToManyModel(RESTFrameworkModel): + text = models.CharField(max_length=100, default='anchor') + rel = models.ManyToManyField(Anchor) + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 2dfc04e1..c614b66a 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -246,6 +246,60 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) + +class ReadOnlyManyToManyTests(TestCase): + def setUp(self): + class ReadOnlyManyToManySerializer(serializers.ModelSerializer): + rel = serializers.ManyRelatedField(readonly=True) + class Meta: + model = ReadOnlyManyToManyModel + + self.serializer_class = ReadOnlyManyToManySerializer + + # An anchor instance to use for the relationship + self.anchor = Anchor() + self.anchor.save() + + # A model instance with a many to many relationship to the anchor + self.instance = ReadOnlyManyToManyModel() + self.instance.save() + self.instance.rel.add(self.anchor) + + # A serialized representation of the model instance + self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} + + + def test_update(self): + """ + Attempt to update an instance of a model with a ManyToMany + relationship. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {'rel': [self.anchor.id, new_anchor.id]} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) + + def test_update_without_relationship(self): + """ + Attempt to update an instance of a model where many to ManyToMany + relationship is not supplied. Not updated due to readonly=True + """ + new_anchor = Anchor() + new_anchor.save() + data = {} + serializer = self.serializer_class(data, instance=self.instance) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + self.assertEquals(len(ReadOnlyManyToManyModel.objects.all()), 1) + self.assertEquals(instance.pk, 1) + # rel is still as original (1 entry) + self.assertEquals(list(instance.rel.all()), [self.anchor]) class DefaultValueTests(TestCase): -- cgit v1.2.3 From 51fae73f3d565e2702c72ff9841cc072d6490804 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 09:28:10 +0100 Subject: Implement per-field validation on Serializers --- rest_framework/serializers.py | 18 ++++++++++++++++++ rest_framework/tests/serializer.py | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 221cbf2f..c9c4faa3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,6 +208,23 @@ class BaseSerializer(Field): return reverted_data + def clean_fields(self, data): + """ + Run clean_ validators on the serializer + """ + fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + + for field_name, field in fields.items(): + try: + clean_method = getattr(self, 'clean_%s' % field_name, None) + if clean_method: + source = field.source or field_name + data = clean_method(data, source) + except ValidationError as err: + self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) + + return data + def restore_object(self, attrs, instance=None): """ Deserialize a dictionary of attributes into an object instance. @@ -241,6 +258,7 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) + attrs = self.clean_fields(attrs) else: self._errors['non_field_errors'] = 'No input provided' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index c614b66a..35908449 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -138,6 +138,31 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.errors, {}) + def test_field_validation(self): + + class CommentSerializerWithFieldValidator(CommentSerializer): + + def clean_content(self, attrs, source): + value = attrs[source] + if "test" not in value: + raise serializers.ValidationError("Test not in value") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'This should not validate' + + serializer = CommentSerializerWithFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From 388a807f64f60d84556288e2ade4f0fe57a8e66b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:27:01 +0100 Subject: Switch from clean_ to validate_, clarify documentation --- rest_framework/serializers.py | 4 ++-- rest_framework/tests/serializer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c9c4faa3..802ca55f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -210,13 +210,13 @@ class BaseSerializer(Field): def clean_fields(self, data): """ - Run clean_ validators on the serializer + Run validate_ methods on the serializer """ fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) for field_name, field in fields.items(): try: - clean_method = getattr(self, 'clean_%s' % field_name, None) + clean_method = getattr(self, 'validate_%s' % field_name, None) if clean_method: source = field.source or field_name data = clean_method(data, source) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 35908449..a32de80d 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -142,7 +142,7 @@ class ValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): - def clean_content(self, attrs, source): + def validate_content(self, attrs, source): value = attrs[source] if "test" not in value: raise serializers.ValidationError("Test not in value") -- cgit v1.2.3 From ac2d39892d6b3fbbe5cd53b9ef83367249ba4880 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:39:17 +0100 Subject: Add cross-field validate method --- rest_framework/serializers.py | 13 +++++++++++++ rest_framework/tests/serializer.py | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 802ca55f..15fe26ee 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -225,6 +225,18 @@ class BaseSerializer(Field): return data + def clean_all(self, attrs): + """ + Run the `validate` method on the serializer, if it exists + """ + try: + validate_method = getattr(self, 'validate', None) + if validate_method: + attrs = validate_method(attrs) + except ValidationError as err: + self._errors['non_field_errors'] = err.messages + return attrs + def restore_object(self, attrs, instance=None): """ Deserialize a dictionary of attributes into an object instance. @@ -259,6 +271,7 @@ class BaseSerializer(Field): if data is not None: attrs = self.restore_fields(data) attrs = self.clean_fields(attrs) + attrs = self.clean_all(attrs) else: self._errors['non_field_errors'] = 'No input provided' diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a32de80d..936f15aa 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -163,6 +163,30 @@ class ValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'content': [u'Test not in value']}) + def test_cross_field_validation(self): + + class CommentSerializerWithCrossFieldValidator(CommentSerializer): + + def validate(self, attrs): + if attrs["email"] not in attrs["content"]: + raise serializers.ValidationError("Email address not in content") + return attrs + + data = { + 'email': 'tom@example.com', + 'content': 'A comment from tom@example.com', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertTrue(serializer.is_valid()) + + data['content'] = 'A comment from foo@bar.com' + + serializer = CommentSerializerWithCrossFieldValidator(data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + class MetadataTests(TestCase): def test_empty(self): -- cgit v1.2.3 From d60d598e0255fb3d55a1213d1025447d83523658 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 11:43:30 +0100 Subject: Clean up internal names and documentation --- rest_framework/serializers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 15fe26ee..2f8108d1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,24 +208,24 @@ class BaseSerializer(Field): return reverted_data - def clean_fields(self, data): + def validate_fields(self, attrs): """ Run validate_ methods on the serializer """ - fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) for field_name, field in fields.items(): try: clean_method = getattr(self, 'validate_%s' % field_name, None) if clean_method: source = field.source or field_name - data = clean_method(data, source) + attrs = clean_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) - return data + return attrs - def clean_all(self, attrs): + def validate_all(self, attrs): """ Run the `validate` method on the serializer, if it exists """ @@ -270,10 +270,10 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) - attrs = self.clean_fields(attrs) - attrs = self.clean_all(attrs) + attrs = self.validate_fields(attrs) + attrs = self.validate_all(attrs) else: - self._errors['non_field_errors'] = 'No input provided' + self._errors['non_field_errors'] = ['No input provided'] if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) -- cgit v1.2.3 From 607c31c6d880501e5dc524fc5a5e1fc136b162fc Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 12:12:27 +0100 Subject: Move per-field and cross-field validation into a single method --- rest_framework/serializers.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2f8108d1..c9f025bc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -208,33 +208,32 @@ class BaseSerializer(Field): return reverted_data - def validate_fields(self, attrs): + def perform_validation(self, attrs): """ - Run validate_ methods on the serializer + Run `validate_()` and `validate()` methods on the serializer """ fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) for field_name, field in fields.items(): try: - clean_method = getattr(self, 'validate_%s' % field_name, None) - if clean_method: + validate_method = getattr(self, 'validate_%s' % field_name, None) + if validate_method: source = field.source or field_name - attrs = clean_method(attrs, source) + attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) + try: + attrs = self.validate(attrs) + except ValidationError as err: + self._errors['non_field_errors'] = err.messages + return attrs - def validate_all(self, attrs): + def validate(self, attrs): """ - Run the `validate` method on the serializer, if it exists + Stub method, to be overridden in Serializer subclasses """ - try: - validate_method = getattr(self, 'validate', None) - if validate_method: - attrs = validate_method(attrs) - except ValidationError as err: - self._errors['non_field_errors'] = err.messages return attrs def restore_object(self, attrs, instance=None): @@ -270,8 +269,7 @@ class BaseSerializer(Field): self._errors = {} if data is not None: attrs = self.restore_fields(data) - attrs = self.validate_fields(attrs) - attrs = self.validate_all(attrs) + attrs = self.perform_validation(attrs) else: self._errors['non_field_errors'] = ['No input provided'] -- cgit v1.2.3 From 32ebf96ef661533a9bb69124ec9cef4af2393014 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 24 Oct 2012 18:22:29 +0100 Subject: Split concrete generic views up into separate bits of functionality --- rest_framework/generics.py | 68 ++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 29 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 18c1033d..cfb3f29e 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -95,27 +95,25 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with a base view. ### -class ListAPIView(mixins.ListModelMixin, - MultipleObjectBaseView): + +class CreateAPIView(mixins.CreateModelMixin, + BaseView): + """ - Concrete view for listing a queryset. + Concrete view for creating a model instance. """ - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) -class ListCreateAPIView(mixins.ListModelMixin, - mixins.CreateModelMixin, - MultipleObjectBaseView): +class ListAPIView(mixins.ListModelMixin, + MultipleObjectBaseView): """ - Concrete view for listing a queryset or creating a model instance. + Concrete view for listing a queryset. """ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) - class RetrieveAPIView(mixins.RetrieveModelMixin, SingleObjectBaseView): @@ -126,31 +124,43 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, return self.retrieve(request, *args, **kwargs) -class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - SingleObjectBaseView): +class DestroyAPIView(mixins.DestroyModelMixin, + SingleObjectBaseView): + """ - Concrete view for retrieving or deleting a model instance. + Concrete view for deleting a model instance. """ - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) -class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - SingleObjectBaseView): +class UpdateAPIView(mixins.UpdateModelMixin, + SingleObjectBaseView): + """ - Concrete view for retrieving, updating or deleting a model instance. + Concrete view for updating a model instance. """ - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) + +class ListCreateAPIView(ListAPIView, + CreateAPIView): + """ + Concrete view for listing a queryset or creating a model instance. + """ + + +class RetrieveDestroyAPIView(RetrieveAPIView, + DestroyAPIView): + """ + Concrete view for retrieving or deleting a model instance. + """ + + +class RetrieveUpdateDestroyAPIView(RetrieveAPIView, + UpdateAPIView, + DestroyAPIView): + """ + Concrete view for retrieving, updating or deleting a model instance. + """ -- cgit v1.2.3 From 3e751ccd8aa2870c125a17de6af6e1909aa2b35e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 24 Oct 2012 20:58:10 +0100 Subject: Fix ModelSerializer logic for fields with default value, which should have required=False set --- rest_framework/serializers.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c9f025bc..8ee9a0ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -406,6 +406,10 @@ class ModelSerializer(Serializer): """ Creates a default instance of a basic non-relational field. """ + kwargs = {} + if model_field.has_default(): + kwargs['required'] = False + field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, @@ -421,14 +425,9 @@ class ModelSerializer(Serializer): models.BooleanField: BooleanField, } try: - ret = field_mapping[model_field.__class__]() + return field_mapping[model_field.__class__](**kwargs) except KeyError: - ret = ModelField(model_field=model_field) - - if model_field.default is not None: - ret.required = False - - return ret + return ModelField(model_field=model_field, **kwargs) def restore_object(self, attrs, instance=None): """ -- cgit v1.2.3 From 8c360770c18ac38a2f4da81a3553fb40592558c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Oct 2012 12:15:31 +0100 Subject: Add pre_save hook in generic views --- rest_framework/mixins.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 29153e18..8873e4ae 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -20,10 +20,14 @@ class CreateModelMixin(object): def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.DATA) if serializer.is_valid(): + self.pre_save(serializer.object) self.object = serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def pre_save(self, obj): + pass + class ListModelMixin(object): """ @@ -46,7 +50,8 @@ class ListModelMixin(object): # which may be `None` to disable pagination. page_size = self.get_paginate_by(self.object_list) if page_size: - paginator, page, queryset, is_paginated = self.paginate_queryset(self.object_list, page_size) + packed = self.paginate_queryset(self.object_list, page_size) + paginator, page, queryset, is_paginated = packed serializer = self.get_pagination_serializer(page) else: serializer = self.get_serializer(instance=self.object_list) @@ -79,20 +84,17 @@ class UpdateModelMixin(object): serializer = self.get_serializer(data=request.DATA, instance=self.object) if serializer.is_valid(): - if self.object is None: - # If PUT occurs to a non existant object, we need to set any - # attributes on the object that are implicit in the URL. - self.update_urlconf_attributes(serializer.object) + self.pre_save(serializer.object) self.object = serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def update_urlconf_attributes(self, obj): + def pre_save(self, obj): """ - When update (re)creates an object, we need to set any attributes that - are tied to the URLconf. + Set any attributes on the object that are implicit in the request. """ + # pk and/or slug attributes are implicit in the URL. pk = self.kwargs.get(self.pk_url_kwarg, None) if pk: setattr(obj, 'pk', pk) -- cgit v1.2.3 From d6e10b50fc6f1735d7dd6ee8bfd9d5d39b635b49 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 12:26:08 +0100 Subject: Re-add implementation of multiple-operation generic views to remove diamond inheritance --- rest_framework/generics.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index cfb3f29e..7a36f36a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -144,23 +144,44 @@ class UpdateAPIView(mixins.UpdateModelMixin, return self.update(request, *args, **kwargs) -class ListCreateAPIView(ListAPIView, - CreateAPIView): +class ListCreateAPIView(mixins.ListModelMixin, + mixins.CreateModelMixin, + MultipleObjectBaseView): """ Concrete view for listing a queryset or creating a model instance. """ + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) -class RetrieveDestroyAPIView(RetrieveAPIView, - DestroyAPIView): +class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + SingleObjectBaseView): """ Concrete view for retrieving or deleting a model instance. """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) -class RetrieveUpdateDestroyAPIView(RetrieveAPIView, - UpdateAPIView, - DestroyAPIView): +class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + SingleObjectBaseView): """ Concrete view for retrieving, updating or deleting a model instance. """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) -- cgit v1.2.3 From 27935f6f6652871c5ed1a2ab879fac22d5257549 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Thu, 25 Oct 2012 13:50:39 +0100 Subject: Rework generic view class names --- rest_framework/generics.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7a36f36a..81014026 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -10,7 +10,7 @@ from django.views.generic.list import MultipleObjectMixin ### Base classes for the generic views ### -class BaseView(views.APIView): +class GenericAPIView(views.APIView): """ Base class for all other generic views. """ @@ -51,7 +51,7 @@ class BaseView(views.APIView): return serializer_class(data, instance=instance, context=context) -class MultipleObjectBaseView(MultipleObjectMixin, BaseView): +class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): """ Base class for generic views onto a queryset. """ @@ -75,7 +75,7 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): return pagination_serializer_class(instance=page, context=context) -class SingleObjectBaseView(SingleObjectMixin, BaseView): +class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): """ Base class for generic views onto a model instance. """ @@ -86,7 +86,7 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): """ Override default to add support for object-level permissions. """ - obj = super(SingleObjectBaseView, self).get_object() + obj = super(SingleObjectAPIView, self).get_object() if not self.has_permission(self.request, obj): self.permission_denied(self.request) return obj @@ -97,7 +97,7 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView): class CreateAPIView(mixins.CreateModelMixin, - BaseView): + GenericAPIView): """ Concrete view for creating a model instance. @@ -107,7 +107,7 @@ class CreateAPIView(mixins.CreateModelMixin, class ListAPIView(mixins.ListModelMixin, - MultipleObjectBaseView): + MultipleObjectAPIView): """ Concrete view for listing a queryset. """ @@ -116,7 +116,7 @@ class ListAPIView(mixins.ListModelMixin, class RetrieveAPIView(mixins.RetrieveModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving a model instance. """ @@ -125,7 +125,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for deleting a model instance. @@ -135,7 +135,7 @@ class DestroyAPIView(mixins.DestroyModelMixin, class UpdateAPIView(mixins.UpdateModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for updating a model instance. @@ -146,7 +146,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, - MultipleObjectBaseView): + MultipleObjectAPIView): """ Concrete view for listing a queryset or creating a model instance. """ @@ -159,7 +159,7 @@ class ListCreateAPIView(mixins.ListModelMixin, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving or deleting a model instance. """ @@ -173,7 +173,7 @@ class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, - SingleObjectBaseView): + SingleObjectAPIView): """ Concrete view for retrieving, updating or deleting a model instance. """ -- cgit v1.2.3 From 195006bbc36c21f0154fe1ab7c46f339b2efe559 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 09:27:59 +0100 Subject: Drop resources from codebase since implementation is only partial (Created resoorces-routers branch for future reference) --- rest_framework/resources.py | 96 --------------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 rest_framework/resources.py (limited to 'rest_framework') diff --git a/rest_framework/resources.py b/rest_framework/resources.py deleted file mode 100644 index dd8a5471..00000000 --- a/rest_framework/resources.py +++ /dev/null @@ -1,96 +0,0 @@ -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -from functools import update_wrapper -import inspect -from django.utils.decorators import classonlymethod -from rest_framework import views, generics - - -def wrapped(source, dest): - """ - Copy public, non-method attributes from source to dest, and return dest. - """ - for attr in [attr for attr in dir(source) - if not attr.startswith('_') and not inspect.ismethod(attr)]: - setattr(dest, attr, getattr(source, attr)) - return dest - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ResourceMixin(object): - """ - Clone Django's `View.as_view()` behaviour *except* using REST framework's - 'method -> action' binding for resources. - """ - - @classonlymethod - def as_view(cls, actions, **initkwargs): - """ - Main entry point for a request-response process. - """ - # sanitize keyword arguments - for key in initkwargs: - if key in cls.http_method_names: - raise TypeError("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - - # Bind methods to actions - for method, action in actions.items(): - handler = getattr(self, action) - setattr(self, method, handler) - - # As you were, solider. - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - return self.dispatch(request, *args, **kwargs) - - # take name and docstring from class - update_wrapper(view, cls, updated=()) - - # and possible attributes set by decorators - # like csrf_exempt from dispatch - update_wrapper(view, cls.dispatch, assigned=()) - return view - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class Resource(ResourceMixin, views.APIView): - pass - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ModelResource(ResourceMixin, views.APIView): - # TODO: Actually delegation won't work - root_class = generics.ListCreateAPIView - detail_class = generics.RetrieveUpdateDestroyAPIView - - def root_view(self): - return wrapped(self, self.root_class()) - - def detail_view(self): - return wrapped(self, self.detail_class()) - - def list(self, request, *args, **kwargs): - return self.root_view().list(request, args, kwargs) - - def create(self, request, *args, **kwargs): - return self.root_view().create(request, args, kwargs) - - def retrieve(self, request, *args, **kwargs): - return self.detail_view().retrieve(request, args, kwargs) - - def update(self, request, *args, **kwargs): - return self.detail_view().update(request, args, kwargs) - - def destroy(self, request, *args, **kwargs): - return self.detail_view().destroy(request, args, kwargs) -- cgit v1.2.3 From 32d602880fc88e2b3e8d8f2a82132bed224f8b49 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:45:52 +0100 Subject: Choice fields from ModelSerializer. --- rest_framework/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8ee9a0ec..d4fcddd5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -3,6 +3,7 @@ import datetime import types from decimal import Decimal from django.db import models +from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model from rest_framework.fields import * @@ -409,6 +410,15 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False + kwargs['default'] = model_field.default + + if model_field.__class__ == models.TextField: + kwargs['widget'] = widgets.Textarea + + # TODO: TypedChoiceField? + if model_field.flatchoices: # This ModelField contains choices + kwargs['choices'] = model_field.flatchoices + return ChoiceField(**kwargs) field_mapping = { models.FloatField: FloatField, -- cgit v1.2.3 From 2efb5f8a14ffc321a1a9e88548abfa8b0782aae4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:15 +0100 Subject: Object-level permissions respected by Browseable API --- rest_framework/renderers.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c64fb517..1a8b1d97 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -224,7 +224,7 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def show_form_for_method(self, view, method, request): + def show_form_for_method(self, view, method, request, obj): """ Returns True if a form should be shown for this method. """ @@ -236,7 +236,7 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request): + if not view.has_permission(request, obj): return # Don't have permission except: return # Don't have permission and exception explicitly raise @@ -295,7 +295,8 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence on of the Resource having an associated form then provide a form that can be used to submit arbitrary content. """ - if not self.show_form_for_method(view, method, request): + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): return if method == 'DELETE' or method == 'OPTIONS': @@ -305,17 +306,13 @@ class BrowsableAPIRenderer(BaseRenderer): media_types = [parser.media_type for parser in view.parser_classes] return self.get_generic_content_form(media_types) - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) + # Creating an on the fly form see: + # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj: - data = serializer.data + data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From fc4614a89c5c2bbdeb3626e2f16e3a2cd6445e3e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:41 +0100 Subject: Whitespace --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6ed37823..85e6ee31 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -258,7 +258,7 @@ class ManyRelatedMixin(object): def field_from_native(self, data, field_name, into): if self.readonly: return - + try: # Form data value = data.getlist(self.source or field_name) -- cgit v1.2.3 From 67f1265e493adc35239d90aeb3bfeb8492fbd741 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 13:20:30 +0100 Subject: Fix failing 'default' on ModelSerializer --- rest_framework/serializers.py | 2 +- rest_framework/tests/models.py | 4 ++-- rest_framework/tests/serializer.py | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d4fcddd5..db6f9f61 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -410,7 +410,7 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False - kwargs['default'] = model_field.default + kwargs['default'] = model_field.get_default() if model_field.__class__ == models.TextField: kwargs['widget'] = widgets.Textarea diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 97cd0849..0ee18c69 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,12 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) - + class ReadOnlyManyToManyModel(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') rel = models.ManyToManyField(Anchor) - + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 936f15aa..67c97f0f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -7,7 +7,7 @@ from rest_framework.tests.models import * class SubComment(object): def __init__(self, sub_comment): self.sub_comment = sub_comment - + class Comment(object): def __init__(self, email, content, created): @@ -18,7 +18,7 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) - + def get_sub_comment(self): sub_comment = SubComment('And Merry Christmas!') return sub_comment @@ -29,7 +29,7 @@ class CommentSerializer(serializers.Serializer): content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() sub_comment = serializers.Field(source='get_sub_comment.sub_comment') - + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,6 +42,7 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -73,7 +74,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) + serializer = CommentSerializer(instance=self.comment) self.assertEquals(serializer.data, self.expected) def test_create(self): @@ -104,7 +105,7 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } self.actionitem = ActionItem('Some to do item', ) @@ -131,7 +132,7 @@ class ValidationTests(TestCase): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" data = { - 'title':'Some action item', + 'title': 'Some action item', #No 'done' value. } serializer = ActionItemSerializer(data, instance=self.actionitem) @@ -295,11 +296,13 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) - + + class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(readonly=True) + class Meta: model = ReadOnlyManyToManyModel @@ -317,7 +320,6 @@ class ReadOnlyManyToManyTests(TestCase): # A serialized representation of the model instance self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} - def test_update(self): """ Attempt to update an instance of a model with a ManyToMany -- cgit v1.2.3 From 44207a347ac765d900f15b65bdd24dbf8eb9ead2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 10:32:49 +0100 Subject: pep8 --- rest_framework/compat.py | 4 +++- rest_framework/generics.py | 2 +- rest_framework/permissions.py | 2 +- rest_framework/request.py | 4 ++-- rest_framework/serializers.py | 8 ++++---- 5 files changed, 11 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7664c400..b0367a32 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -1,6 +1,8 @@ """ -The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. +The `compat` module provides support for backwards compatibility with older +versions of django/python, and compatbility wrappers around optional packages. """ +# flake8: noqa import django # cStringIO only if it's available, otherwise StringIO diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 81014026..190a5f79 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -125,7 +125,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectAPIView): + SingleObjectAPIView): """ Concrete view for deleting a model instance. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6f848cee..51e96196 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -85,7 +85,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': model_cls._meta.module_name } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/request.py b/rest_framework/request.py index 5870be82..a1827ba4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -21,8 +21,8 @@ def is_form_media_type(media_type): Return True if the media type is a valid form media type. """ base_media_type, params = parse_header(media_type) - return base_media_type == 'application/x-www-form-urlencoded' or \ - base_media_type == 'multipart/form-data' + return (base_media_type == 'application/x-www-form-urlencoded' or + base_media_type == 'multipart/form-data') class Empty(object): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index db6f9f61..8a895343 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -34,10 +34,10 @@ def _is_protected_type(obj): """ return isinstance(obj, ( types.NoneType, - int, long, - datetime.datetime, datetime.date, datetime.time, - float, Decimal, - basestring) + int, long, + datetime.datetime, datetime.date, datetime.time, + float, Decimal, + basestring) ) -- cgit v1.2.3 From b9e576f16ef7cc98f671e9c18ff8ae1a95bfe3ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:44:23 +0100 Subject: Push tests into a seperate app namespace 'rest_framework.test' Prevents tests from running by default when rest_framework is installed as 3rd party app. Fixes #316, #185 --- rest_framework/runtests/runtests.py | 2 +- rest_framework/runtests/settings.py | 9 +-------- rest_framework/tests/__init__.py | 13 ------------- rest_framework/tests/models.py | 2 +- rest_framework/tests/tests.py | 13 +++++++++++++ 5 files changed, 16 insertions(+), 23 deletions(-) create mode 100644 rest_framework/tests/tests.py (limited to 'rest_framework') diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index b2438c9b..1bd0a5fc 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -32,7 +32,7 @@ def main(): else: print usage() sys.exit(1) - failures = test_runner.run_tests(['rest_framework' + test_case]) + failures = test_runner.run_tests(['tests' + test_case]) sys.exit(failures) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 67de82c8..951b1e72 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -91,6 +91,7 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'rest_framework', 'rest_framework.authtoken', + 'rest_framework.tests' ) STATIC_URL = '/static/' @@ -100,14 +101,6 @@ import django if django.VERSION < (1, 3): INSTALLED_APPS += ('staticfiles',) -# OAuth support is optional, so we only test oauth if it's installed. -try: - import oauth_provider -except ImportError: - pass -else: - INSTALLED_APPS += ('oauth_provider',) - # If we're running on the Jenkins server we want to archive the coverage reports as XML. import os if os.environ.get('HUDSON_URL', None): diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index adeaf6da..e69de29b 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -1,13 +0,0 @@ -""" -Force import of all modules in this package in order to get the standard test -runner to pick up the tests. Yowzers. -""" -import os - -modules = [filename.rsplit('.', 1)[0] - for filename in os.listdir(os.path.dirname(__file__)) - if filename.endswith('.py') and not filename.startswith('_')] -__test__ = dict() - -for module in modules: - exec("from rest_framework.tests.%s import *" % module) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 0ee18c69..d4ea729b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -40,7 +40,7 @@ class RESTFrameworkModel(models.Model): Base for test models that sets app_label, so they play nicely. """ class Meta: - app_label = 'rest_framework' + app_label = 'tests' abstract = True diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py new file mode 100644 index 00000000..adeaf6da --- /dev/null +++ b/rest_framework/tests/tests.py @@ -0,0 +1,13 @@ +""" +Force import of all modules in this package in order to get the standard test +runner to pick up the tests. Yowzers. +""" +import os + +modules = [filename.rsplit('.', 1)[0] + for filename in os.listdir(os.path.dirname(__file__)) + if filename.endswith('.py') and not filename.startswith('_')] +__test__ = dict() + +for module in modules: + exec("from rest_framework.tests.%s import *" % module) -- cgit v1.2.3 From d995742afc09ff8d387751a6fe47b9686845740b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:04:33 +0100 Subject: Add AllowAny permission --- rest_framework/settings.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 3c508294..9c40a214 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -37,11 +37,14 @@ DEFAULTS = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication' ), - 'DEFAULT_PERMISSION_CLASSES': (), - 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_THROTTLE_CLASSES': ( + ), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', - 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 'DEFAULT_PAGINATION_SERIALIZER_CLASS': -- cgit v1.2.3 From af96fe05d0138c34128fc3944fc2701cbad5bd01 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:17:49 +0100 Subject: Add AllowAny class --- rest_framework/permissions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 51e96196..655b78a3 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -18,6 +18,17 @@ class BasePermission(object): raise NotImplementedError(".has_permission() must be overridden.") +class AllowAny(BasePermission): + """ + Allow any access. + This isn't strictly required, since you could use an empty + permission_classes list, but it's useful because it makes the intention + more explicit. + """ + def has_permission(self, request, view, obj=None): + return True + + class IsAuthenticated(BasePermission): """ Allows access only to authenticated users. -- cgit v1.2.3 From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: TemplateHTMLRenderer, StaticHTMLRenderer --- rest_framework/renderers.py | 41 +++++++++++++++++++++++++++++++----- rest_framework/tests/htmlrenderer.py | 6 +++--- 2 files changed, 39 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1a8b1d97..cfe4df6d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -139,13 +139,24 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) -class HTMLRenderer(BaseRenderer): +class TemplateHTMLRenderer(BaseRenderer): """ - A Base class provided for convenience. + An HTML renderer for use with templates. - Render the object simply by using the given template. - To create a template renderer, subclass this class, and set - the :attr:`media_type` and :attr:`template` attributes. + The data supplied to the Response object should be a dictionary that will + be used as context for the template. + + The template name is determined by (in order of preference): + + 1. An explicit `.template_name` attribute set on the response. + 2. An explicit `.template_name` attribute set on this class. + 3. The return result of calling `view.get_template_names()`. + + For example: + data = {'users': User.objects.all()} + return Response(data, template_name='users.html') + + For pre-rendered HTML, see StaticHTMLRenderer. """ media_type = 'text/html' @@ -188,6 +199,26 @@ class HTMLRenderer(BaseRenderer): raise ConfigurationError('Returned a template response with no template_name') +class StaticHTMLRenderer(BaseRenderer): + """ + An HTML renderer class that simply returns pre-rendered HTML. + + The data supplied to the Response object should be a string representing + the pre-rendered HTML content. + + For example: + data = 'example' + return Response(data) + + For template rendered HTML, see TemplateHTMLRenderer. + """ + media_type = 'text/html' + format = 'html' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index da2f83c3..10d7e31d 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -3,12 +3,12 @@ from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import HTMLRenderer +from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response @api_view(('GET',)) -@renderer_classes((HTMLRenderer,)) +@renderer_classes((TemplateHTMLRenderer,)) def example(request): """ A view that can returns an HTML representation. @@ -22,7 +22,7 @@ urlpatterns = patterns('', ) -class HTMLRendererTests(TestCase): +class TemplateHTMLRendererTests(TestCase): urls = 'rest_framework.tests.htmlrenderer' def setUp(self): -- cgit v1.2.3 From bc99142c7dc1ebf84ca0858ce32b400a537e1908 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 19:35:50 +0100 Subject: Added wo tests. One for PUTing on a non-existing id-based url. And another for PUTing on a non-existing slug-based url. Fix doctoring for 'test_put_cannot_set_id'. --- rest_framework/tests/generics.py | 44 ++++++++++++++++++++++++++++++++++++++-- rest_framework/tests/models.py | 5 +++++ 2 files changed, 47 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f4263478..151532a7 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status -from rest_framework.tests.models import BasicModel, Comment +from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel factory = RequestFactory() @@ -22,6 +22,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugBasedInstanceView(InstanceView): + """ + A model with a slug-field. + """ + model = SlugBasedModel + + class TestRootView(TestCase): def setUp(self): """ @@ -129,6 +136,7 @@ class TestInstanceView(TestCase): for obj in self.objects.all() ] self.view = InstanceView.as_view() + self.slug_based_view = SlugBasedInstanceView.as_view() def test_get_instance_view(self): """ @@ -198,7 +206,7 @@ class TestInstanceView(TestCase): def test_put_cannot_set_id(self): """ - POST requests to create a new object should not be able to set the id. + PUT requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -224,6 +232,38 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist, if creation is not possible, + e.g. the pk for an id-field is determined by the database, + a HTTP_403_FORBIDDEN error-response must be returned. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set th pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=5).render() + expected = { + 'detail': u'A resource could not be created at the requested URI' + } + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEquals(response.data, expected) + + def test_put_as_create_on_slug_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. + """ + content = {'text': 'foobar'} + request = factory.put('/test_slug', json.dumps(content), + content_type='application/json') + response = self.slug_based_view(request, pk='test_slug').render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) + updated = self.objects.get(slug='test_slug') + self.assertEquals(updated.text, 'foobar') + # Regression test for #285 diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index d4ea729b..ac73a4bb 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + class DefaultValueModel(RESTFrameworkModel): text = models.CharField(default='foobar', max_length=100) -- cgit v1.2.3 From 5bb66803761c0536497158d14ce0a23665a335da Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:45:42 +0100 Subject: test_put_as_create_on_id_based_url should check for a created-response. --- rest_framework/tests/generics.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 151532a7..48805720 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -235,20 +235,16 @@ class TestInstanceView(TestCase): def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object - at the requested url if it doesn't exist, if creation is not possible, - e.g. the pk for an id-field is determined by the database, - a HTTP_403_FORBIDDEN error-response must be returned. + at the requested url if it doesn't exist. """ content = {'text': 'foobar'} # pk fields can not be created on demand, only the database can set th pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') response = self.view(request, pk=5).render() - expected = { - 'detail': u'A resource could not be created at the requested URI' - } - self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEquals(response.data, expected) + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): """ @@ -261,8 +257,8 @@ class TestInstanceView(TestCase): response = self.slug_based_view(request, pk='test_slug').render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) - updated = self.objects.get(slug='test_slug') - self.assertEquals(updated.text, 'foobar') + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') # Regression test for #285 -- cgit v1.2.3 From 1a16289edeea73253826916ca230af2bf30ba39f Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:56:48 +0100 Subject: Get the correct instance --- rest_framework/tests/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 48805720..a0a4109d 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -243,7 +243,7 @@ class TestInstanceView(TestCase): content_type='application/json') response = self.view(request, pk=5).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) - new_obj = self.objects.get(slug='test_slug') + new_obj = self.objects.get(pk=5) self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): -- cgit v1.2.3 From 6e4ab09aae8295e4ef722d59894bc2934435ae46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:21:45 +0000 Subject: readonly -> read_only --- rest_framework/fields.py | 14 +++++++------- rest_framework/renderers.py | 2 +- rest_framework/tests/serializer.py | 6 +++--- rest_framework/tests/validators.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 85e6ee31..c0e527e5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -111,17 +111,17 @@ class WritableField(Field): widget = widgets.TextInput default = None - def __init__(self, source=None, readonly=False, required=None, + def __init__(self, source=None, read_only=False, required=None, validators=[], error_messages=None, widget=None, default=None): super(WritableField, self).__init__(source=source) - self.readonly = readonly + self.read_only = read_only if required is None: - self.required = not(readonly) + self.required = not(read_only) else: - assert not readonly, "Cannot set required=True and readonly=True" + assert not read_only, "Cannot set required=True and read_only=True" self.required = required messages = {} @@ -166,7 +166,7 @@ class WritableField(Field): Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. """ - if self.readonly: + if self.read_only: return try: @@ -240,7 +240,7 @@ class RelatedField(WritableField): return self.to_native(value) def field_from_native(self, data, field_name, into): - if self.readonly: + if self.read_only: return value = data.get(field_name) @@ -256,7 +256,7 @@ class ManyRelatedMixin(object): return [self.to_native(item) for item in value.all()] def field_from_native(self, data, field_name, into): - if self.readonly: + if self.read_only: return try: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index cfe4df6d..938e8664 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields = {} for k, v in serializer.get_fields(True).items(): - if getattr(v, 'readonly', True): + if getattr(v, 'read_only', True): continue kwargs = {} diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 67c97f0f..5df3bd7e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -301,7 +301,7 @@ class ManyToManyTests(TestCase): class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(read_only=True) class Meta: model = ReadOnlyManyToManyModel @@ -323,7 +323,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update(self): """ Attempt to update an instance of a model with a ManyToMany - relationship. Not updated due to readonly=True + relationship. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() @@ -339,7 +339,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update_without_relationship(self): """ Attempt to update an instance of a model where many to ManyToMany - relationship is not supplied. Not updated due to readonly=True + relationship is not supplied. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() diff --git a/rest_framework/tests/validators.py b/rest_framework/tests/validators.py index b390c42f..c032985e 100644 --- a/rest_framework/tests/validators.py +++ b/rest_framework/tests/validators.py @@ -285,7 +285,7 @@ # uiop = models.CharField(max_length=256, blank=True) # @property -# def readonly(self): +# def read_only(self): # return 'read only' # class MockResource(ModelResource): @@ -298,7 +298,7 @@ # def test_property_fields_are_allowed_on_model_forms(self): # """Validation on ModelForms may include property fields that exist on the Model to be included in the input.""" -# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only'} # self.assertEqual(self.validator.validate_request(content, None), content) # def test_property_fields_are_not_required_on_model_forms(self): @@ -310,19 +310,19 @@ # """If some (otherwise valid) content includes fields that are not in the form then validation should fail. # It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up # broken clients more easily (eg submitting content with a misnamed field)""" -# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only', 'extra': 'extra'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # def test_validate_requires_fields_on_model_forms(self): # """If some (otherwise valid) content includes fields that are not in the form then validation should fail. # It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up # broken clients more easily (eg submitting content with a misnamed field)""" -# content = {'readonly': 'read only'} +# content = {'read_only': 'read only'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # def test_validate_does_not_require_blankable_fields_on_model_forms(self): # """Test standard ModelForm validation behaviour - fields with blank=True are not required.""" -# content = {'qwerty': 'example', 'readonly': 'read only'} +# content = {'qwerty': 'example', 'read_only': 'read only'} # self.validator.validate_request(content, None) # def test_model_form_validator_uses_model_forms(self): -- cgit v1.2.3 From 351382fe35f966c989b27add5bb04d0d983a99ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:43:43 +0000 Subject: nested -> depth --- rest_framework/serializers.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8a895343..31d9d7a5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -74,7 +74,7 @@ class SerializerOptions(object): Meta class options for Serializer """ def __init__(self, meta): - self.nested = getattr(meta, 'nested', False) + self.depth = getattr(meta, 'depth', 0) self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) @@ -156,10 +156,8 @@ class BaseSerializer(Field): """ super(BaseSerializer, self).initialize(parent) self.stack = parent.stack[:] - if parent.opts.nested and not isinstance(parent.opts.nested, bool): - self.opts.nested = parent.opts.nested - 1 - else: - self.opts.nested = parent.opts.nested + if parent.opts.depth: + self.opts.depth = parent.opts.depth - 1 ##### # Methods to convert or revert from objects <--> primative representations. @@ -182,14 +180,10 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - fields = self.get_fields(serialize=True, obj=obj, nested=self.opts.nested) + fields = self.get_fields(serialize=True, obj=obj, nested=bool(self.opts.depth)) for field_name, field in fields.items(): key = self.get_field_key(field_name) - try: - value = field.field_to_native(obj, field_name) - except RecursionOccured: - field = self.get_fields(serialize=True, obj=obj, nested=False)[field_name] - value = field.field_to_native(obj, field_name) + value = field.field_to_native(obj, field_name) ret[key] = value ret.fields[key] = field return ret @@ -199,7 +193,7 @@ class BaseSerializer(Field): Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. """ - fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + fields = self.get_fields(serialize=False, data=data, nested=bool(self.opts.depth)) reverted_data = {} for field_name, field in fields.items(): try: @@ -213,7 +207,8 @@ class BaseSerializer(Field): """ Run `validate_()` and `validate()` methods on the serializer """ - fields = self.get_fields(serialize=False, data=attrs, nested=self.opts.nested) + # TODO: refactor this so we're not determining the fields again + fields = self.get_fields(serialize=False, data=attrs, nested=bool(self.opts.depth)) for field_name, field in fields.items(): try: -- cgit v1.2.3 From de6908fbef89f9fb02b5a2a7bfcd85280448f241 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:50:14 +0000 Subject: Remove recursion detection --- rest_framework/serializers.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31d9d7a5..ce04b3e2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -23,10 +23,6 @@ class SortedDictWithMetadata(SortedDict, DictWithMetadata): pass -class RecursionOccured(BaseException): - pass - - def _is_protected_type(obj): """ True if the object is a native datatype that does not need to @@ -93,7 +89,6 @@ class BaseSerializer(Field): self.parent = None self.root = None - self.stack = [] self.context = context or {} self.init_data = data @@ -152,10 +147,9 @@ class BaseSerializer(Field): def initialize(self, parent): """ Same behaviour as usual Field, except that we need to keep track - of state so that we can deal with handling maximum depth and recursion. + of state so that we can deal with handling maximum depth. """ super(BaseSerializer, self).initialize(parent) - self.stack = parent.stack[:] if parent.opts.depth: self.opts.depth = parent.opts.depth - 1 @@ -173,10 +167,6 @@ class BaseSerializer(Field): Core of serialization. Convert an object into a dictionary of serialized field values. """ - if obj in self.stack and not self.source == '*': - raise RecursionOccured() - self.stack.append(obj) - ret = self._dict_class() ret.fields = {} -- cgit v1.2.3 From f4edd9256667af205f4edf9ada642ef9a62802e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Oct 2012 12:51:21 +0000 Subject: Writable welated fields should return a model instance from .from_native(), not a pk --- rest_framework/fields.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0e527e5..ffc0c9d4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -244,7 +244,7 @@ class RelatedField(WritableField): return value = data.get(field_name) - into[(self.source or field_name) + '_id'] = self.from_native(value) + into[(self.source or field_name)] = self.from_native(value) class ManyRelatedMixin(object): @@ -288,6 +288,12 @@ class PrimaryKeyRelatedField(RelatedField): def to_native(self, pk): return pk + def from_native(self, data): + try: + return self.queryset.get(pk=data) + except ObjectDoesNotExist: + raise ValidationError('Invalid hyperlink - object does not exist.') + def field_to_native(self, obj, field_name): try: # Prefer obj.serializable_value for performance reasons @@ -377,7 +383,7 @@ class HyperlinkedRelatedField(RelatedField): # Try explicit primary key. if pk is not None: - return pk + queryset = self.queryset.filter(pk=pk) # Next, try looking up by slug. elif slug is not None: slug_field = self.get_slug_field() @@ -390,7 +396,7 @@ class HyperlinkedRelatedField(RelatedField): obj = queryset.get() except ObjectDoesNotExist: raise ValidationError('Invalid hyperlink - object does not exist.') - return obj.pk + return obj class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): -- cgit v1.2.3 From 752f191a763b4375e6fa03f8bd88416d59226760 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Oct 2012 13:18:51 +0000 Subject: Fix breadcrumbs --- rest_framework/static/rest_framework/css/default.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 739b9300..b46f025e 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -32,6 +32,10 @@ h2, h3 { margin-right: 1em; } +ul.breadcrumb { + margin: 58px 0 0 0; +} + /* To allow tooltips to work on disabled elements */ .disabled-tooltip-shield { position: absolute; @@ -65,7 +69,7 @@ html{ background: none; } -body, .navbar .navbar-inner .container-fluid{ +body, .navbar .navbar-inner .container-fluid { max-width: 1150px; margin: 0 auto; } @@ -76,13 +80,14 @@ body{ } #content{ - margin: 40px 0 0 0; + margin: 0; } /* custom navigation styles */ .wrapper .navbar{ - width:100%; + width: 100%; position: absolute; - left:0; + left: 0; + top: 0; } .navbar .navbar-inner{ -- cgit v1.2.3 From dfcb560f8f8343d9ffa3cbe24e2036fd4cd55acf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Oct 2012 13:57:46 +0000 Subject: Fix up login styling --- rest_framework/templates/rest_framework/login.html | 56 ++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index 65af512e..c1271399 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -3,42 +3,50 @@ - + + + - + -
- -