aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXavier Ordoquy2012-12-02 12:43:32 +0100
committerXavier Ordoquy2012-12-02 12:43:32 +0100
commit5fad46d7e213afed503b1533515cab96875a5936 (patch)
tree72ab362e86a83ba53361613dbb1e7863889ea749
parentfa53dde576c8733292eacf27c80cf7a0ad222c3b (diff)
parent3114b4fa50e7aee296a0de17e7bcdc0753700ec3 (diff)
downloaddjango-rest-framework-5fad46d7e213afed503b1533515cab96875a5936.tar.bz2
Merge remote-tracking branch 'reference/master' into p3k
-rw-r--r--README.md23
-rw-r--r--docs/api-guide/authentication.md2
-rw-r--r--docs/api-guide/serializers.md4
-rw-r--r--docs/api-guide/views.md4
-rw-r--r--docs/topics/credits.md10
-rw-r--r--docs/topics/release-notes.md17
-rw-r--r--docs/tutorial/2-requests-and-responses.md6
-rw-r--r--docs/tutorial/3-class-based-views.md14
-rw-r--r--optionals.txt2
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authtoken/views.py2
-rw-r--r--rest_framework/fields.py26
-rw-r--r--rest_framework/negotiation.py4
-rw-r--r--rest_framework/renderers.py28
-rw-r--r--rest_framework/serializers.py25
-rw-r--r--rest_framework/tests/authentication.py4
-rw-r--r--rest_framework/tests/serializer.py12
17 files changed, 123 insertions, 62 deletions
diff --git a/README.md b/README.md
index 9a12d535..f646f957 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,29 @@ To run the tests.
# Changelog
+## 2.1.6
+
+**Date**: 23rd Nov 2012
+
+* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
+
+## 2.1.5
+
+**Date**: 23rd Nov 2012
+
+* Bugfix: Fix DjangoModelPermissions.
+
+## 2.1.4
+
+**Date**: 22nd Nov 2012
+
+* Support for partial updates with serializers.
+* Added `RegexField`.
+* Added `SerializerMethodField`.
+* Serializer performance improvements.
+* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
+* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
+
## 2.1.3
**Date**: 16th Nov 2012
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 8ed6ef31..43fc15d2 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -116,7 +116,7 @@ When using `TokenAuthentication`, you may want to provide a mechanism for client
REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
urlpatterns += patterns('',
- url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token')
+ url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
)
Note that the URL part of the pattern can be whatever you want to use.
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 048c1200..19efde3c 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -77,6 +77,10 @@ When deserializing data, we can either create a new instance, or update an exist
serializer = CommentSerializer(data=data) # Create new instance
serializer = CommentSerializer(comment, data=data) # Update `instance`
+By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
+
+ serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
+
## Validation
When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages.
diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md
index 5b072827..d1e42ec1 100644
--- a/docs/api-guide/views.md
+++ b/docs/api-guide/views.md
@@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas
For example:
+ from rest_framework.views import APIView
+ from rest_framework.response import Response
+ from rest_framework import authentication, permissions
+
class ListUsers(APIView):
"""
View to list all users in the system.
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 955870d2..e0c589b2 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -64,6 +64,11 @@ The following people have helped make REST framework great.
* Eugene Mechanism - [mechanism]
* Jonas Liljestrand - [jonlil]
* Justin Davis - [irrelative]
+* Dustin Bachrach - [dbachrach]
+* Mark Shirley - [maspwr]
+* Olivier Aubert - [oaubert]
+* Yuri Prezument - [yprez]
+* Fabian Buechler - [fabianbuechler]
Many thanks to everyone who's contributed to the project.
@@ -163,3 +168,8 @@ To contact the author directly:
[mechanism]: https://github.com/mechanism
[jonlil]: https://github.com/jonlil
[irrelative]: https://github.com/irrelative
+[dbachrach]: https://github.com/dbachrach
+[maspwr]: https://github.com/maspwr
+[oaubert]: https://github.com/oaubert
+[yprez]: https://github.com/yprez
+[fabianbuechler]: https://github.com/fabianbuechler
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index c641a1b3..867b138b 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -4,8 +4,23 @@
>
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
-## Master
+## 2.1.6
+**Date**: 23rd Nov 2012
+
+* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
+
+## 2.1.5
+
+**Date**: 23rd Nov 2012
+
+* Bugfix: Fix DjangoModelPermissions.
+
+## 2.1.4
+
+**Date**: 22nd Nov 2012
+
+* Support for partial updates with serializers.
* Added `RegexField`.
* Added `SerializerMethodField`.
* Serializer performance improvements.
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index b29daf05..187effb9 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -41,8 +41,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
- from snippet.models import Snippet
- from snippet.serializers import SnippetSerializer
+ from snippets.models import Snippet
+ from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
@@ -113,7 +113,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
- urlpatterns = patterns('snippet.views',
+ urlpatterns = patterns('snippets.views',
url(r'^snippets/$', 'snippet_list'),
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
)
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index eddf6311..d87d2046 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -6,8 +6,8 @@ We can also write our API views using class based views, rather than function ba
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
- from snippet.models import Snippet
- from snippet.serializers import SnippetSerializer
+ from snippets.models import Snippet
+ from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
@@ -66,7 +66,7 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
- from snippetpost import views
+ from snippets import views
urlpatterns = patterns('',
url(r'^snippets/$', views.SnippetList.as_view()),
@@ -85,8 +85,8 @@ The create/retrieve/update/delete operations that we've been using so far are go
Let's take a look at how we can compose our views by using the mixin classes.
- from snippet.models import Snippet
- from snippet.serializers import SnippetSerializer
+ from snippets.models import Snippet
+ from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
@@ -128,8 +128,8 @@ Pretty similar. This time we're using the `SingleObjectBaseView` class to provi
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
- from snippet.models import Snippet
- from snippet.serializers import SnippetSerializer
+ from snippets.models import Snippet
+ from snippets.serializers import SnippetSerializer
from rest_framework import generics
diff --git a/optionals.txt b/optionals.txt
index 320cf216..1d2358c6 100644
--- a/optionals.txt
+++ b/optionals.txt
@@ -1,3 +1,3 @@
markdown>=2.1.0
PyYAML>=3.10
--e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
+django-filter>=0.5.4
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 88108a8d..48cebbc5 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.3'
+__version__ = '2.1.6'
VERSION = __version__ # synonym
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 3ac674e2..cfaacbe9 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -18,7 +18,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 5c5a86c1..8ed7efa5 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -12,6 +12,7 @@ from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
+from django import forms
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type
@@ -45,6 +46,7 @@ class Field(object):
empty = ''
type_name = None
_use_files = None
+ form_field_class = forms.CharField
def __init__(self, source=None):
self.parent = None
@@ -64,6 +66,8 @@ class Field(object):
self.parent = parent
self.root = parent.root or parent
self.context = self.root.context
+ if self.root.partial:
+ self.required = False
def field_from_native(self, data, files, field_name, into):
"""
@@ -231,7 +235,7 @@ class ModelField(WritableField):
getattr(self.model_field, 'min_length', None))
self.max_length = kwargs.pop('max_length',
getattr(self.model_field, 'max_length', None))
-
+
super(ModelField, self).__init__(*args, **kwargs)
if self.min_length is not None:
@@ -402,6 +406,7 @@ class PrimaryKeyRelatedField(RelatedField):
Represents a to-one relationship as a pk value.
"""
default_read_only = False
+ form_field_class = forms.ChoiceField
# TODO: Remove these field hacks...
def prepare_value(self, obj):
@@ -448,6 +453,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
Represents a to-many relationship as a pk value.
"""
default_read_only = False
+ form_field_class = forms.MultipleChoiceField
def prepare_value(self, obj):
return self.to_native(obj.pk)
@@ -491,6 +497,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
class SlugRelatedField(RelatedField):
default_read_only = False
+ form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
@@ -512,7 +519,7 @@ class SlugRelatedField(RelatedField):
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
- pass
+ form_field_class = forms.MultipleChoiceField
### Hyperlinked relationships
@@ -525,6 +532,7 @@ class HyperlinkedRelatedField(RelatedField):
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
default_read_only = False
+ form_field_class = forms.ChoiceField
def __init__(self, *args, **kwargs):
try:
@@ -624,7 +632,7 @@ class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
"""
Represents a to-many relationship, using hyperlinking.
"""
- pass
+ form_field_class = forms.MultipleChoiceField
class HyperlinkedIdentityField(Field):
@@ -682,6 +690,7 @@ class HyperlinkedIdentityField(Field):
class BooleanField(WritableField):
type_name = 'BooleanField'
+ form_field_class = forms.BooleanField
widget = widgets.CheckboxInput
default_error_messages = {
'invalid': _("'%s' value must be either True or False."),
@@ -703,6 +712,7 @@ class BooleanField(WritableField):
class CharField(WritableField):
type_name = 'CharField'
+ form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
self.max_length, self.min_length = max_length, min_length
@@ -747,6 +757,7 @@ class SlugField(CharField):
class ChoiceField(WritableField):
type_name = 'ChoiceField'
+ form_field_class = forms.ChoiceField
widget = widgets.Select
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
@@ -793,6 +804,7 @@ class ChoiceField(WritableField):
class EmailField(CharField):
type_name = 'EmailField'
+ form_field_class = forms.EmailField
default_error_messages = {
'invalid': _('Enter a valid e-mail address.'),
@@ -843,6 +855,8 @@ class RegexField(CharField):
class DateField(WritableField):
type_name = 'DateField'
+ widget = widgets.DateInput
+ form_field_class = forms.DateField
default_error_messages = {
'invalid': _("'%s' value has an invalid date format. It must be "
@@ -880,6 +894,8 @@ class DateField(WritableField):
class DateTimeField(WritableField):
type_name = 'DateTimeField'
+ widget = widgets.DateTimeInput
+ form_field_class = forms.DateTimeField
default_error_messages = {
'invalid': _("'%s' value has an invalid format. It must be in "
@@ -934,6 +950,7 @@ class DateTimeField(WritableField):
class IntegerField(WritableField):
type_name = 'IntegerField'
+ form_field_class = forms.IntegerField
default_error_messages = {
'invalid': _('Enter a whole number.'),
@@ -963,6 +980,7 @@ class IntegerField(WritableField):
class FloatField(WritableField):
type_name = 'FloatField'
+ form_field_class = forms.FloatField
default_error_messages = {
'invalid': _("'%s' value must be a float."),
@@ -982,6 +1000,7 @@ class FloatField(WritableField):
class FileField(WritableField):
_use_files = True
type_name = 'FileField'
+ form_field_class = forms.FileField
widget = widgets.FileInput
default_error_messages = {
@@ -1024,6 +1043,7 @@ class FileField(WritableField):
class ImageField(FileField):
_use_files = True
+ form_field_class = forms.ImageField
default_error_messages = {
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index dae38477..ee2800a6 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -2,6 +2,7 @@ from django.http import Http404
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
+from rest_framework.utils.mediatypes import _MediaType
class BaseContentNegotiation(object):
@@ -48,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted.
- if len(renderer.media_type) > len(media_type):
+ if (_MediaType(renderer.media_type).precedence >
+ _MediaType(media_type).precedence):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
return renderer, renderer.media_type
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 4abce906..44a40baf 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -308,26 +308,6 @@ class BrowsableAPIRenderer(BaseRenderer):
return True
def serializer_to_form_fields(self, serializer):
- field_mapping = {
- serializers.FloatField: forms.FloatField,
- serializers.IntegerField: forms.IntegerField,
- serializers.DateTimeField: forms.DateTimeField,
- serializers.DateField: forms.DateField,
- serializers.EmailField: forms.EmailField,
- serializers.RegexField: forms.RegexField,
- serializers.CharField: forms.CharField,
- serializers.ChoiceField: forms.ChoiceField,
- serializers.BooleanField: forms.BooleanField,
- serializers.PrimaryKeyRelatedField: forms.ChoiceField,
- serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
- serializers.SlugRelatedField: forms.ChoiceField,
- serializers.ManySlugRelatedField: forms.MultipleChoiceField,
- serializers.HyperlinkedRelatedField: forms.ChoiceField,
- serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField,
- serializers.FileField: forms.FileField,
- serializers.ImageField: forms.ImageField,
- }
-
fields = {}
for k, v in serializer.get_fields().items():
if getattr(v, 'read_only', True):
@@ -351,13 +331,7 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs['label'] = k
- try:
- fields[k] = field_mapping[v.__class__](**kwargs)
- except KeyError:
- if getattr(v, 'choices', None) is not None:
- fields[k] = forms.ChoiceField(**kwargs)
- else:
- fields[k] = forms.CharField(**kwargs)
+ fields[k] = v.form_field_class(**kwargs)
return fields
def get_form(self, view, method, request):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 1163bc05..cbb6b4df 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -62,7 +62,7 @@ def _get_declared_fields(bases, attrs):
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
- # in order to the correct order of fields.
+ # in order to maintain the correct order of fields.
for base in bases[::-1]:
if hasattr(base, 'base_fields'):
fields = list(base.base_fields.items()) + fields
@@ -93,19 +93,19 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
- def __init__(self, instance=None, data=None, files=None, context=None, **kwargs):
+ def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
- self.fields = copy.deepcopy(self.base_fields)
self.parent = None
self.root = None
+ self.partial = partial
self.context = context or {}
self.init_data = data
self.init_files = files
self.object = instance
- self.default_fields = self.get_default_fields()
+ self.fields = self.get_fields()
self._data = None
self._files = None
@@ -130,13 +130,15 @@ class BaseSerializer(Field):
ret = SortedDict()
# Get the explicitly declared fields
- for key, field in self.fields.items():
+ base_fields = copy.deepcopy(self.base_fields)
+ for key, field in base_fields.items():
ret[key] = field
# Set up the field
field.initialize(parent=self, field_name=key)
# Add in the default fields
- for key, val in self.default_fields.items():
+ default_fields = self.get_default_fields()
+ for key, val in default_fields.items():
if key not in ret:
ret[key] = val
@@ -183,8 +185,7 @@ class BaseSerializer(Field):
ret = self._dict_class()
ret.fields = {}
- fields = self.get_fields()
- for field_name, field in fields.items():
+ for field_name, field in self.fields.items():
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
@@ -196,9 +197,8 @@ 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()
reverted_data = {}
- for field_name, field in fields.items():
+ for field_name, field in self.fields.items():
try:
field.field_from_native(data, files, field_name, reverted_data)
except ValidationError as err:
@@ -210,10 +210,7 @@ class BaseSerializer(Field):
"""
Run `validate_<fieldname>()` and `validate()` methods on the serializer
"""
- # TODO: refactor this so we're not determining the fields again
- fields = self.get_fields()
-
- for field_name, field in fields.items():
+ for field_name, field in self.fields.items():
try:
validate_method = getattr(self, 'validate_%s' % field_name, None)
if validate_method:
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index 33ef0312..b1da5b6f 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -167,14 +167,14 @@ class TokenAuthTests(TestCase):
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
- self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.status_code, 401)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
response = client.post('/auth-token/login/',
json.dumps({'username': self.username}), 'application/json')
- self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.status_code, 401)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 804f578d..329d27a9 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -117,6 +117,18 @@ class BasicTests(TestCase):
self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
+ def test_partial_update(self):
+ msg = 'Merry New Year!'
+ partial_data = {'content': msg}
+ serializer = CommentSerializer(self.comment, data=partial_data)
+ self.assertEquals(serializer.is_valid(), False)
+ serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
+ expected = self.comment
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEquals(serializer.object, expected)
+ self.assertTrue(serializer.object is expected)
+ self.assertEquals(serializer.data['content'], msg)
+
def test_model_fields_as_expected(self):
"""
Make sure that the fields returned are the same as defined