From d48ba1cff76ffceb1d700e9e0c6ccf518a6382da Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 05:47:27 +0800 Subject: [PATCH 01/29] turn of pagination --- docs/api-guide/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 0829589f8..f86e6ce11 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -102,7 +102,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie paginate_by_param = 'page_size' max_paginate_by = 100 -Note that using a `paginate_by` value of `None` will turn off pagination for the view. +Note that using a `paginate_by` value of `None` will turn off pagination for the view. But if you specified `PAGINATE_BY` and `PAGINATE_BY_PARAM` in your settings file then you have to set both `paginate_by` and `paginate_by_param` to a `None` value in order to turn off pagination for the view. For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. From 2d20512d259f51a5a5c2b71b20f98d24e0176f16 Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 21:10:51 +0800 Subject: [PATCH 02/29] #1390, docs, turning of pagination --- docs/api-guide/pagination.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f86e6ce11..047a09883 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -102,7 +102,8 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie paginate_by_param = 'page_size' max_paginate_by = 100 -Note that using a `paginate_by` value of `None` will turn off pagination for the view. But if you specified `PAGINATE_BY` and `PAGINATE_BY_PARAM` in your settings file then you have to set both `paginate_by` and `paginate_by_param` to a `None` value in order to turn off pagination for the view. +Note that using a `paginate_by` value of `None` will turn off pagination for the view. +Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter. For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. From d18d32669ac47178f26409f149160dc2c0c5359c Mon Sep 17 00:00:00 2001 From: tuky Date: Wed, 12 Feb 2014 18:11:18 +0100 Subject: [PATCH 03/29] remove spaces from META['HTTP_X_FORWARDED_FOR'] as throttle key memcached cannot handle spaces in keys --- rest_framework/throttling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index a946d837f..56023bdad 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -155,6 +155,8 @@ class AnonRateThrottle(SimpleRateThrottle): ident = request.META.get('HTTP_X_FORWARDED_FOR') if ident is None: ident = request.META.get('REMOTE_ADDR') + else: + ident = u''.join(ident.split()) return self.cache_format % { 'scope': self.scope, From 5e4336845fed97a819e69669ed7aa3b9bf443edb Mon Sep 17 00:00:00 2001 From: tuky Date: Fri, 14 Feb 2014 13:47:17 +0100 Subject: [PATCH 04/29] Update throttling.py python 3 u'' gone --- rest_framework/throttling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 56023bdad..c36b58bf2 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -156,7 +156,7 @@ class AnonRateThrottle(SimpleRateThrottle): if ident is None: ident = request.META.get('REMOTE_ADDR') else: - ident = u''.join(ident.split()) + ident = ''.join(ident.split()) return self.cache_format % { 'scope': self.scope, From 775013864380c1443813e1ffdfdd7e8c49ed061e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 07:47:11 +0100 Subject: [PATCH 05/29] Fixed the many to many behavior. --- rest_framework/serializers.py | 1 + .../tests/test_serializer_nested.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d479..c788dec16 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -992,6 +992,7 @@ class ModelSerializer(Serializer): if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): + [self.save_object(o) for o in object_list] setattr(obj, accessor_name, object_list) del(obj._m2m_data) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 6d69ffbd0..fcd379bc0 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -345,3 +345,31 @@ class NestedModelSerializerUpdateTests(TestCase): result = deserialize.object result.save() self.assertEqual(result.id, john.id) + + def test_creation_with_nested_many_to_many_relation(self): + class ManyToManyTargetSerializer(serializers.ModelSerializer): + class Meta: + model = models.ManyToManyTarget + + class ManyToManySourceSerializer(serializers.ModelSerializer): + targets = ManyToManyTargetSerializer(many=True, allow_add_remove=True) + class Meta: + model = models.ManyToManySource + + data = { + 'name': 'source', + 'targets': [{ + 'name': 'target1' + }, { + 'name': 'another target' + }] + } + + source_count = models.ManyToManySource.objects.count() + target_count = models.ManyToManyTarget.objects.count() + + deserialize = ManyToManySourceSerializer(data=data) + self.assertTrue(deserialize.is_valid(), deserialize.errors) + deserialize.save() + self.assertEqual(models.ManyToManySource.objects.count(), source_count + 1) + self.assertEqual(models.ManyToManyTarget.objects.count(), target_count + 2) From 04315c12af09d9b2ee1106ab31af5891833dd2f9 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Mon, 24 Mar 2014 19:25:28 +0100 Subject: [PATCH 06/29] Use help_text, verbose_name, editable attributes for related fields --- rest_framework/serializers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 88972e257..46beb6ac7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,6 +828,15 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if not model_field.editable: + kwargs['read_only'] = True + + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name + + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): From ab5082d15c04866401c6f1bc7d77d21e695f996d Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Fri, 28 Mar 2014 19:42:46 +0100 Subject: [PATCH 07/29] Do not check model_field's attributes if it is None --- rest_framework/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46beb6ac7..d7941df14 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,14 +828,14 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - if not model_field.editable: - kwargs['read_only'] = True + if not model_field.editable: + kwargs['read_only'] = True - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text return PrimaryKeyRelatedField(**kwargs) From d8bf8787923080a64842a12e3e476aff27bfa8fc Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Sun, 30 Mar 2014 11:48:17 +0200 Subject: [PATCH 08/29] Metadata for related fields -- added test case. --- rest_framework/tests/models.py | 6 ++- rest_framework/tests/test_generics.py | 73 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6c8f2342b..355e070e4 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -143,14 +143,16 @@ class ForeignKeyTarget(RESTFrameworkModel): class ForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) - target = models.ForeignKey(ForeignKeyTarget, related_name='sources') + target = models.ForeignKey(ForeignKeyTarget, related_name='sources', + verbose_name='Target object') # Nullable ForeignKey class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, - related_name='nullable_sources') + related_name='nullable_sources', + verbose_name='Optional target object') # OneToOne diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 996bd5b0e..0cadc5dee 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -5,6 +5,7 @@ from django.test import TestCase from rest_framework import generics, renderers, serializers, status from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel +from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget from rest_framework.compat import six factory = APIRequestFactory() @@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): return queryset.exclude(text='filtered out') +class FKInstanceView(generics.RetrieveUpdateDestroyAPIView): + """ + FK: example description for OPTIONS. + """ + model = ForeignKeySource + + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -407,6 +415,71 @@ class TestInstanceView(TestCase): self.assertFalse(self.objects.filter(id=999).exists()) +class TestFKInstanceView(TestCase): + def setUp(self): + """ + Create 3 BasicModel instances. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + t = ForeignKeyTarget(name=item) + t.save() + ForeignKeySource(name='source_' + item, target=t).save() + + self.objects = ForeignKeySource.objects + self.data = [ + {'id': obj.id, 'name': obj.name} + for obj in self.objects.all() + ] + self.view = FKInstanceView.as_view() + + def test_options_root_view(self): + """ + OPTIONS requests to ListCreateAPIView should return metadata + """ + request = factory.options('/999') + with self.assertNumQueries(1): + response = self.view(request, pk=999).render() + expected = { + 'name': 'Fk Instance', + 'description': 'FK: example description for OPTIONS.', + 'renders': [ + 'application/json', + 'text/html' + ], + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'actions': { + 'PUT': { + 'id': { + 'type': 'integer', + 'required': False, + 'read_only': True, + 'label': u'ID' + }, + 'name': { + 'type': 'string', + 'required': True, + 'read_only': False, + 'label': 'name', + 'max_length': 100 + }, + 'target': { + 'type': 'field', + 'required': True, + 'read_only': False, + 'label': 'Target object' + } + } + } + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + class TestOverriddenGetObject(TestCase): """ Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the From 8904f179d1bc925d52001497e92b9cd509e65bd5 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Sun, 30 Mar 2014 12:06:03 +0200 Subject: [PATCH 09/29] Stray unicode string marker removed --- rest_framework/tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 0cadc5dee..b4ae20219 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -458,7 +458,7 @@ class TestFKInstanceView(TestCase): 'type': 'integer', 'required': False, 'read_only': True, - 'label': u'ID' + 'label': 'ID' }, 'name': { 'type': 'string', From 4160bbcf72947164b1757dd6a83f514d56422dd9 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 11 Apr 2014 23:13:43 +0200 Subject: [PATCH 10/29] Fixed the reversed relation for virtual fields (generic foreign keys). --- rest_framework/serializers.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c788dec16..6a3432240 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -919,6 +919,7 @@ class ModelSerializer(Serializer): Restore the model instance. """ m2m_data = {} + virtual_m2m_data = {} related_data = {} nested_forward_relations = {} meta = self.opts.model._meta @@ -936,10 +937,16 @@ class ModelSerializer(Serializer): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many + meta.virtual_fields: + for field in meta.many_to_many: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) + # Virtual m2m relations + # This is mostly for GenericRelations + for field in meta.virtual_fields: + if field.name in attrs: + virtual_m2m_data[field.name] = attrs.pop(field.name) + # Nested forward relations - These need to be marked so we can save # them before saving the parent model instance. for field_name in attrs.keys(): @@ -964,6 +971,7 @@ class ModelSerializer(Serializer): # at the point of save. instance._related_data = related_data instance._m2m_data = m2m_data + instance._virtual_m2m_data = virtual_m2m_data instance._nested_forward_relations = nested_forward_relations return instance @@ -992,10 +1000,18 @@ class ModelSerializer(Serializer): if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): + # We need to save m2m data before linking the objects together [self.save_object(o) for o in object_list] setattr(obj, accessor_name, object_list) del(obj._m2m_data) + if getattr(obj, '_virtual_m2m_data', None): + for accessor_name, object_list in obj._virtual_m2m_data.items(): + # In case of GenericRelation, we have a FK as constraint + setattr(obj, accessor_name, object_list) + [self.save_object(o) for o in object_list] + del(obj._m2m_data) + if getattr(obj, '_related_data', None): related_fields = dict([ (field.get_accessor_name(), field) From d8cb85ef8fb0a0804d9b2c09d909ad99f69301c8 Mon Sep 17 00:00:00 2001 From: Laurent Bristiel Date: Mon, 28 Apr 2014 22:00:36 +0200 Subject: [PATCH 11/29] typo --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index fb927ea8b..7d06f246c 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -70,7 +70,7 @@ The following attributes control the basic view behavior. **Shortcuts**: -* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided. +* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided. **Pagination**: From fc44cd8d6a358ac6b5fd5155f69d032c7656ff4b Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Tue, 29 Apr 2014 21:45:57 +0200 Subject: [PATCH 12/29] Sync test result w/ new label --- rest_framework/tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index b4ae20219..4a3b67f65 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -471,7 +471,7 @@ class TestFKInstanceView(TestCase): 'type': 'field', 'required': True, 'read_only': False, - 'label': 'Target object' + 'label': 'Target' } } } From 295a4ab62d9af9ad7f74792c6543a1cf35cee2f9 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Tue, 29 Apr 2014 22:16:11 +0200 Subject: [PATCH 13/29] Added help_text to expected response in test --- rest_framework/tests/test_generics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 4a3b67f65..57d327cc6 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -471,7 +471,8 @@ class TestFKInstanceView(TestCase): 'type': 'field', 'required': True, 'read_only': False, - 'label': 'Target' + 'label': 'Target', + 'help_text': 'Target' } } } From 9f9ad964cd5c6999f365cc56267fe607feb9386a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 30 Apr 2014 22:59:43 +0200 Subject: [PATCH 14/29] Removed the former virtual field list. --- rest_framework/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 03ac78bea..131aeed31 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -930,7 +930,6 @@ class ModelSerializer(Serializer): Restore the model instance. """ m2m_data = {} - virtual_m2m_data = {} related_data = {} nested_forward_relations = {} meta = self.opts.model._meta From 5dab5e4dcf0920093377ca06544386ea598de84b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 30 Apr 2014 23:51:07 +0200 Subject: [PATCH 15/29] Deal with reversed GenericFK. --- rest_framework/serializers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 131aeed31..6146864f7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -16,7 +16,7 @@ import datetime import inspect import types from decimal import Decimal -from django.contrib.contenttypes.generic import GenericForeignKey +from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation from django.core.paginator import Page from django.db import models from django.forms import widgets @@ -932,6 +932,7 @@ class ModelSerializer(Serializer): m2m_data = {} related_data = {} nested_forward_relations = {} + generic_fk = [] meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -950,6 +951,8 @@ class ModelSerializer(Serializer): for field in meta.many_to_many + meta.virtual_fields: if isinstance(field, GenericForeignKey): continue + if isinstance(field, GenericRelation): + generic_fk.append(field.name) if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) @@ -973,6 +976,7 @@ class ModelSerializer(Serializer): # saved the model get hidden away on these # private attributes, so we can deal with them # at the point of save. + instance._generic_fk = generic_fk instance._related_data = related_data instance._m2m_data = m2m_data instance._nested_forward_relations = nested_forward_relations @@ -1003,9 +1007,13 @@ class ModelSerializer(Serializer): if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): - # We need to save m2m data before linking the objects together - [self.save_object(o) for o in object_list] - setattr(obj, accessor_name, object_list) + if accessor_name in getattr(obj, '_generic_fk', []): + # We are dealing with a reversed generic FK + setattr(obj, accessor_name, object_list) + [self.save_object(o) for o in object_list if not isinstance(o, GenericRelation)] + if accessor_name not in getattr(obj, '_generic_fk', []): + # We need to save m2m data before linking the objects together + setattr(obj, accessor_name, object_list) del(obj._m2m_data) if getattr(obj, '_related_data', None): From c15dab903d3759578449279cc034d766d362d41f Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Thu, 1 May 2014 10:18:16 +0100 Subject: [PATCH 16/29] Mark strings in AuthTokenSerializer as translatable --- rest_framework/authtoken/serializers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 60a3740e7..995f2e646 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,4 +1,6 @@ from django.contrib.auth import authenticate +from django.utils.translation import ugettext_lazy as _ + from rest_framework import serializers @@ -15,10 +17,13 @@ class AuthTokenSerializer(serializers.Serializer): if user: if not user.is_active: - raise serializers.ValidationError('User account is disabled.') + msg = _('User account is disabled.') + raise serializers.ValidationError() attrs['user'] = user return attrs else: - raise serializers.ValidationError('Unable to login with provided credentials.') + msg = _('Unable to login with provided credentials.') + raise serializers.ValidationError(msg) else: - raise serializers.ValidationError('Must include "username" and "password"') + msg = _('Must include "username" and "password"') + raise serializers.ValidationError(msg) From ccf3c508bd6750073ea3bbaefff567b92880df73 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Fri, 2 May 2014 21:58:49 +0100 Subject: [PATCH 17/29] Fix missing message in ValidationError --- rest_framework/authtoken/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 995f2e646..99e99ae3d 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -18,7 +18,7 @@ class AuthTokenSerializer(serializers.Serializer): if user: if not user.is_active: msg = _('User account is disabled.') - raise serializers.ValidationError() + raise serializers.ValidationError(msg) attrs['user'] = user return attrs else: From 4e33ff05d9aabee0a90bfb0ef8ce58a5d274b9a2 Mon Sep 17 00:00:00 2001 From: Lucian Mocanu Date: Sun, 4 May 2014 00:12:08 +0200 Subject: [PATCH 18/29] Automatically set the field name as value for the HTML `id` attribute on the rendered widget. --- rest_framework/fields.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8cdc55515..e67338499 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -154,7 +154,12 @@ class Field(object): def widget_html(self): if not self.widget: return '' - return self.widget.render(self._name, self._value) + + attrs = {} + if 'id' not in self.widget.attrs: + attrs['id'] = self._name + + return self.widget.render(self._name, self._value, attrs=attrs) def label_tag(self): return '' % (self._name, self.label) From cdc7d19034170e5d775166763e6df1220e131d35 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 14:41:10 +0200 Subject: [PATCH 19/29] Added missing "to" word --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 979c4a3e3..dbe693ed5 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -104,7 +104,7 @@ Don't forget to sync the database for the first time. ## Creating a Serializer class -The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. +The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. from django.forms import widgets from rest_framework import serializers From 05fc974dc961de6d4e11b7baf51f7b3791c06711 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 14:44:54 +0200 Subject: [PATCH 20/29] Added missing "the" word --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index dbe693ed5..55b194576 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -143,7 +143,7 @@ The first thing we need to get started on our Web API is to provide a way of ser # Create new instance return Snippet(**attrs) -The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. +The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial. From 9e3ba939e152c2eb96d3c9b4460a3d4ce76931cd Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 20:27:44 +0200 Subject: [PATCH 21/29] Removed superfluous "./"s --- docs/tutorial/4-authentication-and-permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 432371f34..491df1608 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -44,11 +44,11 @@ When that's all done we'll need to update our database tables. Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. rm tmp.db - python ./manage.py syncdb + python manage.py syncdb You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. - python ./manage.py createsuperuser + python manage.py createsuperuser ## Adding endpoints for our User models From 4bdb6787c1fa3e49fcb586491816e8c151329921 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 6 May 2014 11:33:13 +0200 Subject: [PATCH 22/29] Added an test on updates through many to many field. --- .../tests/test_serializer_nested.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index fcd379bc0..5525ceffe 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -373,3 +373,46 @@ class NestedModelSerializerUpdateTests(TestCase): deserialize.save() self.assertEqual(models.ManyToManySource.objects.count(), source_count + 1) self.assertEqual(models.ManyToManyTarget.objects.count(), target_count + 2) + + def test_update_with_nested_many_to_many_relation(self): + class ManyToManyTargetSerializer(serializers.ModelSerializer): + class Meta: + model = models.ManyToManyTarget + + class ManyToManySourceSerializer(serializers.ModelSerializer): + targets = ManyToManyTargetSerializer(many=True, allow_add_remove=True) + class Meta: + model = models.ManyToManySource + + source = models.ManyToManySource.objects.create(name='source') + target1 = models.ManyToManyTarget.objects.create(name='target1') + target2 = models.ManyToManyTarget.objects.create(name='target2') + source.targets = [target1, target2] + + data = { + 'id': source.id, + 'name': source.name + '0', + 'targets': [{ + 'id': target1.id, + 'name': target1.name + '1', + }, { + 'id': target2.id, + 'name': target2.name + '2', + }] + } + + source_count = models.ManyToManySource.objects.count() + target_count = models.ManyToManyTarget.objects.count() + + deserialize = ManyToManySourceSerializer(data=data, instance=source) + self.assertTrue(deserialize.is_valid(), deserialize.errors) + deserialize.save() + self.assertEqual(models.ManyToManySource.objects.count(), source_count) + self.assertEqual(models.ManyToManyTarget.objects.count(), target_count) + + # Were the models updated ? + self.assertEqual(source.name, 'source0') + alt_target1 = models.ManyToManyTarget.objects.get(id=target1.id) + self.assertEqual(alt_target1.name, target1.name + '1') + alt_target2 = models.ManyToManyTarget.objects.get(id=target2.id) + self.assertEqual(alt_target2.name, target2.name + '2') From b90ebce00bdc8f3a0232da7e123f5e0d60f38acc Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 6 May 2014 12:23:26 +0200 Subject: [PATCH 23/29] Failing test case: an item that wasn't in the relation before is created although it already exist. --- rest_framework/tests/test_serializer_nested.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 5525ceffe..65a6d5d46 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -387,7 +387,7 @@ class NestedModelSerializerUpdateTests(TestCase): source = models.ManyToManySource.objects.create(name='source') target1 = models.ManyToManyTarget.objects.create(name='target1') target2 = models.ManyToManyTarget.objects.create(name='target2') - source.targets = [target1, target2] + source.targets = [target1] data = { 'id': source.id, From 708c7b3a816c3c2df7847695044ef852dc89e72c Mon Sep 17 00:00:00 2001 From: Lucian Mocanu Date: Tue, 6 May 2014 14:17:51 +0200 Subject: [PATCH 24/29] Added test case to check if the proper attributes are set on html widgets. --- rest_framework/tests/test_fields.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index e127feef9..03f79cf4d 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -4,6 +4,7 @@ General serializer field tests. from __future__ import unicode_literals import datetime +import re from decimal import Decimal from uuid import uuid4 from django.core import validators @@ -103,6 +104,16 @@ class BasicFieldTests(TestCase): keys = list(field.to_native(ret).keys()) self.assertEqual(keys, ['c', 'b', 'a', 'z']) + def test_widget_html_attributes(self): + """ + Make sure widget_html() renders the correct attributes + """ + r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?') + form = TimeFieldModelSerializer().data + attributes = r.findall(form.fields['clock'].widget_html()) + self.assertIn(('name', 'clock'), attributes) + self.assertIn(('id', 'clock'), attributes) + class DateFieldTest(TestCase): """ From 9dc5e15e5afaf806378bb52ea2134f8dec2af386 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 13:00:41 +0200 Subject: [PATCH 25/29] Added missing "the" word --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 870632f1b..0a7c33639 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single queryset = User.objects.all() serializer_class = UserSerializer -Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes. +Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes. Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. From beb7253a961870a37833b7df6d1dfd3e8c1db778 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 15:06:38 +0200 Subject: [PATCH 26/29] Removed unnecessary "that" --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 0a7c33639..dad71601a 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -85,7 +85,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view. -Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual. +Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual. urlpatterns = format_suffix_patterns(patterns('snippets.views', url(r'^$', 'api_root'), From e033a0b9a0d655666385cd9831c7f1279573b47f Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 15:07:40 +0200 Subject: [PATCH 27/29] Replaced singular "is" by plural "are" --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index dad71601a..04b42f2e7 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -138,7 +138,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam ## Onwards and upwards -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: +We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: * Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. * Join the [REST framework discussion group][group], and help build the community. From 11115fde9cc8f70dfd85ce937893d67fd061f3c1 Mon Sep 17 00:00:00 2001 From: Elliott Date: Wed, 7 May 2014 11:37:20 -0700 Subject: [PATCH 28/29] Add colon to time zone offset in readable_datetime_formats --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 68b956822..9f53a0000 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -62,7 +62,7 @@ def get_component(obj, attr_name): def readable_datetime_formats(formats): format = ', '.join(formats).replace(ISO_8601, - 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') + 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]') return humanize_strptime(format) From 0ff474d7c4882a31f8fb133caa82d0368b0406c2 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 8 May 2014 11:20:03 +0200 Subject: [PATCH 29/29] Updated failing test from #1575 --- rest_framework/tests/test_fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index e127feef9..894b5b3cf 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -312,7 +312,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"]) else: self.fail("ValidationError was not properly raised") @@ -326,7 +326,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"]) else: self.fail("ValidationError was not properly raised")