diff --git a/README.md b/README.md index 9a12d5358..f646f957f 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 8ed6ef318..43fc15d2d 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 048c12006..19efde3c7 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 5b072827e..d1e42ec1c 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 955870d25..e0c589b23 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 c641a1b39..867b138bf 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 b29daf05a..187effb9d 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[0-9]+)$', 'snippet_detail') ) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index eddf63110..d87d20464 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 320cf2163..1d2358c6e 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 88108a8d2..48cebbc5e 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 3ac674e28..cfaacbe9a 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 5c5a86c1a..8ed7efa56 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 dae384772..ee2800a6e 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 4abce9065..44a40baf1 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 1163bc053..cbb6b4df8 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_()` 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 33ef03126..b1da5b6fb 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 804f578d4..329d27a9e 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