From 3f79a9a3d3e7692d90476f8a6907957b47aab821 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2013 22:39:45 +0000 Subject: [PATCH 0001/1652] one-one writable nested modelserializers --- rest_framework/serializers.py | 11 +- rest_framework/tests/relations_nested.py | 190 ++++++++++++----------- 2 files changed, 110 insertions(+), 91 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6aca2f574..26c34044c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -753,7 +753,16 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - setattr(obj, accessor_name, related) + if related is None: + previous = getattr(obj, accessor_name, related) + if previous: + previous.delete() + elif isinstance(related, models.Model): + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) + else: + setattr(obj, accessor_name, related) del(obj._related_data) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index a125ba656..4592e5593 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -1,115 +1,125 @@ from __future__ import unicode_literals +from django.db import models from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource -class ForeignKeySourceSerializer(serializers.ModelSerializer): +class OneToOneTarget(models.Model): + name = models.CharField(max_length=100) + + +class OneToOneTargetSource(models.Model): + name = models.CharField(max_length=100) + target = models.OneToOneField(OneToOneTarget, null=True, blank=True, + related_name='target_source') + + +class OneToOneSource(models.Model): + name = models.CharField(max_length=100) + target_source = models.OneToOneField(OneToOneTargetSource, related_name='source') + + +class OneToOneSourceSerializer(serializers.ModelSerializer): class Meta: - depth = 1 - model = ForeignKeySource + model = OneToOneSource + exclude = ('target_source', ) -class FlatForeignKeySourceSerializer(serializers.ModelSerializer): - class Meta: - model = ForeignKeySource - - -class ForeignKeyTargetSerializer(serializers.ModelSerializer): - sources = FlatForeignKeySourceSerializer(many=True) +class OneToOneTargetSourceSerializer(serializers.ModelSerializer): + source = OneToOneSourceSerializer() class Meta: - model = ForeignKeyTarget + model = OneToOneTargetSource + exclude = ('target', ) -class NullableForeignKeySourceSerializer(serializers.ModelSerializer): - class Meta: - depth = 1 - model = NullableForeignKeySource - - -class NullableOneToOneSourceSerializer(serializers.ModelSerializer): - class Meta: - model = NullableOneToOneSource - - -class NullableOneToOneTargetSerializer(serializers.ModelSerializer): - nullable_source = NullableOneToOneSourceSerializer() +class OneToOneTargetSerializer(serializers.ModelSerializer): + target_source = OneToOneTargetSourceSerializer() class Meta: model = OneToOneTarget -class ReverseForeignKeyTests(TestCase): +class NestedOneToOneTests(TestCase): def setUp(self): - target = ForeignKeyTarget(name='target-1') - target.save() - new_target = ForeignKeyTarget(name='target-2') - new_target.save() for idx in range(1, 4): - source = ForeignKeySource(name='source-%d' % idx, target=target) + target = OneToOneTarget(name='target-%d' % idx) + target.save() + target_source = OneToOneTargetSource(name='target-source-%d' % idx, target=target) + target_source.save() + source = OneToOneSource(name='source-%d' % idx, target_source=target_source) source.save() - def test_foreign_key_retrieve(self): - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, - {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, - {'id': 3, 'name': 'source-3', 'target': {'id': 1, 'name': 'target-1'}}, - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_foreign_key_retrieve(self): - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [ - {'id': 1, 'name': 'source-1', 'target': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': 1}, - ]}, - {'id': 2, 'name': 'target-2', 'sources': [ - ]} - ] - self.assertEqual(serializer.data, expected) - - -class NestedNullableForeignKeyTests(TestCase): - def setUp(self): - target = ForeignKeyTarget(name='target-1') - target.save() - for idx in range(1, 4): - if idx == 3: - target = None - source = NullableForeignKeySource(name='source-%d' % idx, target=target) - source.save() - - def test_foreign_key_retrieve_with_null(self): - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, - {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, - {'id': 3, 'name': 'source-3', 'target': None}, - ] - self.assertEqual(serializer.data, expected) - - -class NestedNullableOneToOneTests(TestCase): - def setUp(self): - target = OneToOneTarget(name='target-1') - target.save() - new_target = OneToOneTarget(name='target-2') - new_target.save() - source = NullableOneToOneSource(name='source-1', target=target) - source.save() - - def test_reverse_foreign_key_retrieve_with_null(self): + def test_one_to_one_retrieve(self): queryset = OneToOneTarget.objects.all() - serializer = NullableOneToOneTargetSerializer(queryset, many=True) + serializer = OneToOneTargetSerializer(queryset) expected = [ - {'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}}, - {'id': 2, 'name': 'target-2', 'nullable_source': None}, + {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, + {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, + {'id': 3, 'name': 'target-3', 'target_source': {'id': 3, 'name': 'target-source-3', 'source': {'id': 3, 'name': 'source-3'}}} + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_one_create(self): + data = {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4, 'name': 'source-4'}}} + serializer = OneToOneTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'target-4') + + # Ensure (target 4, target_source 4, source 4) are added, and + # everything else is as expected. + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, + {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, + {'id': 3, 'name': 'target-3', 'target_source': {'id': 3, 'name': 'target-source-3', 'source': {'id': 3, 'name': 'source-3'}}}, + {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4, 'name': 'source-4'}}} + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_one_create_with_invalid_data(self): + data = {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4}}} + serializer = OneToOneTargetSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'target_source': [{'source': [{'name': ['This field is required.']}]}]}) + + def test_one_to_one_update(self): + data = {'id': 3, 'name': 'target-3-updated', 'target_source': {'id': 3, 'name': 'target-source-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}} + instance = OneToOneTarget.objects.get(pk=3) + serializer = OneToOneTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'target-3-updated') + + # Ensure (target 3, target_source 3, source 3) are updated, + # and everything else is as expected. + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, + {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, + {'id': 3, 'name': 'target-3-updated', 'target_source': {'id': 3, 'name': 'target-source-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}} + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_one_delete(self): + data = {'id': 3, 'name': 'target-3', 'target_source': None} + instance = OneToOneTarget.objects.get(pk=3) + serializer = OneToOneTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + serializer.save() + + # Ensure (target_source 3, source 3) are deleted, + # and everything else is as expected. + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, + {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, + {'id': 3, 'name': 'target-3', 'target_source': None} ] self.assertEqual(serializer.data, expected) From d97e72cdb2f4fcc5aa2c19527a2b2ff11cf784bb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 25 Mar 2013 17:28:23 +0000 Subject: [PATCH 0002/1652] Cleanup one-one nested tests and implementation --- rest_framework/serializers.py | 37 ++++- rest_framework/tests/relations_nested.py | 194 +++++++++++++++-------- 2 files changed, 161 insertions(+), 70 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 26c34044c..668bcc49d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -667,9 +667,12 @@ class ModelSerializer(Serializer): cls = self.opts.model opts = get_concrete_model(cls)._meta exclusions = [field.name for field in opts.fields + opts.many_to_many] + for field_name, field in self.fields.items(): field_name = field.source or field_name - if field_name in exclusions and not field.read_only: + if field_name in exclusions \ + and not field.read_only \ + and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -695,6 +698,7 @@ class ModelSerializer(Serializer): """ m2m_data = {} related_data = {} + nested_forward_relations = {} meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -714,6 +718,12 @@ class ModelSerializer(Serializer): if field.name in attrs: 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(): + if isinstance(self.fields.get(field_name, None), Serializer): + nested_forward_relations[field_name] = attrs[field_name] + # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -729,6 +739,7 @@ class ModelSerializer(Serializer): # at the point of save. instance._related_data = related_data instance._m2m_data = m2m_data + instance._nested_forward_relations = nested_forward_relations return instance @@ -744,6 +755,13 @@ class ModelSerializer(Serializer): """ Save the deserialized object and return it. """ + if getattr(obj, '_nested_forward_relations', None): + # Nested relationships need to be saved before we can save the + # parent instance. + for field_name, sub_object in obj._nested_forward_relations.items(): + self.save_object(sub_object) + setattr(obj, field_name, sub_object) + obj.save(**kwargs) if getattr(obj, '_m2m_data', None): @@ -753,15 +771,22 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - if related is None: - previous = getattr(obj, accessor_name, related) - if previous: - previous.delete() - elif isinstance(related, models.Model): + field = self.fields.get(accessor_name, None) + if isinstance(field, Serializer): + # TODO: Following will be needed for reverse FK + # if field.many: + # # Nested reverse fk relationship + # for related_item in related: + # fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + # setattr(related_item, fk_field, obj) + # self.save_object(related_item) + # else: + # Nested reverse one-one relationship fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name setattr(related, fk_field, obj) self.save_object(related) else: + # Reverse FK or reverse one-one setattr(obj, accessor_name, related) del(obj._related_data) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 4592e5593..e7af65651 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -8,61 +8,46 @@ class OneToOneTarget(models.Model): name = models.CharField(max_length=100) -class OneToOneTargetSource(models.Model): - name = models.CharField(max_length=100) - target = models.OneToOneField(OneToOneTarget, null=True, blank=True, - related_name='target_source') - - class OneToOneSource(models.Model): name = models.CharField(max_length=100) - target_source = models.OneToOneField(OneToOneTargetSource, related_name='source') + target = models.OneToOneField(OneToOneTarget, related_name='source') -class OneToOneSourceSerializer(serializers.ModelSerializer): - class Meta: - model = OneToOneSource - exclude = ('target_source', ) - - -class OneToOneTargetSourceSerializer(serializers.ModelSerializer): - source = OneToOneSourceSerializer() - - class Meta: - model = OneToOneTargetSource - exclude = ('target', ) - - -class OneToOneTargetSerializer(serializers.ModelSerializer): - target_source = OneToOneTargetSourceSerializer() - - class Meta: - model = OneToOneTarget - - -class NestedOneToOneTests(TestCase): +class ReverseNestedOneToOneTests(TestCase): def setUp(self): + class OneToOneSourceSerializer(serializers.ModelSerializer): + class Meta: + model = OneToOneSource + fields = ('id', 'name') + + class OneToOneTargetSerializer(serializers.ModelSerializer): + source = OneToOneSourceSerializer() + + class Meta: + model = OneToOneTarget + fields = ('id', 'name', 'source') + + self.Serializer = OneToOneTargetSerializer + for idx in range(1, 4): target = OneToOneTarget(name='target-%d' % idx) target.save() - target_source = OneToOneTargetSource(name='target-source-%d' % idx, target=target) - target_source.save() - source = OneToOneSource(name='source-%d' % idx, target_source=target_source) + source = OneToOneSource(name='source-%d' % idx, target=target) source.save() def test_one_to_one_retrieve(self): queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) + serializer = self.Serializer(queryset) expected = [ - {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, - {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, - {'id': 3, 'name': 'target-3', 'target_source': {'id': 3, 'name': 'target-source-3', 'source': {'id': 3, 'name': 'source-3'}}} + {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, + {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, + {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}} ] self.assertEqual(serializer.data, expected) def test_one_to_one_create(self): - data = {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4, 'name': 'source-4'}}} - serializer = OneToOneTargetSerializer(data=data) + data = {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}} + serializer = self.Serializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEqual(serializer.data, data) @@ -71,25 +56,25 @@ class NestedOneToOneTests(TestCase): # Ensure (target 4, target_source 4, source 4) are added, and # everything else is as expected. queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) + serializer = self.Serializer(queryset) expected = [ - {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, - {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, - {'id': 3, 'name': 'target-3', 'target_source': {'id': 3, 'name': 'target-source-3', 'source': {'id': 3, 'name': 'source-3'}}}, - {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4, 'name': 'source-4'}}} + {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, + {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, + {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}}, + {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}} ] self.assertEqual(serializer.data, expected) def test_one_to_one_create_with_invalid_data(self): - data = {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4}}} - serializer = OneToOneTargetSerializer(data=data) + data = {'id': 4, 'name': 'target-4', 'source': {'id': 4}} + serializer = self.Serializer(data=data) self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target_source': [{'source': [{'name': ['This field is required.']}]}]}) + self.assertEqual(serializer.errors, {'source': [{'name': ['This field is required.']}]}) def test_one_to_one_update(self): - data = {'id': 3, 'name': 'target-3-updated', 'target_source': {'id': 3, 'name': 'target-source-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}} + data = {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}} instance = OneToOneTarget.objects.get(pk=3) - serializer = OneToOneTargetSerializer(instance, data=data) + serializer = self.Serializer(instance, data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEqual(serializer.data, data) @@ -98,28 +83,109 @@ class NestedOneToOneTests(TestCase): # Ensure (target 3, target_source 3, source 3) are updated, # and everything else is as expected. queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) + serializer = self.Serializer(queryset) expected = [ - {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, - {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, - {'id': 3, 'name': 'target-3-updated', 'target_source': {'id': 3, 'name': 'target-source-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}} + {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, + {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, + {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}} ] self.assertEqual(serializer.data, expected) - def test_one_to_one_delete(self): - data = {'id': 3, 'name': 'target-3', 'target_source': None} - instance = OneToOneTarget.objects.get(pk=3) - serializer = OneToOneTargetSerializer(instance, data=data) + +class ForwardNestedOneToOneTests(TestCase): + def setUp(self): + class OneToOneTargetSerializer(serializers.ModelSerializer): + class Meta: + model = OneToOneTarget + fields = ('id', 'name') + + class OneToOneSourceSerializer(serializers.ModelSerializer): + target = OneToOneTargetSerializer() + + class Meta: + model = OneToOneSource + fields = ('id', 'name', 'target') + + self.Serializer = OneToOneSourceSerializer + + for idx in range(1, 4): + target = OneToOneTarget(name='target-%d' % idx) + target.save() + source = OneToOneSource(name='source-%d' % idx, target=target) + source.save() + + def test_one_to_one_retrieve(self): + queryset = OneToOneSource.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, + {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}} + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_one_create(self): + data = {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}} + serializer = self.Serializer(data=data) self.assertTrue(serializer.is_valid()) - serializer.save() + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'source-4') - # Ensure (target_source 3, source 3) are deleted, - # and everything else is as expected. - queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) + # Ensure (target 4, target_source 4, source 4) are added, and + # everything else is as expected. + queryset = OneToOneSource.objects.all() + serializer = self.Serializer(queryset) expected = [ - {'id': 1, 'name': 'target-1', 'target_source': {'id': 1, 'name': 'target-source-1', 'source': {'id': 1, 'name': 'source-1'}}}, - {'id': 2, 'name': 'target-2', 'target_source': {'id': 2, 'name': 'target-source-2', 'source': {'id': 2, 'name': 'source-2'}}}, - {'id': 3, 'name': 'target-3', 'target_source': None} + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, + {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}}, + {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}} ] self.assertEqual(serializer.data, expected) + + def test_one_to_one_create_with_invalid_data(self): + data = {'id': 4, 'name': 'source-4', 'target': {'id': 4}} + serializer = self.Serializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'target': [{'name': ['This field is required.']}]}) + + def test_one_to_one_update(self): + data = {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}} + instance = OneToOneSource.objects.get(pk=3) + serializer = self.Serializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'source-3-updated') + + # Ensure (target 3, target_source 3, source 3) are updated, + # and everything else is as expected. + queryset = OneToOneSource.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, + {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}} + ] + self.assertEqual(serializer.data, expected) + + + # TODO: Nullable 1-1 tests + # def test_one_to_one_delete(self): + # data = {'id': 3, 'name': 'target-3', 'target_source': None} + # instance = OneToOneTarget.objects.get(pk=3) + # serializer = self.Serializer(instance, data=data) + # self.assertTrue(serializer.is_valid()) + # serializer.save() + + # # Ensure (target_source 3, source 3) are deleted, + # # and everything else is as expected. + # queryset = OneToOneTarget.objects.all() + # serializer = self.Serializer(queryset) + # expected = [ + # {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, + # {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, + # {'id': 3, 'name': 'target-3', 'source': None} + # ] + # self.assertEqual(serializer.data, expected) From 73efa96de983fc644328d2fc498651aa917a2272 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sat, 6 Apr 2013 08:43:21 -0700 Subject: [PATCH 0003/1652] one-many writable nested modelserializer support --- rest_framework/serializers.py | 56 +++++++---- rest_framework/tests/relations_nested.py | 98 +++++++++++++++++++ .../tests/serializer_bulk_update.py | 6 +- 3 files changed, 139 insertions(+), 21 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 668bcc49d..73cad00f6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -130,14 +130,14 @@ class BaseSerializer(WritableField): def __init__(self, instance=None, data=None, files=None, context=None, partial=False, many=None, - allow_delete=False, **kwargs): + allow_add_remove=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial self.many = many - self.allow_delete = allow_delete + self.allow_add_remove = allow_add_remove self.context = context or {} @@ -154,8 +154,8 @@ class BaseSerializer(WritableField): if many and instance is not None and not hasattr(instance, '__iter__'): raise ValueError('instance should be a queryset or other iterable with many=True') - if allow_delete and not many: - raise ValueError('allow_delete should only be used for bulk updates, but you have not set many=True') + if allow_add_remove and not many: + raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True') ##### # Methods to determine which fields to use when (de)serializing objects. @@ -288,8 +288,15 @@ class BaseSerializer(WritableField): You should override this method to control how deserialized objects are instantiated. """ + removed_relations = [] + + # Deleted related objects + if self._deleted: + removed_relations = list(self._deleted) + if instance is not None: instance.update(attrs) + instance._removed_relations = removed_relations return instance return attrs @@ -377,6 +384,7 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj if value in (None, ''): into[(self.source or field_name)] = None @@ -386,7 +394,8 @@ class BaseSerializer(WritableField): 'data': value, 'context': self.context, 'partial': self.partial, - 'many': self.many + 'many': self.many, + 'allow_add_remove': self.allow_add_remove } serializer = self.__class__(**kwargs) @@ -496,6 +505,9 @@ class BaseSerializer(WritableField): def save_object(self, obj, **kwargs): obj.save(**kwargs) + if self.allow_add_remove and hasattr(obj, '_removed_relations'): + [self.delete_object(item) for item in obj._removed_relations] + def delete_object(self, obj): obj.delete() @@ -508,7 +520,7 @@ class BaseSerializer(WritableField): else: self.save_object(self.object, **kwargs) - if self.allow_delete and self._deleted: + if self.allow_add_remove and self._deleted: [self.delete_object(item) for item in self._deleted] return self.object @@ -699,6 +711,7 @@ class ModelSerializer(Serializer): m2m_data = {} related_data = {} nested_forward_relations = {} + removed_relations = [] meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -724,6 +737,10 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] + # Deleted related objects + if self._deleted: + removed_relations = list(self._deleted) + # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -740,6 +757,7 @@ class ModelSerializer(Serializer): instance._related_data = related_data instance._m2m_data = m2m_data instance._nested_forward_relations = nested_forward_relations + instance._removed_relations = removed_relations return instance @@ -764,6 +782,9 @@ class ModelSerializer(Serializer): obj.save(**kwargs) + if self.allow_add_remove and hasattr(obj, '_removed_relations'): + [self.delete_object(item) for item in obj._removed_relations] + if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): setattr(obj, accessor_name, object_list) @@ -773,18 +794,17 @@ class ModelSerializer(Serializer): for accessor_name, related in obj._related_data.items(): field = self.fields.get(accessor_name, None) if isinstance(field, Serializer): - # TODO: Following will be needed for reverse FK - # if field.many: - # # Nested reverse fk relationship - # for related_item in related: - # fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - # setattr(related_item, fk_field, obj) - # self.save_object(related_item) - # else: - # Nested reverse one-one relationship - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related, fk_field, obj) - self.save_object(related) + if field.many: + # Nested reverse fk relationship + for related_item in related: + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related_item, fk_field, obj) + self.save_object(related_item) + else: + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) else: # Reverse FK or reverse one-one setattr(obj, accessor_name, related) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index e7af65651..20683d4a6 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -13,6 +13,15 @@ class OneToOneSource(models.Model): target = models.OneToOneField(OneToOneTarget, related_name='source') +class OneToManyTarget(models.Model): + name = models.CharField(max_length=100) + + +class OneToManySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(OneToManyTarget, related_name='sources') + + class ReverseNestedOneToOneTests(TestCase): def setUp(self): class OneToOneSourceSerializer(serializers.ModelSerializer): @@ -189,3 +198,92 @@ class ForwardNestedOneToOneTests(TestCase): # {'id': 3, 'name': 'target-3', 'source': None} # ] # self.assertEqual(serializer.data, expected) + + +class ReverseNestedOneToManyTests(TestCase): + def setUp(self): + class OneToManySourceSerializer(serializers.ModelSerializer): + class Meta: + model = OneToManySource + fields = ('id', 'name') + + class OneToManyTargetSerializer(serializers.ModelSerializer): + sources = OneToManySourceSerializer(many=True, allow_add_remove=True) + + class Meta: + model = OneToManyTarget + fields = ('id', 'name', 'sources') + + self.Serializer = OneToManyTargetSerializer + + target = OneToManyTarget(name='target-1') + target.save() + for idx in range(1, 4): + source = OneToManySource(name='source-%d' % idx, target=target) + source.save() + + def test_one_to_many_retrieve(self): + queryset = OneToManyTarget.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}]}, + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_many_create(self): + data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}, + {'id': 4, 'name': 'source-4'}]} + instance = OneToManyTarget.objects.get(pk=1) + serializer = self.Serializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'target-1') + + # Ensure source 4 is added, and everything else is as + # expected. + queryset = OneToManyTarget.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}, + {'id': 4, 'name': 'source-4'}]} + ] + self.assertEqual(serializer.data, expected) + + def test_one_to_many_create_with_invalid_data(self): + data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}, + {'id': 4}]} + serializer = self.Serializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'sources': [{}, {}, {}, {'name': ['This field is required.']}]}) + + def test_one_to_many_update(self): + data = {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}]} + instance = OneToManyTarget.objects.get(pk=1) + serializer = self.Serializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'target-1-updated') + + # Ensure (target 1, source 1) are updated, + # and everything else is as expected. + queryset = OneToManyTarget.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, + {'id': 2, 'name': 'source-2'}, + {'id': 3, 'name': 'source-3'}]} + + ] + self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/serializer_bulk_update.py b/rest_framework/tests/serializer_bulk_update.py index afc1a1a9f..5328e7331 100644 --- a/rest_framework/tests/serializer_bulk_update.py +++ b/rest_framework/tests/serializer_bulk_update.py @@ -201,7 +201,7 @@ class BulkUpdateSerializerTests(TestCase): 'author': 'Haruki Murakami' } ] - serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True) + serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.data, data) serializer.save() @@ -223,7 +223,7 @@ class BulkUpdateSerializerTests(TestCase): 'author': 'Haruki Murakami' } ] - serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True) + serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.data, data) serializer.save() @@ -249,6 +249,6 @@ class BulkUpdateSerializerTests(TestCase): {}, {'id': ['Enter a whole number.']} ] - serializer = self.BookSerializer(self.books(), data=data, many=True, allow_delete=True) + serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True) self.assertEqual(serializer.is_valid(), False) self.assertEqual(serializer.errors, expected_errors) From bda25479aa7e73c90bc77b7c7219eaa411af138e Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Wed, 10 Apr 2013 08:44:54 -0700 Subject: [PATCH 0004/1652] Update docs with allow_add_remove --- docs/api-guide/serializers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 42e81cad5..aeb33916e 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -244,15 +244,15 @@ This allows you to write views that update or create multiple items when a `PUT` Bulk updates will update any instances that already exist, and create new instances for data items that do not have a corresponding instance. -When performing a bulk update you may want any items that are not present in the incoming data to be deleted. To do so, pass `allow_delete=True` to the serializer. +When performing a bulk update you may want any items that are not present in the incoming data to be deleted. To do so, pass `allow_add_remove=True` to the serializer. - serializer = BookSerializer(queryset, data=data, many=True, allow_delete=True) + serializer = BookSerializer(queryset, data=data, many=True, allow_add_remove=True) serializer.is_valid() # True serializer.save() # `.save()` will be called on each updated or newly created instance. # `.delete()` will be called on any other items in the `queryset`. -Passing `allow_delete=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating any objects found in the incoming data. +Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating any objects found in the incoming data. #### How identity is determined when performing bulk updates From fdc5cc3d81679d30cd20acf063dc7dc74ad17d7a Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Thu, 18 Apr 2013 10:28:20 -0700 Subject: [PATCH 0005/1652] Fix model serializer nestesd delete behavior --- rest_framework/serializers.py | 40 +++++++++--------------- rest_framework/tests/relations_nested.py | 19 +++++++++++ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f0f11a4c..78c455481 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -20,6 +20,9 @@ from rest_framework.relations import * from rest_framework.fields import * +class RelationsList(list): + _deleted = [] + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -149,7 +152,6 @@ class BaseSerializer(WritableField): self._data = None self._files = None self._errors = None - self._deleted = None if many and instance is not None and not hasattr(instance, '__iter__'): raise ValueError('instance should be a queryset or other iterable with many=True') @@ -288,15 +290,8 @@ class BaseSerializer(WritableField): You should override this method to control how deserialized objects are instantiated. """ - removed_relations = [] - - # Deleted related objects - if self._deleted: - removed_relations = list(self._deleted) - if instance is not None: instance.update(attrs) - instance._removed_relations = removed_relations return instance return attrs @@ -438,7 +433,7 @@ class BaseSerializer(WritableField): PendingDeprecationWarning, stacklevel=3) if many: - ret = [] + ret = RelationsList() errors = [] update = self.object is not None @@ -466,7 +461,7 @@ class BaseSerializer(WritableField): errors.append(self._errors) if update: - self._deleted = identity_to_objects.values() + ret._deleted = identity_to_objects.values() self._errors = any(errors) and errors or [] else: @@ -509,9 +504,6 @@ class BaseSerializer(WritableField): def save_object(self, obj, **kwargs): obj.save(**kwargs) - if self.allow_add_remove and hasattr(obj, '_removed_relations'): - [self.delete_object(item) for item in obj._removed_relations] - def delete_object(self, obj): obj.delete() @@ -521,11 +513,11 @@ class BaseSerializer(WritableField): """ if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] - else: - self.save_object(self.object, **kwargs) - if self.allow_add_remove and self._deleted: - [self.delete_object(item) for item in self._deleted] + if self.allow_add_remove and self.object._deleted: + [self.delete_object(item) for item in self.object._deleted] + else: + self.save_object(self.object, **kwargs) return self.object @@ -715,7 +707,6 @@ class ModelSerializer(Serializer): m2m_data = {} related_data = {} nested_forward_relations = {} - removed_relations = [] meta = self.opts.model._meta # Reverse fk or one-to-one relations @@ -741,10 +732,6 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] - # Deleted related objects - if self._deleted: - removed_relations = list(self._deleted) - # Update an existing instance... if instance is not None: for key, val in attrs.items(): @@ -761,7 +748,6 @@ class ModelSerializer(Serializer): instance._related_data = related_data instance._m2m_data = m2m_data instance._nested_forward_relations = nested_forward_relations - instance._removed_relations = removed_relations return instance @@ -786,9 +772,6 @@ class ModelSerializer(Serializer): obj.save(**kwargs) - if self.allow_add_remove and hasattr(obj, '_removed_relations'): - [self.delete_object(item) for item in obj._removed_relations] - if getattr(obj, '_m2m_data', None): for accessor_name, object_list in obj._m2m_data.items(): setattr(obj, accessor_name, object_list) @@ -804,6 +787,11 @@ class ModelSerializer(Serializer): fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) + + # Delete any removed objects + if field.allow_add_remove and related._deleted: + [self.delete_object(item) for item in related._deleted] + else: # Nested reverse one-one relationship fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 20683d4a6..22c98e7ff 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -287,3 +287,22 @@ class ReverseNestedOneToManyTests(TestCase): ] self.assertEqual(serializer.data, expected) + + def test_one_to_many_delete(self): + data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 3, 'name': 'source-3'}]} + instance = OneToManyTarget.objects.get(pk=1) + serializer = self.Serializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + serializer.save() + + # Ensure source 2 is deleted, and everything else is as + # expected. + queryset = OneToManyTarget.objects.all() + serializer = self.Serializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, + {'id': 3, 'name': 'source-3'}]} + + ] + self.assertEqual(serializer.data, expected) From 7e0a93f0eefead25f0e9b6615675f394af3a4ba0 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 19 Apr 2013 10:46:57 -0700 Subject: [PATCH 0006/1652] Don't use field when saving related data --- rest_framework/serializers.py | 36 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 78c455481..b39cb8105 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -460,7 +460,7 @@ class BaseSerializer(WritableField): ret.append(self.from_native(item, None)) errors.append(self._errors) - if update: + if update and self.allow_add_remove: ret._deleted = identity_to_objects.values() self._errors = any(errors) and errors or [] @@ -514,7 +514,7 @@ class BaseSerializer(WritableField): if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] - if self.allow_add_remove and self.object._deleted: + if self.object._deleted: [self.delete_object(item) for item in self.object._deleted] else: self.save_object(self.object, **kwargs) @@ -779,24 +779,22 @@ class ModelSerializer(Serializer): if getattr(obj, '_related_data', None): for accessor_name, related in obj._related_data.items(): - field = self.fields.get(accessor_name, None) - if isinstance(field, Serializer): - if field.many: - # Nested reverse fk relationship - for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related_item, fk_field, obj) - self.save_object(related_item) - - # Delete any removed objects - if field.allow_add_remove and related._deleted: - [self.delete_object(item) for item in related._deleted] - - else: - # Nested reverse one-one relationship + if isinstance(related, RelationsList): + # Nested reverse fk relationship + for related_item in related: fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name - setattr(related, fk_field, obj) - self.save_object(related) + setattr(related_item, fk_field, obj) + self.save_object(related_item) + + # Delete any removed objects + if related._deleted: + [self.delete_object(item) for item in related._deleted] + + elif isinstance(related, models.Model): + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) else: # Reverse FK or reverse one-one setattr(obj, accessor_name, related) From 14482a966168a98d43099d00c163d1c8c3b6471b Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Wed, 8 May 2013 22:44:23 -0700 Subject: [PATCH 0007/1652] Fix deprecation warnings in relations_nested tests --- rest_framework/tests/relations_nested.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 22c98e7ff..8325580f6 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -46,7 +46,7 @@ class ReverseNestedOneToOneTests(TestCase): def test_one_to_one_retrieve(self): queryset = OneToOneTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, @@ -65,7 +65,7 @@ class ReverseNestedOneToOneTests(TestCase): # Ensure (target 4, target_source 4, source 4) are added, and # everything else is as expected. queryset = OneToOneTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, @@ -92,7 +92,7 @@ class ReverseNestedOneToOneTests(TestCase): # Ensure (target 3, target_source 3, source 3) are updated, # and everything else is as expected. queryset = OneToOneTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}}, {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}}, @@ -125,7 +125,7 @@ class ForwardNestedOneToOneTests(TestCase): def test_one_to_one_retrieve(self): queryset = OneToOneSource.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, @@ -144,7 +144,7 @@ class ForwardNestedOneToOneTests(TestCase): # Ensure (target 4, target_source 4, source 4) are added, and # everything else is as expected. queryset = OneToOneSource.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, @@ -171,7 +171,7 @@ class ForwardNestedOneToOneTests(TestCase): # Ensure (target 3, target_source 3, source 3) are updated, # and everything else is as expected. queryset = OneToOneSource.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, @@ -224,7 +224,7 @@ class ReverseNestedOneToManyTests(TestCase): def test_one_to_many_retrieve(self): queryset = OneToManyTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, {'id': 2, 'name': 'source-2'}, @@ -247,7 +247,7 @@ class ReverseNestedOneToManyTests(TestCase): # Ensure source 4 is added, and everything else is as # expected. queryset = OneToManyTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, {'id': 2, 'name': 'source-2'}, @@ -279,7 +279,7 @@ class ReverseNestedOneToManyTests(TestCase): # Ensure (target 1, source 1) are updated, # and everything else is as expected. queryset = OneToManyTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, {'id': 2, 'name': 'source-2'}, @@ -299,7 +299,7 @@ class ReverseNestedOneToManyTests(TestCase): # Ensure source 2 is deleted, and everything else is as # expected. queryset = OneToManyTarget.objects.all() - serializer = self.Serializer(queryset) + serializer = self.Serializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'}, {'id': 3, 'name': 'source-3'}]} From 3fcc01273c5efef26d911e50c02a4a43f89b34eb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 20:29:52 +0100 Subject: [PATCH 0008/1652] Remove deprecated code --- rest_framework/compat.py | 5 +- rest_framework/fields.py | 7 --- rest_framework/permissions.py | 12 +---- rest_framework/relations.py | 69 ++++--------------------- rest_framework/serializers.py | 32 ++---------- rest_framework/tests/test_serializer.py | 4 +- 6 files changed, 21 insertions(+), 108 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b748dcc51..161fffa8d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -494,11 +494,14 @@ try: if provider_version in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes - from datetime.datetime import now as provider_now + import datetime + provider_now = datetime.datetime.now else: # Any other supported version does use timezone aware datetimes from django.utils.timezone import now as provider_now except ImportError: + import traceback + traceback.print_exc() oauth2_provider = None oauth2_provider_models = None oauth2_provider_forms = None diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 35848b4ce..2e23715de 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -224,13 +224,6 @@ class WritableField(Field): validators=[], error_messages=None, widget=None, default=None, blank=None): - # 'blank' is to be deprecated in favor of 'required' - if blank is not None: - warnings.warn('The `blank` keyword argument is deprecated. ' - 'Use the `required` keyword argument instead.', - DeprecationWarning, stacklevel=2) - required = not(blank) - super(WritableField, self).__init__(source=source, label=label, help_text=help_text) self.read_only = read_only diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 1036663e0..0c7b02ffa 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -2,13 +2,10 @@ Provides a set of pluggable permission policies. """ from __future__ import unicode_literals -import inspect -import warnings +from rest_framework.compat import oauth2_provider_scope, oauth2_constants SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -from rest_framework.compat import oauth2_provider_scope, oauth2_constants - class BasePermission(object): """ @@ -25,13 +22,6 @@ class BasePermission(object): """ Return `True` if permission is granted, `False` otherwise. """ - if len(inspect.getargspec(self.has_permission).args) == 4: - warnings.warn( - 'The `obj` argument in `has_permission` is deprecated. ' - 'Use `has_object_permission()` instead for object permissions.', - DeprecationWarning, stacklevel=2 - ) - return self.has_permission(request, view, obj) return True diff --git a/rest_framework/relations.py b/rest_framework/relations.py index edaf76d6e..ede694e31 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -40,14 +40,6 @@ class RelatedField(WritableField): many = False def __init__(self, *args, **kwargs): - - # 'null' is to be deprecated in favor of 'required' - if 'null' in kwargs: - warnings.warn('The `null` keyword argument is deprecated. ' - 'Use the `required` keyword argument instead.', - DeprecationWarning, stacklevel=2) - kwargs['required'] = not kwargs.pop('null') - queryset = kwargs.pop('queryset', None) self.many = kwargs.pop('many', self.many) if self.many: @@ -424,14 +416,11 @@ class HyperlinkedRelatedField(RelatedField): request = self.context.get('request', None) format = self.format or self.context.get('format', None) - if request is None: - msg = ( - "Using `HyperlinkedRelatedField` without including the request " - "in the serializer context is deprecated. " - "Add `context={'request': request}` when instantiating " - "the serializer." - ) - warnings.warn(msg, DeprecationWarning, stacklevel=4) + assert request is not None, ( + "`HyperlinkedRelatedField` requires the request in the serializer " + "context. Add `context={'request': request}` when instantiating " + "the serializer." + ) # If the object has not yet been saved then we cannot hyperlink to it. if getattr(obj, 'pk', None) is None: @@ -530,11 +519,11 @@ class HyperlinkedIdentityField(Field): format = self.context.get('format', None) view_name = self.view_name - if request is None: - warnings.warn("Using `HyperlinkedIdentityField` without including the " - "request in the serializer context is deprecated. " - "Add `context={'request': request}` when instantiating the serializer.", - DeprecationWarning, stacklevel=4) + assert request is not None, ( + "`HyperlinkedIdentityField` requires the request in the serializer" + " context. Add `context={'request': request}` when instantiating " + "the serializer." + ) # By default use whatever format is given for the current context # unless the target is a different type to the source. @@ -593,41 +582,3 @@ class HyperlinkedIdentityField(Field): pass raise NoReverseMatch() - - -### Old-style many classes for backwards compat - -class ManyRelatedField(RelatedField): - def __init__(self, *args, **kwargs): - warnings.warn('`ManyRelatedField()` is deprecated. ' - 'Use `RelatedField(many=True)` instead.', - DeprecationWarning, stacklevel=2) - kwargs['many'] = True - super(ManyRelatedField, self).__init__(*args, **kwargs) - - -class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField): - def __init__(self, *args, **kwargs): - warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. ' - 'Use `PrimaryKeyRelatedField(many=True)` instead.', - DeprecationWarning, stacklevel=2) - kwargs['many'] = True - super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs) - - -class ManySlugRelatedField(SlugRelatedField): - def __init__(self, *args, **kwargs): - warnings.warn('`ManySlugRelatedField()` is deprecated. ' - 'Use `SlugRelatedField(many=True)` instead.', - DeprecationWarning, stacklevel=2) - kwargs['many'] = True - super(ManySlugRelatedField, self).__init__(*args, **kwargs) - - -class ManyHyperlinkedRelatedField(HyperlinkedRelatedField): - def __init__(self, *args, **kwargs): - warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. ' - 'Use `HyperlinkedRelatedField(many=True)` instead.', - DeprecationWarning, stacklevel=2) - kwargs['many'] = True - super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 023f7ccfb..ae39cce88 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,7 +15,6 @@ import copy import datetime import types from decimal import Decimal -from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict @@ -141,7 +140,7 @@ class BaseSerializer(WritableField): _dict_class = SortedDictWithMetadata def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, many=None, + context=None, partial=False, many=False, allow_add_remove=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) @@ -348,12 +347,7 @@ class BaseSerializer(WritableField): if value is None: return None - if self.many is not None: - many = self.many - else: - many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type)) - - if many: + if self.many: return [self.to_native(item) for item in value] return self.to_native(value) @@ -424,16 +418,7 @@ class BaseSerializer(WritableField): if self._errors is None: data, files = self.init_data, self.init_files - if self.many is not None: - many = self.many - else: - many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) - if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=3) - - if many: + if self.many: ret = [] errors = [] update = self.object is not None @@ -486,16 +471,7 @@ class BaseSerializer(WritableField): if self._data is None: obj = self.object - if self.many is not None: - many = self.many - else: - many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) - if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' - 'Use the `many=True` flag when instantiating the serializer.', - DeprecationWarning, stacklevel=2) - - if many: + if self.many: self._data = [self.to_native(item) for item in obj] else: self._data = self.to_native(obj) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8b87a0847..151eb6484 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1268,7 +1268,7 @@ class NestedSerializerContextTests(TestCase): model = Album fields = ("photo_set", "callable") - photo_set = PhotoSerializer(source="photo_set") + photo_set = PhotoSerializer(source="photo_set", many=True) callable = serializers.SerializerMethodField("_callable") def _callable(self, instance): @@ -1280,7 +1280,7 @@ class NestedSerializerContextTests(TestCase): albums = None class AlbumCollectionSerializer(serializers.Serializer): - albums = AlbumSerializer(source="albums") + albums = AlbumSerializer(source="albums", many=True) album1 = Album.objects.create(title="album 1") album2 = Album.objects.create(title="album 2") From 379ad8a82485e61b180ee823ba49799d39446aeb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 20:36:14 +0100 Subject: [PATCH 0009/1652] pending deprecations -> deprecated --- rest_framework/generics.py | 32 +++++++++++++++---------------- rest_framework/mixins.py | 8 ++++---- rest_framework/relations.py | 36 +++++++++++++++++------------------ rest_framework/serializers.py | 8 ++++---- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 99e9782e2..874a142c8 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -108,11 +108,11 @@ class GenericAPIView(views.APIView): deprecated_style = False if page_size is not None: warnings.warn('The `page_size` parameter to `paginate_queryset()` ' - 'is due to be deprecated. ' + 'is deprecated. ' 'Note that the return style of this method is also ' 'changed, and will simply return a page object ' 'when called without a `page_size` argument.', - PendingDeprecationWarning, stacklevel=2) + DeprecationWarning, stacklevel=2) deprecated_style = True else: # Determine the required page size. @@ -123,10 +123,10 @@ class GenericAPIView(views.APIView): if not self.allow_empty: warnings.warn( - 'The `allow_empty` parameter is due to be deprecated. ' + 'The `allow_empty` parameter is deprecated. ' 'To use `allow_empty=False` style behavior, You should override ' '`get_queryset()` and explicitly raise a 404 on empty querysets.', - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) paginator = self.paginator_class(queryset, page_size, @@ -166,10 +166,10 @@ class GenericAPIView(views.APIView): if not filter_backends and self.filter_backend: warnings.warn( 'The `filter_backend` attribute and `FILTER_BACKEND` setting ' - 'are due to be deprecated in favor of a `filter_backends` ' + 'are deprecated in favor of a `filter_backends` ' 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' 'a *list* of filter backend classes.', - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] @@ -192,8 +192,8 @@ class GenericAPIView(views.APIView): """ if queryset is not None: warnings.warn('The `queryset` parameter to `get_paginate_by()` ' - 'is due to be deprecated.', - PendingDeprecationWarning, stacklevel=2) + 'is deprecated.', + DeprecationWarning, stacklevel=2) if self.paginate_by_param: query_params = self.request.QUERY_PARAMS @@ -272,16 +272,16 @@ class GenericAPIView(views.APIView): filter_kwargs = {self.lookup_field: lookup} elif pk is not None and self.lookup_field == 'pk': warnings.warn( - 'The `pk_url_kwarg` attribute is due to be deprecated. ' + 'The `pk_url_kwarg` attribute is deprecated. ' 'Use the `lookup_field` attribute instead', - PendingDeprecationWarning + DeprecationWarning ) filter_kwargs = {'pk': pk} elif slug is not None and self.lookup_field == 'pk': warnings.warn( - 'The `slug_url_kwarg` attribute is due to be deprecated. ' + 'The `slug_url_kwarg` attribute is deprecated. ' 'Use the `lookup_field` attribute instead', - PendingDeprecationWarning + DeprecationWarning ) filter_kwargs = {self.slug_field: slug} else: @@ -482,9 +482,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, class MultipleObjectAPIView(GenericAPIView): def __init__(self, *args, **kwargs): warnings.warn( - 'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' + 'Subclassing `MultipleObjectAPIView` is deprecated. ' 'You should simply subclass `GenericAPIView` instead.', - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) super(MultipleObjectAPIView, self).__init__(*args, **kwargs) @@ -492,8 +492,8 @@ class MultipleObjectAPIView(GenericAPIView): class SingleObjectAPIView(GenericAPIView): def __init__(self, *args, **kwargs): warnings.warn( - 'Subclassing `SingleObjectAPIView` is due to be deprecated. ' + 'Subclassing `SingleObjectAPIView` is deprecated. ' 'You should simply subclass `GenericAPIView` instead.', - PendingDeprecationWarning, stacklevel=2 + DeprecationWarning, stacklevel=2 ) super(SingleObjectAPIView, self).__init__(*args, **kwargs) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f11def6d4..679dfa6c3 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -24,14 +24,14 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None) include = [] if pk: - # Pending deprecation + # Deprecated pk_field = obj._meta.pk while pk_field.rel: pk_field = pk_field.rel.to._meta.pk include.append(pk_field.name) if slug_field: - # Pending deprecation + # Deprecated include.append(slug_field) if lookup_field and lookup_field != 'pk': @@ -77,10 +77,10 @@ class ListModelMixin(object): # `.allow_empty = False`, to raise 404 errors on empty querysets. if not self.allow_empty and not self.object_list: warnings.warn( - 'The `allow_empty` parameter is due to be deprecated. ' + 'The `allow_empty` parameter is deprecated. ' 'To use `allow_empty=False` style behavior, You should override ' '`get_queryset()` and explicitly raise a 404 on empty querysets.', - PendingDeprecationWarning + DeprecationWarning ) class_name = self.__class__.__name__ error_msg = self.empty_error % {'class_name': class_name} diff --git a/rest_framework/relations.py b/rest_framework/relations.py index ede694e31..f1f7dea72 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -314,7 +314,7 @@ class HyperlinkedRelatedField(RelatedField): 'incorrect_type': _('Incorrect type. Expected url string, received %s.'), } - # These are all pending deprecation + # These are all deprecated pk_url_kwarg = 'pk' slug_field = 'slug' slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden @@ -328,16 +328,16 @@ class HyperlinkedRelatedField(RelatedField): self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) self.format = kwargs.pop('format', None) - # These are pending deprecation + # These are deprecated if 'pk_url_kwarg' in kwargs: - msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) if 'slug_url_kwarg' in kwargs: - msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) if 'slug_field' in kwargs: - msg = 'slug_field is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'slug_field is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) self.slug_field = kwargs.pop('slug_field', self.slug_field) @@ -380,9 +380,9 @@ class HyperlinkedRelatedField(RelatedField): # If the lookup succeeds using the default slug params, # then `slug_field` is being used implicitly, and we # we need to warn about the pending deprecation. - msg = 'Implicit slug field hyperlinked fields are pending deprecation.' \ + msg = 'Implicit slug field hyperlinked fields are deprecated.' \ 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + warnings.warn(msg, DeprecationWarning, stacklevel=2) return ret except NoReverseMatch: pass @@ -480,7 +480,7 @@ class HyperlinkedIdentityField(Field): lookup_field = 'pk' read_only = True - # These are all pending deprecation + # These are all deprecated pk_url_kwarg = 'pk' slug_field = 'slug' slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden @@ -496,16 +496,16 @@ class HyperlinkedIdentityField(Field): lookup_field = kwargs.pop('lookup_field', None) self.lookup_field = lookup_field or self.lookup_field - # These are pending deprecation + # These are deprecated if 'pk_url_kwarg' in kwargs: - msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) if 'slug_url_kwarg' in kwargs: - msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) if 'slug_field' in kwargs: - msg = 'slug_field is pending deprecation. Use lookup_field instead.' - warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + msg = 'slug_field is deprecated. Use lookup_field instead.' + warnings.warn(msg, DeprecationWarning, stacklevel=2) self.slug_field = kwargs.pop('slug_field', self.slug_field) default_slug_kwarg = self.slug_url_kwarg or self.slug_field diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ae39cce88..dd9e14ad7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -593,10 +593,10 @@ class ModelSerializer(Serializer): if len(inspect.getargspec(self.get_nested_field).args) == 2: warnings.warn( 'The `get_nested_field(model_field)` call signature ' - 'is due to be deprecated. ' + 'is deprecated. ' 'Use `get_nested_field(model_field, related_model, ' 'to_many) instead', - PendingDeprecationWarning + DeprecationWarning ) field = self.get_nested_field(model_field) else: @@ -605,10 +605,10 @@ class ModelSerializer(Serializer): if len(inspect.getargspec(self.get_nested_field).args) == 3: warnings.warn( 'The `get_related_field(model_field, to_many)` call ' - 'signature is due to be deprecated. ' + 'signature is deprecated. ' 'Use `get_related_field(model_field, related_model, ' 'to_many) instead', - PendingDeprecationWarning + DeprecationWarning ) field = self.get_related_field(model_field, to_many=to_many) else: From 7815811fe3047b5110e6993ecd72349f6f232232 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sun, 14 Jul 2013 18:13:37 -0700 Subject: [PATCH 0010/1652] Update nested serialization docs --- docs/api-guide/relations.md | 2 -- docs/api-guide/serializers.md | 17 ++++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 50c9bc546..21942eeff 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -213,8 +213,6 @@ Nested relationships can be expressed by using serializers as fields. If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. -Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style. - ## Example For example, the following serializer: diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index d9c23580c..23c999429 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -177,7 +177,7 @@ If a nested representation may optionally accept the `None` value you should pas content = serializers.CharField(max_length=200) created = serializers.DateTimeField() -Similarly if a nested representation should be a list of items, you should the `many=True` flag to the nested serialized. +Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized. class CommentSerializer(serializers.Serializer): user = UserSerializer(required=False) @@ -185,11 +185,13 @@ Similarly if a nested representation should be a list of items, you should the ` content = serializers.CharField(max_length=200) created = serializers.DateTimeField() ---- +Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object. -**Note**: Nested serializers are only suitable for read-only representations, as there are cases where they would have ambiguous or non-obvious behavior if used when updating instances. For read-write representations you should always use a flat representation, by using one of the `RelatedField` subclasses. - ---- + serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'user': 'doe'}, 'content': 'baz'}) + serializer.is_valid() + # False + serializer.errors + # {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']} ## Dealing with multiple objects @@ -293,8 +295,7 @@ You can provide arbitrary additional context by passing a `context` argument whe The context dictionary can be used within any serializer field logic, such as a custom `.to_native()` method, by accessing the `self.context` attribute. ---- - +- # ModelSerializer Often you'll want serializer classes that map closely to model definitions. @@ -331,6 +332,8 @@ The default `ModelSerializer` uses primary keys for relationships, but you can a The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation. +If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself. + ## Specifying which fields should be read-only You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so: From b5dc6b61131cc36b0540133a28613c06e7f4e26a Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sun, 14 Jul 2013 18:18:39 -0700 Subject: [PATCH 0011/1652] Fix docs typo --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 23c999429..022853caa 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -187,7 +187,7 @@ Similarly if a nested representation should be a list of items, you should pass Validation of nested objects will work the same as before. Errors with nested objects will be nested under the field name of the nested object. - serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'user': 'doe'}, 'content': 'baz'}) + serializer = CommentSerializer(comment, data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'}) serializer.is_valid() # False serializer.errors From d72603bc6a16112008959c5267839f819c2bc43a Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Wed, 5 Jun 2013 17:39:14 -0400 Subject: [PATCH 0012/1652] Add support for collection routes to SimpleRouter --- rest_framework/decorators.py | 26 +++++++++++++++ rest_framework/routers.py | 33 +++++++++++++++++-- rest_framework/tests/test_routers.py | 48 +++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index c69756a43..dacd380fb 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -113,6 +113,7 @@ def link(**kwargs): """ def decorator(func): func.bind_to_methods = ['get'] + func.collection = False func.kwargs = kwargs return func return decorator @@ -124,6 +125,31 @@ def action(methods=['post'], **kwargs): """ def decorator(func): func.bind_to_methods = methods + func.collection = False + func.kwargs = kwargs + return func + return decorator + + +def collection_link(**kwargs): + """ + Used to mark a method on a ViewSet that should be routed for GET requests. + """ + def decorator(func): + func.bind_to_methods = ['get'] + func.collection = True + func.kwargs = kwargs + return func + return decorator + + +def collection_action(methods=['post'], **kwargs): + """ + Used to mark a method on a ViewSet that should be routed for POST requests. + """ + def decorator(func): + func.bind_to_methods = methods + func.collection = True func.kwargs = kwargs return func return decorator diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 930011d39..9b859a7c7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -88,6 +88,17 @@ class SimpleRouter(BaseRouter): name='{basename}-list', initkwargs={'suffix': 'List'} ), + # Dynamically generated collection routes. + # Generated using @collection_action or @collection_link decorators + # on methods of the viewset. + Route( + url=r'^{prefix}/{methodname}{trailing_slash}$', + mapping={ + '{httpmethod}': '{methodname}', + }, + name='{basename}-collection-{methodnamehyphen}', + initkwargs={} + ), # Detail route. Route( url=r'^{prefix}/{lookup}{trailing_slash}$', @@ -107,7 +118,7 @@ class SimpleRouter(BaseRouter): mapping={ '{httpmethod}': '{methodname}', }, - name='{basename}-{methodnamehyphen}', + name='{basename}-dynamic-{methodnamehyphen}', initkwargs={} ), ] @@ -142,20 +153,25 @@ class SimpleRouter(BaseRouter): known_actions = flatten([route.mapping.values() for route in self.routes]) # Determine any `@action` or `@link` decorated methods on the viewset + collection_routes = [] dynamic_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) httpmethods = getattr(attr, 'bind_to_methods', None) + collection = getattr(attr, 'collection', False) if httpmethods: if methodname in known_actions: raise ImproperlyConfigured('Cannot use @action or @link decorator on ' 'method "%s" as it is an existing route' % methodname) httpmethods = [method.lower() for method in httpmethods] - dynamic_routes.append((httpmethods, methodname)) + if collection: + collection_routes.append((httpmethods, methodname)) + else: + dynamic_routes.append((httpmethods, methodname)) ret = [] for route in self.routes: - if route.mapping == {'{httpmethod}': '{methodname}'}: + if route.name == '{basename}-dynamic-{methodnamehyphen}': # Dynamic routes (@link or @action decorator) for httpmethods, methodname in dynamic_routes: initkwargs = route.initkwargs.copy() @@ -166,6 +182,17 @@ class SimpleRouter(BaseRouter): name=replace_methodname(route.name, methodname), initkwargs=initkwargs, )) + elif route.name == '{basename}-collection-{methodnamehyphen}': + # Dynamic routes (@collection_link or @collection_action decorator) + for httpmethods, methodname in collection_routes: + initkwargs = route.initkwargs.copy() + initkwargs.update(getattr(viewset, methodname).kwargs) + ret.append(Route( + url=replace_methodname(route.url, methodname), + mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), + name=replace_methodname(route.name, methodname), + initkwargs=initkwargs, + )) else: # Standard route ret.append(route) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 5fcccb741..60f150d2c 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers, viewsets, permissions from rest_framework.compat import include, patterns, url -from rest_framework.decorators import link, action +from rest_framework.decorators import link, action, collection_link, collection_action from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.test import APIRequestFactory @@ -214,3 +214,49 @@ class TestActionAppliedToExistingRoute(TestCase): with self.assertRaises(ImproperlyConfigured): self.router.urls + + +class StaticAndDynamicViewSet(viewsets.ViewSet): + def list(self, request, *args, **kwargs): + return Response({'method': 'list'}) + + @collection_action() + def collection_action(self, request, *args, **kwargs): + return Response({'method': 'action1'}) + + @action() + def dynamic_action(self, request, *args, **kwargs): + return Response({'method': 'action2'}) + + @collection_link() + def collection_link(self, request, *args, **kwargs): + return Response({'method': 'link1'}) + + @link() + def dynamic_link(self, request, *args, **kwargs): + return Response({'method': 'link2'}) + + +class TestStaticAndDynamicRouter(TestCase): + def setUp(self): + self.router = SimpleRouter() + + def test_link_and_action_decorator(self): + routes = self.router.get_routes(StaticAndDynamicViewSet) + decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] + # Make sure all these endpoints exist and none have been clobbered + for i, endpoint in enumerate(['collection_action', 'collection_link', 'dynamic_action', 'dynamic_link']): + route = decorator_routes[i] + # check url listing + if endpoint.startswith('collection_'): + self.assertEqual(route.url, + '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint)) + else: + self.assertEqual(route.url, + '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)) + # check method to function mapping + if endpoint.endswith('action'): + method_map = 'post' + else: + method_map = 'get' + self.assertEqual(route.mapping[method_map], endpoint) From 5b11e23f6fb35834057fba35832a597ce443cc77 Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Wed, 5 Jun 2013 17:41:29 -0400 Subject: [PATCH 0013/1652] Add docs for collection routes --- docs/api-guide/viewsets.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 47e59e2b2..9fa6615ba 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -92,7 +92,9 @@ The default routers included with REST framework will provide routes for a stand def destroy(self, request, pk=None): pass -If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests. +If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@collection_link`, `@collection_action`, `@link`, or `@action` decorators. The `@collection_link` and `@link` decorator will route `GET` requests, and the `@collection_action` and `@action` decorator will route `POST` requests. + +The `@link` and `@action` decorators contain `pk` in their URL pattern and are intended for methods which require a single instance. The `@collection_link` and `@collection_action` decorators are intended for methods which operate on a collection of objects. For example: @@ -121,13 +123,20 @@ For example: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -The `@action` and `@link` decorators can additionally take extra arguments that will be set for the routed view only. For example... + @collection_link() + def recent_users(self, request): + recent_users = User.objects.all().order('-last_login') + page = self.paginate_queryset(recent_users) + serializer = self.get_pagination_serializer(page) + return Response(serializer.data) + +The decorators can additionally take extra arguments that will be set for the routed view only. For example... @action(permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): ... -The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: +The `@collection_action` and `@action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): From 57cf8b5fa4f62f9b58912f10536a7ae5076ce54c Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Thu, 6 Jun 2013 11:51:52 -0400 Subject: [PATCH 0014/1652] Rework extra routes doc for better readability --- docs/api-guide/viewsets.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 9fa6615ba..e83487fb0 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -92,7 +92,7 @@ The default routers included with REST framework will provide routes for a stand def destroy(self, request, pk=None): pass -If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@collection_link`, `@collection_action`, `@link`, or `@action` decorators. The `@collection_link` and `@link` decorator will route `GET` requests, and the `@collection_action` and `@action` decorator will route `POST` requests. +If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link`, `@action`, `@collection_link`, or `@collection_action` decorators. The `@link` and `@collection_link` decorators will route `GET` requests, and the `@action` and `@collection_action` decorators will route `POST` requests. The `@link` and `@action` decorators contain `pk` in their URL pattern and are intended for methods which require a single instance. The `@collection_link` and `@collection_action` decorators are intended for methods which operate on a collection of objects. @@ -136,7 +136,7 @@ The decorators can additionally take extra arguments that will be set for the ro def set_password(self, request, pk=None): ... -The `@collection_action` and `@action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: +The `@action` and `@collection_action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): From 8d521c068a254cef604df1f15690275dca986778 Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Sun, 16 Jun 2013 12:43:59 -0400 Subject: [PATCH 0015/1652] Revert route name change and add key to Route object to identify different route types --- rest_framework/routers.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 9b859a7c7..541df4a9d 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -25,7 +25,7 @@ from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns -Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) +Route = namedtuple('Route', ['key', 'url', 'mapping', 'name', 'initkwargs']) def replace_methodname(format_string, methodname): @@ -80,6 +80,7 @@ class SimpleRouter(BaseRouter): routes = [ # List route. Route( + key='list', url=r'^{prefix}{trailing_slash}$', mapping={ 'get': 'list', @@ -92,15 +93,17 @@ class SimpleRouter(BaseRouter): # Generated using @collection_action or @collection_link decorators # on methods of the viewset. Route( + key='collection', url=r'^{prefix}/{methodname}{trailing_slash}$', mapping={ '{httpmethod}': '{methodname}', }, - name='{basename}-collection-{methodnamehyphen}', + name='{basename}-{methodnamehyphen}', initkwargs={} ), # Detail route. Route( + key='detail', url=r'^{prefix}/{lookup}{trailing_slash}$', mapping={ 'get': 'retrieve', @@ -114,11 +117,12 @@ class SimpleRouter(BaseRouter): # Dynamically generated routes. # Generated using @action or @link decorators on methods of the viewset. Route( + key='dynamic', url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', mapping={ '{httpmethod}': '{methodname}', }, - name='{basename}-dynamic-{methodnamehyphen}', + name='{basename}-{methodnamehyphen}', initkwargs={} ), ] @@ -171,23 +175,25 @@ class SimpleRouter(BaseRouter): ret = [] for route in self.routes: - if route.name == '{basename}-dynamic-{methodnamehyphen}': + if route.key == 'dynamic': # Dynamic routes (@link or @action decorator) for httpmethods, methodname in dynamic_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( + key=route.key, url=replace_methodname(route.url, methodname), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), name=replace_methodname(route.name, methodname), initkwargs=initkwargs, )) - elif route.name == '{basename}-collection-{methodnamehyphen}': + elif route.key == 'collection': # Dynamic routes (@collection_link or @collection_action decorator) for httpmethods, methodname in collection_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( + key=route.key, url=replace_methodname(route.url, methodname), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), name=replace_methodname(route.name, methodname), From f02274307826ebf98998e502fecca171bb0de696 Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Sun, 16 Jun 2013 12:51:33 -0400 Subject: [PATCH 0016/1652] Rename router collection test case --- rest_framework/tests/test_routers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 60f150d2c..e0a7e292e 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -216,7 +216,7 @@ class TestActionAppliedToExistingRoute(TestCase): self.router.urls -class StaticAndDynamicViewSet(viewsets.ViewSet): +class CollectionAndDynamicViewSet(viewsets.ViewSet): def list(self, request, *args, **kwargs): return Response({'method': 'list'}) @@ -237,12 +237,12 @@ class StaticAndDynamicViewSet(viewsets.ViewSet): return Response({'method': 'link2'}) -class TestStaticAndDynamicRouter(TestCase): +class TestCollectionAndDynamicRouter(TestCase): def setUp(self): self.router = SimpleRouter() def test_link_and_action_decorator(self): - routes = self.router.get_routes(StaticAndDynamicViewSet) + routes = self.router.get_routes(CollectionAndDynamicViewSet) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] # Make sure all these endpoints exist and none have been clobbered for i, endpoint in enumerate(['collection_action', 'collection_link', 'dynamic_action', 'dynamic_link']): From e14cbaf6961ad9c94deaf0417d8e8ce5ec96d0ac Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Sat, 13 Jul 2013 11:11:53 -0400 Subject: [PATCH 0017/1652] Changed collection_* decorators to list_* --- docs/api-guide/viewsets.md | 10 ++++----- rest_framework/decorators.py | 16 +++++++------- rest_framework/routers.py | 31 ++++++++++++++-------------- rest_framework/tests/test_routers.py | 24 ++++++++++----------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index e83487fb0..6d6bb1334 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -92,15 +92,15 @@ The default routers included with REST framework will provide routes for a stand def destroy(self, request, pk=None): pass -If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link`, `@action`, `@collection_link`, or `@collection_action` decorators. The `@link` and `@collection_link` decorators will route `GET` requests, and the `@action` and `@collection_action` decorators will route `POST` requests. +If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link`, `@action`, `@list_link`, or `@list_action` decorators. The `@link` and `@list_link` decorators will route `GET` requests, and the `@action` and `@list_action` decorators will route `POST` requests. -The `@link` and `@action` decorators contain `pk` in their URL pattern and are intended for methods which require a single instance. The `@collection_link` and `@collection_action` decorators are intended for methods which operate on a collection of objects. +The `@link` and `@action` decorators contain `pk` in their URL pattern and are intended for methods which require a single instance. The `@list_link` and `@list_action` decorators are intended for methods which operate on a list of objects. For example: from django.contrib.auth.models import User from rest_framework import viewsets - from rest_framework.decorators import action + from rest_framework.decorators import action, list_link from rest_framework.response import Response from myapp.serializers import UserSerializer, PasswordSerializer @@ -123,7 +123,7 @@ For example: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @collection_link() + @list_link() def recent_users(self, request): recent_users = User.objects.all().order('-last_login') page = self.paginate_queryset(recent_users) @@ -136,7 +136,7 @@ The decorators can additionally take extra arguments that will be set for the ro def set_password(self, request, pk=None): ... -The `@action` and `@collection_action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: +The `@action` and `@list_action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index dacd380fb..92f551db7 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -109,11 +109,11 @@ def permission_classes(permission_classes): def link(**kwargs): """ - Used to mark a method on a ViewSet that should be routed for GET requests. + Used to mark a method on a ViewSet that should be routed for detail GET requests. """ def decorator(func): func.bind_to_methods = ['get'] - func.collection = False + func.detail = True func.kwargs = kwargs return func return decorator @@ -121,35 +121,35 @@ def link(**kwargs): def action(methods=['post'], **kwargs): """ - Used to mark a method on a ViewSet that should be routed for POST requests. + Used to mark a method on a ViewSet that should be routed for detail POST requests. """ def decorator(func): func.bind_to_methods = methods - func.collection = False + func.detail = True func.kwargs = kwargs return func return decorator -def collection_link(**kwargs): +def list_link(**kwargs): """ Used to mark a method on a ViewSet that should be routed for GET requests. """ def decorator(func): func.bind_to_methods = ['get'] - func.collection = True + func.detail = False func.kwargs = kwargs return func return decorator -def collection_action(methods=['post'], **kwargs): +def list_action(methods=['post'], **kwargs): """ Used to mark a method on a ViewSet that should be routed for POST requests. """ def decorator(func): func.bind_to_methods = methods - func.collection = True + func.detail = False func.kwargs = kwargs return func return decorator diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 541df4a9d..c8f711e91 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -89,8 +89,8 @@ class SimpleRouter(BaseRouter): name='{basename}-list', initkwargs={'suffix': 'List'} ), - # Dynamically generated collection routes. - # Generated using @collection_action or @collection_link decorators + # Dynamically generated list routes. + # Generated using @list_action or @list_link decorators # on methods of the viewset. Route( key='collection', @@ -114,7 +114,7 @@ class SimpleRouter(BaseRouter): name='{basename}-detail', initkwargs={'suffix': 'Instance'} ), - # Dynamically generated routes. + # Dynamically generated detail routes. # Generated using @action or @link decorators on methods of the viewset. Route( key='dynamic', @@ -157,27 +157,28 @@ class SimpleRouter(BaseRouter): known_actions = flatten([route.mapping.values() for route in self.routes]) # Determine any `@action` or `@link` decorated methods on the viewset - collection_routes = [] - dynamic_routes = [] + detail_routes = [] + list_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) httpmethods = getattr(attr, 'bind_to_methods', None) - collection = getattr(attr, 'collection', False) + detail = getattr(attr, 'detail', True) if httpmethods: if methodname in known_actions: - raise ImproperlyConfigured('Cannot use @action or @link decorator on ' - 'method "%s" as it is an existing route' % methodname) + raise ImproperlyConfigured('Cannot use @action, @link, @list_action ' + 'or @list_link decorator on method "%s" ' + 'as it is an existing route' % methodname) httpmethods = [method.lower() for method in httpmethods] - if collection: - collection_routes.append((httpmethods, methodname)) + if detail: + detail_routes.append((httpmethods, methodname)) else: - dynamic_routes.append((httpmethods, methodname)) + list_routes.append((httpmethods, methodname)) ret = [] for route in self.routes: if route.key == 'dynamic': - # Dynamic routes (@link or @action decorator) - for httpmethods, methodname in dynamic_routes: + # Dynamic detail routes (@link or @action decorator) + for httpmethods, methodname in detail_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( @@ -188,8 +189,8 @@ class SimpleRouter(BaseRouter): initkwargs=initkwargs, )) elif route.key == 'collection': - # Dynamic routes (@collection_link or @collection_action decorator) - for httpmethods, methodname in collection_routes: + # Dynamic list routes (@list_link or @list_action decorator) + for httpmethods, methodname in list_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index e0a7e292e..393101763 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers, viewsets, permissions from rest_framework.compat import include, patterns, url -from rest_framework.decorators import link, action, collection_link, collection_action +from rest_framework.decorators import link, action, list_link, list_action from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.test import APIRequestFactory @@ -216,39 +216,39 @@ class TestActionAppliedToExistingRoute(TestCase): self.router.urls -class CollectionAndDynamicViewSet(viewsets.ViewSet): +class DynamicListAndDetailViewSet(viewsets.ViewSet): def list(self, request, *args, **kwargs): return Response({'method': 'list'}) - @collection_action() - def collection_action(self, request, *args, **kwargs): + @list_action() + def list_action(self, request, *args, **kwargs): return Response({'method': 'action1'}) @action() - def dynamic_action(self, request, *args, **kwargs): + def detail_action(self, request, *args, **kwargs): return Response({'method': 'action2'}) - @collection_link() - def collection_link(self, request, *args, **kwargs): + @list_link() + def list_link(self, request, *args, **kwargs): return Response({'method': 'link1'}) @link() - def dynamic_link(self, request, *args, **kwargs): + def detail_link(self, request, *args, **kwargs): return Response({'method': 'link2'}) -class TestCollectionAndDynamicRouter(TestCase): +class TestDynamicListAndDetailRouter(TestCase): def setUp(self): self.router = SimpleRouter() def test_link_and_action_decorator(self): - routes = self.router.get_routes(CollectionAndDynamicViewSet) + routes = self.router.get_routes(DynamicListAndDetailViewSet) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] # Make sure all these endpoints exist and none have been clobbered - for i, endpoint in enumerate(['collection_action', 'collection_link', 'dynamic_action', 'dynamic_link']): + for i, endpoint in enumerate(['list_action', 'list_link', 'detail_action', 'detail_link']): route = decorator_routes[i] # check url listing - if endpoint.startswith('collection_'): + if endpoint.startswith('list_'): self.assertEqual(route.url, '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint)) else: From ca7ba07b4e42bd1c7c6bb8088c0c5a2c434b56ee Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Sat, 13 Jul 2013 11:12:59 -0400 Subject: [PATCH 0018/1652] Introduce DynamicDetailRoute and DynamicListRoute to distinguish between different route types --- rest_framework/routers.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index c8f711e91..b8f19b66a 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -25,7 +25,9 @@ from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns -Route = namedtuple('Route', ['key', 'url', 'mapping', 'name', 'initkwargs']) +Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) +DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs']) +DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs']) def replace_methodname(format_string, methodname): @@ -80,7 +82,6 @@ class SimpleRouter(BaseRouter): routes = [ # List route. Route( - key='list', url=r'^{prefix}{trailing_slash}$', mapping={ 'get': 'list', @@ -92,18 +93,13 @@ class SimpleRouter(BaseRouter): # Dynamically generated list routes. # Generated using @list_action or @list_link decorators # on methods of the viewset. - Route( - key='collection', + DynamicListRoute( url=r'^{prefix}/{methodname}{trailing_slash}$', - mapping={ - '{httpmethod}': '{methodname}', - }, name='{basename}-{methodnamehyphen}', initkwargs={} ), # Detail route. Route( - key='detail', url=r'^{prefix}/{lookup}{trailing_slash}$', mapping={ 'get': 'retrieve', @@ -116,12 +112,8 @@ class SimpleRouter(BaseRouter): ), # Dynamically generated detail routes. # Generated using @action or @link decorators on methods of the viewset. - Route( - key='dynamic', + DynamicDetailRoute( url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', - mapping={ - '{httpmethod}': '{methodname}', - }, name='{basename}-{methodnamehyphen}', initkwargs={} ), @@ -154,7 +146,7 @@ class SimpleRouter(BaseRouter): Returns a list of the Route namedtuple. """ - known_actions = flatten([route.mapping.values() for route in self.routes]) + known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]) # Determine any `@action` or `@link` decorated methods on the viewset detail_routes = [] @@ -176,25 +168,23 @@ class SimpleRouter(BaseRouter): ret = [] for route in self.routes: - if route.key == 'dynamic': + if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@link or @action decorator) for httpmethods, methodname in detail_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( - key=route.key, url=replace_methodname(route.url, methodname), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), name=replace_methodname(route.name, methodname), initkwargs=initkwargs, )) - elif route.key == 'collection': + elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_link or @list_action decorator) for httpmethods, methodname in list_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( - key=route.key, url=replace_methodname(route.url, methodname), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), name=replace_methodname(route.name, methodname), From eaae8fb2d973769a827214e0606a7e41028d5d34 Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Mon, 15 Jul 2013 18:35:13 -0400 Subject: [PATCH 0019/1652] Combined link_* and action_* decorators into detail_route and list_route, marked the originals as deprecated. --- docs/api-guide/routers.md | 16 +++++----- docs/api-guide/viewsets.md | 16 +++++----- docs/tutorial/6-viewsets-and-routers.md | 8 ++--- rest_framework/decorators.py | 19 +++++++----- rest_framework/routers.py | 14 ++++----- rest_framework/tests/test_routers.py | 40 ++++++++++++------------- 6 files changed, 59 insertions(+), 54 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 865829057..f196dc3cd 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -35,12 +35,12 @@ The example above would generate the following URL patterns: * URL pattern: `^accounts/$` Name: `'account-list'` * URL pattern: `^accounts/{pk}/$` Name: `'account-detail'` -### Extra link and actions +### Registering additional routes -Any methods on the viewset decorated with `@link` or `@action` will also be routed. +Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed. For example, a given method like this on the `UserViewSet` class: - @action(permission_classes=[IsAdminOrIsSelf]) + @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): ... @@ -52,7 +52,7 @@ The following URL pattern would additionally be generated: ## SimpleRouter -This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@link` or `@action` decorators. +This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@detail_route` or `@list_route` decorators. @@ -62,8 +62,8 @@ This router includes routes for the standard set of `list`, `create`, `retrieve` - - + +
URL StyleHTTP MethodActionURL Name
PUTupdate
PATCHpartial_update
DELETEdestroy
{prefix}/{lookup}/{methodname}/GET@link decorated method{basename}-{methodname}
POST@action decorated method
{prefix}/{lookup}/{methodname}/GET@detail_route decorated method{basename}-{methodname}
POST@detail_route decorated method
By default the URLs created by `SimpleRouter` are appending with a trailing slash. @@ -86,8 +86,8 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d PUTupdate PATCHpartial_update DELETEdestroy - {prefix}/{lookup}/{methodname}/[.format]GET@link decorated method{basename}-{methodname} - POST@action decorated method + {prefix}/{lookup}/{methodname}/[.format]GET@detail_route decorated method{basename}-{methodname} + POST@detail_route decorated method As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 6d6bb1334..7a8d5979b 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -92,15 +92,15 @@ The default routers included with REST framework will provide routes for a stand def destroy(self, request, pk=None): pass -If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link`, `@action`, `@list_link`, or `@list_action` decorators. The `@link` and `@list_link` decorators will route `GET` requests, and the `@action` and `@list_action` decorators will route `POST` requests. +If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@detail_route` or `@list_route` decorators. -The `@link` and `@action` decorators contain `pk` in their URL pattern and are intended for methods which require a single instance. The `@list_link` and `@list_action` decorators are intended for methods which operate on a list of objects. +The `@detail_route` decorator contains `pk` in its URL pattern and is intended for methods which require a single instance. The `@list_route` decorator is intended for methods which operate on a list of objects. For example: from django.contrib.auth.models import User from rest_framework import viewsets - from rest_framework.decorators import action, list_link + from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from myapp.serializers import UserSerializer, PasswordSerializer @@ -111,7 +111,7 @@ For example: queryset = User.objects.all() serializer_class = UserSerializer - @action() + @detail_route(methods=['post']) def set_password(self, request, pk=None): user = self.get_object() serializer = PasswordSerializer(data=request.DATA) @@ -123,7 +123,7 @@ For example: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @list_link() + @list_route() def recent_users(self, request): recent_users = User.objects.all().order('-last_login') page = self.paginate_queryset(recent_users) @@ -132,13 +132,13 @@ For example: The decorators can additionally take extra arguments that will be set for the routed view only. For example... - @action(permission_classes=[IsAdminOrIsSelf]) + @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): ... -The `@action` and `@list_action` decorators will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: +By default, the decorators will route `GET` requests, but may also accept other HTTP methods, by using the `methods` argument. For example: - @action(methods=['POST', 'DELETE']) + @detail_route(methods=['post', 'delete']) def unset_password(self, request, pk=None): ... --- diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index f16add39d..f126ba045 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -25,7 +25,7 @@ Here we've used `ReadOnlyModelViewSet` class to automatically provide the defaul 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 rest_framework.decorators import link + from rest_framework.decorators import detail_route class SnippetViewSet(viewsets.ModelViewSet): """ @@ -39,7 +39,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,) - @link(renderer_classes=[renderers.StaticHTMLRenderer]) + @detail_route(renderer_classes=[renderers.StaticHTMLRenderer]) def highlight(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted) @@ -49,9 +49,9 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. -Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. +Notice that we've also used the `@detail_route` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. -Custom actions which use the `@link` decorator will respond to `GET` requests. We could have instead used the `@action` decorator if we wanted an action that responded to `POST` requests. +Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests. ## Binding ViewSets to URLs explicitly diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 92f551db7..1ca176f2c 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -3,13 +3,14 @@ The most important decorator in this module is `@api_view`, which is used for writing function-based views with REST framework. There are also various decorators for setting the API policies on function -based views, as well as the `@action` and `@link` decorators, which are +based views, as well as the `@detail_route` and `@list_route` decorators, which are used to annotate methods on viewsets that should be included by routers. """ from __future__ import unicode_literals from rest_framework.compat import six from rest_framework.views import APIView import types +import warnings def api_view(http_method_names): @@ -111,6 +112,8 @@ def link(**kwargs): """ Used to mark a method on a ViewSet that should be routed for detail GET requests. """ + msg = 'link is pending deprecation. Use detail_route instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) def decorator(func): func.bind_to_methods = ['get'] func.detail = True @@ -123,6 +126,8 @@ def action(methods=['post'], **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail POST requests. """ + msg = 'action is pending deprecation. Use detail_route instead.' + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) def decorator(func): func.bind_to_methods = methods func.detail = True @@ -131,21 +136,21 @@ def action(methods=['post'], **kwargs): return decorator -def list_link(**kwargs): +def detail_route(methods=['get'], **kwargs): """ - Used to mark a method on a ViewSet that should be routed for GET requests. + Used to mark a method on a ViewSet that should be routed for detail requests. """ def decorator(func): - func.bind_to_methods = ['get'] - func.detail = False + func.bind_to_methods = methods + func.detail = True func.kwargs = kwargs return func return decorator -def list_action(methods=['post'], **kwargs): +def list_route(methods=['get'], **kwargs): """ - Used to mark a method on a ViewSet that should be routed for POST requests. + Used to mark a method on a ViewSet that should be routed for list requests. """ def decorator(func): func.bind_to_methods = methods diff --git a/rest_framework/routers.py b/rest_framework/routers.py index b8f19b66a..b761ba9ae 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -91,7 +91,7 @@ class SimpleRouter(BaseRouter): initkwargs={'suffix': 'List'} ), # Dynamically generated list routes. - # Generated using @list_action or @list_link decorators + # Generated using @list_route decorator # on methods of the viewset. DynamicListRoute( url=r'^{prefix}/{methodname}{trailing_slash}$', @@ -111,7 +111,7 @@ class SimpleRouter(BaseRouter): initkwargs={'suffix': 'Instance'} ), # Dynamically generated detail routes. - # Generated using @action or @link decorators on methods of the viewset. + # Generated using @detail_route decorator on methods of the viewset. DynamicDetailRoute( url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', name='{basename}-{methodnamehyphen}', @@ -148,7 +148,7 @@ class SimpleRouter(BaseRouter): known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)]) - # Determine any `@action` or `@link` decorated methods on the viewset + # Determine any `@detail_route` or `@list_route` decorated methods on the viewset detail_routes = [] list_routes = [] for methodname in dir(viewset): @@ -157,8 +157,8 @@ class SimpleRouter(BaseRouter): detail = getattr(attr, 'detail', True) if httpmethods: if methodname in known_actions: - raise ImproperlyConfigured('Cannot use @action, @link, @list_action ' - 'or @list_link decorator on method "%s" ' + raise ImproperlyConfigured('Cannot use @detail_route or @list_route ' + 'decorators on method "%s" ' 'as it is an existing route' % methodname) httpmethods = [method.lower() for method in httpmethods] if detail: @@ -169,7 +169,7 @@ class SimpleRouter(BaseRouter): ret = [] for route in self.routes: if isinstance(route, DynamicDetailRoute): - # Dynamic detail routes (@link or @action decorator) + # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) @@ -180,7 +180,7 @@ class SimpleRouter(BaseRouter): initkwargs=initkwargs, )) elif isinstance(route, DynamicListRoute): - # Dynamic list routes (@list_link or @list_action decorator) + # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 393101763..c3597e389 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers, viewsets, permissions from rest_framework.compat import include, patterns, url -from rest_framework.decorators import link, action, list_link, list_action +from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter from rest_framework.test import APIRequestFactory @@ -18,23 +18,23 @@ class BasicViewSet(viewsets.ViewSet): def list(self, request, *args, **kwargs): return Response({'method': 'list'}) - @action() + @detail_route(methods=['post']) def action1(self, request, *args, **kwargs): return Response({'method': 'action1'}) - @action() + @detail_route(methods=['post']) def action2(self, request, *args, **kwargs): return Response({'method': 'action2'}) - @action(methods=['post', 'delete']) + @detail_route(methods=['post', 'delete']) def action3(self, request, *args, **kwargs): return Response({'method': 'action2'}) - @link() + @detail_route() def link1(self, request, *args, **kwargs): return Response({'method': 'link1'}) - @link() + @detail_route() def link2(self, request, *args, **kwargs): return Response({'method': 'link2'}) @@ -175,7 +175,7 @@ class TestActionKeywordArgs(TestCase): class TestViewSet(viewsets.ModelViewSet): permission_classes = [] - @action(permission_classes=[permissions.AllowAny]) + @detail_route(methods=['post'], permission_classes=[permissions.AllowAny]) def custom(self, request, *args, **kwargs): return Response({ 'permission_classes': self.permission_classes @@ -196,14 +196,14 @@ class TestActionKeywordArgs(TestCase): class TestActionAppliedToExistingRoute(TestCase): """ - Ensure `@action` decorator raises an except when applied + Ensure `@detail_route` decorator raises an except when applied to an existing route """ def test_exception_raised_when_action_applied_to_existing_route(self): class TestViewSet(viewsets.ModelViewSet): - @action() + @detail_route(methods=['post']) def retrieve(self, request, *args, **kwargs): return Response({ 'hello': 'world' @@ -220,20 +220,20 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): def list(self, request, *args, **kwargs): return Response({'method': 'list'}) - @list_action() - def list_action(self, request, *args, **kwargs): + @list_route(methods=['post']) + def list_route_post(self, request, *args, **kwargs): return Response({'method': 'action1'}) - @action() - def detail_action(self, request, *args, **kwargs): + @detail_route(methods=['post']) + def detail_route_post(self, request, *args, **kwargs): return Response({'method': 'action2'}) - @list_link() - def list_link(self, request, *args, **kwargs): + @list_route() + def list_route_get(self, request, *args, **kwargs): return Response({'method': 'link1'}) - @link() - def detail_link(self, request, *args, **kwargs): + @detail_route() + def detail_route_get(self, request, *args, **kwargs): return Response({'method': 'link2'}) @@ -241,11 +241,11 @@ class TestDynamicListAndDetailRouter(TestCase): def setUp(self): self.router = SimpleRouter() - def test_link_and_action_decorator(self): + def test_list_and_detail_route_decorators(self): routes = self.router.get_routes(DynamicListAndDetailViewSet) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] # Make sure all these endpoints exist and none have been clobbered - for i, endpoint in enumerate(['list_action', 'list_link', 'detail_action', 'detail_link']): + for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']): route = decorator_routes[i] # check url listing if endpoint.startswith('list_'): @@ -255,7 +255,7 @@ class TestDynamicListAndDetailRouter(TestCase): self.assertEqual(route.url, '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint)) # check method to function mapping - if endpoint.endswith('action'): + if endpoint.endswith('_post'): method_map = 'post' else: method_map = 'get' From db9672d3048eebb3d3c3fb2b4a345e17b5aa23cc Mon Sep 17 00:00:00 2001 From: Alex Burgel Date: Wed, 24 Jul 2013 17:24:29 -0400 Subject: [PATCH 0020/1652] Add support for removing field files by sending an empty string --- rest_framework/fields.py | 5 ++++- rest_framework/tests/test_files.py | 28 ++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f99318877..9ba5c0eb7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -307,7 +307,10 @@ class WritableField(Field): try: if self.use_files: files = files or {} - native = files[field_name] + try: + native = files[field_name] + except KeyError: + native = data[field_name] else: native = data[field_name] except KeyError: diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index 487046aca..495c2a7fd 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -7,13 +7,13 @@ import datetime class UploadedFile(object): - def __init__(self, file, created=None): + def __init__(self, file=None, created=None): self.file = file self.created = created or datetime.datetime.now() class UploadedFileSerializer(serializers.Serializer): - file = serializers.FileField() + file = serializers.FileField(required=False) created = serializers.DateTimeField() def restore_object(self, attrs, instance=None): @@ -47,5 +47,25 @@ class FileSerializerTests(TestCase): now = datetime.datetime.now() serializer = UploadedFileSerializer(data={'created': now}) - self.assertFalse(serializer.is_valid()) - self.assertIn('file', serializer.errors) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.object.created, now) + self.assertIsNone(serializer.object.file) + + def test_remove_with_empty_string(self): + """ + Passing empty string as data should cause file to be removed + + Test for: + https://github.com/tomchristie/django-rest-framework/issues/937 + """ + now = datetime.datetime.now() + file = BytesIO(six.b('stuff')) + file.name = 'stuff.txt' + file.size = len(file.getvalue()) + + uploaded_file = UploadedFile(file=file, created=now) + + serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''}) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.object.created, uploaded_file.created) + self.assertIsNone(serializer.object.file) From 1a4ff1567ea4231cde9a2f23725550a754f3f54c Mon Sep 17 00:00:00 2001 From: James Rutherford Date: Mon, 29 Jul 2013 10:16:15 +0100 Subject: [PATCH 0021/1652] Updated authtoken docs to mention south migrations --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ee1282b5a..fd6bfb56a 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -121,7 +121,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y 'rest_framework.authtoken' ) -Make sure to run `manage.py syncdb` after changing your settings. +Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below). You'll also need to create tokens for your users. From 195b1af7ba34b833fc17f5693d7fbd9c8e7cce78 Mon Sep 17 00:00:00 2001 From: James Rutherford Date: Mon, 29 Jul 2013 10:16:51 +0100 Subject: [PATCH 0022/1652] Minor typo fix --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index fd6bfb56a..b1ab46227 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -203,7 +203,7 @@ You can do so by inserting a `needed_by` attribute in your user migration: For more details, see the [south documentation on dependencies][south-dependencies]. -Also not that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: +Also note that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`. python manage.py migrate From 205c626d6ec472603207e12f8cf9deb3f00bf729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 29 Jul 2013 15:53:26 +0200 Subject: [PATCH 0023/1652] Add @jimr for #1013 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 95cac7176..4cc7f34b8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -151,6 +151,7 @@ The following people have helped make REST framework great. * Will Kahn-Greene - [willkg] * Kevin Brown - [kevin-brown] * Rodrigo Martell - [coderigo] +* James Rutherford - [jimr] Many thanks to everyone who's contributed to the project. @@ -338,3 +339,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [coderigo]: https://github.com/coderigo [willkg]: https://github.com/willkg [kevin-brown]: https://github.com/kevin-brown +[jimr]: https://github.com/jimr From 56d19dcc1c71494e83336a799ccf1f500a5af3b8 Mon Sep 17 00:00:00 2001 From: leandersikma Date: Tue, 30 Jul 2013 11:26:17 +0200 Subject: [PATCH 0024/1652] Wrong name of the login_base html file. Changed rest_framework/base_login.html to rest_framework/login_base.html (which is the correct name the user should extends in their templates). --- docs/topics/browsable-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index 2ae8cadb9..b2c78f3c6 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -90,7 +90,7 @@ The browsable API makes use of the Bootstrap tooltips component. Any element wi ### Login Template -To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/base_login.html`. +To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/login_base.html`. You can add your site name or branding by including the branding block: From 294d957361c78cdafcef60f915738ed0e533327c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 31 Jul 2013 20:14:49 +0200 Subject: [PATCH 0025/1652] Add drf-any-permission docs entry --- docs/api-guide/permissions.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 2c0a055c8..57636bc6f 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -188,6 +188,16 @@ Note that the generic views will check the appropriate object level permissions, Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details. +--- + +# Third party packages + +The following third party packages are also available. + +## DRF Any Permissions + +The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to the REST framework. Only one of the given permissions have to be true in order to get access to the view. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md @@ -197,3 +207,4 @@ Also note that the generic views will only check the object-level permissions fo [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [2.2-announcement]: ../topics/2.2-announcement.md [filtering]: filtering.md +[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions From e61210399154723f342ab9295c938bf72c8da7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 31 Jul 2013 20:25:28 +0200 Subject: [PATCH 0026/1652] Fix typo --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 57636bc6f..5597886d2 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -196,7 +196,7 @@ The following third party packages are also available. ## DRF Any Permissions -The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to the REST framework. Only one of the given permissions have to be true in order to get access to the view. +The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to the REST framework. Only one of the given permissions has to be true in order to get access to the view. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md From 3802442c89a722da7e48210d315856b5993fcdbe Mon Sep 17 00:00:00 2001 From: Ricky Rosario Date: Thu, 1 Aug 2013 17:02:16 -0400 Subject: [PATCH 0027/1652] Add missing comma to generic view example. --- 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 67853ed01..32a4feef4 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -40,7 +40,7 @@ For more complex cases you might also want to override various methods on the vi For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry. - url(r'^/users/', ListCreateAPIView.as_view(model=User) name='user-list') + url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list') --- From e4144b5b674cc8849c248727a09b82b82d1a01c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 2 Aug 2013 08:59:18 +0200 Subject: [PATCH 0028/1652] Add @rlr for #1022 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 4cc7f34b8..d1fa3681d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -152,6 +152,7 @@ The following people have helped make REST framework great. * Kevin Brown - [kevin-brown] * Rodrigo Martell - [coderigo] * James Rutherford - [jimr] +* ricky rosario - [rlr] Many thanks to everyone who's contributed to the project. @@ -340,3 +341,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [willkg]: https://github.com/willkg [kevin-brown]: https://github.com/kevin-brown [jimr]: https://github.com/jimr +[rlr]: https://github.com/rlr From 4ff1dc6a11ec9e1fd897cf2fdb74d57be7420515 Mon Sep 17 00:00:00 2001 From: James Summerfield Date: Sat, 3 Aug 2013 10:23:39 +0100 Subject: [PATCH 0029/1652] Fixing typos in routers.md --- docs/api-guide/routers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 865829057..7f53f109d 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -38,7 +38,7 @@ The example above would generate the following URL patterns: ### Extra link and actions Any methods on the viewset decorated with `@link` or `@action` will also be routed. -For example, a given method like this on the `UserViewSet` class: +For example, given a method like this on the `UserViewSet` class: @action(permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): @@ -66,7 +66,7 @@ This router includes routes for the standard set of `list`, `create`, `retrieve` POST@action decorated method -By default the URLs created by `SimpleRouter` are appending with a trailing slash. +By default the URLs created by `SimpleRouter` are appended with a trailing slash. This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example: router = SimpleRouter(trailing_slash=False) @@ -90,7 +90,7 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d POST@action decorated method -As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. +As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. router = DefaultRouter(trailing_slash=False) @@ -139,7 +139,7 @@ The `SimpleRouter` class provides another example of setting the `.routes` attri ## Advanced custom routers -If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should insect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute. +If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute. You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. From 4d8d2340be4de905af3488dc721c7b94b1371ef0 Mon Sep 17 00:00:00 2001 From: Veronica Lynn Date: Wed, 7 Aug 2013 14:00:06 -0400 Subject: [PATCH 0030/1652] Fixed typos in a bunch of docs --- docs/api-guide/permissions.md | 2 +- docs/api-guide/relations.md | 8 ++++---- docs/api-guide/renderers.md | 2 +- docs/api-guide/responses.md | 4 ++-- docs/api-guide/reverse.md | 2 +- docs/api-guide/routers.md | 2 +- docs/api-guide/serializers.md | 6 +++--- docs/api-guide/settings.md | 4 ++-- docs/api-guide/testing.md | 2 +- docs/api-guide/views.md | 2 +- docs/topics/2.2-announcement.md | 6 +++--- docs/topics/2.3-announcement.md | 12 ++++++------ docs/topics/documenting-your-api.md | 8 ++++---- docs/tutorial/1-serialization.md | 2 +- docs/tutorial/3-class-based-views.md | 2 +- 15 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 5597886d2..096ef3691 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -147,7 +147,7 @@ If you need to test if a request is a read operation or a write operation, you s **Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required. -As of version 2.2 this signature has now been replaced with two separate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed. +As of version 2.2 this signature has now been replaced with two separate method calls, which is more explicit and obvious. The old style signature continues to work, but its use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed. For more details see the [2.2 release announcement][2.2-announcement]. diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 50c9bc546..829a3c548 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -39,7 +39,7 @@ In order to explain the various types of relational fields, we'll use a couple o ## RelatedField -`RelatedField` may be used to represent the target of the relationship using it's `__unicode__` method. +`RelatedField` may be used to represent the target of the relationship using its `__unicode__` method. For example, the following serializer. @@ -71,7 +71,7 @@ This field is read only. ## PrimaryKeyRelatedField -`PrimaryKeyRelatedField` may be used to represent the target of the relationship using it's primary key. +`PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key. For example, the following serializer: @@ -252,7 +252,7 @@ If you want to implement a read-write relational field, you must also implement ## Example -For, example, we could define a relational field, to serialize a track to a custom string representation, using it's ordering, title, and duration. +For, example, we could define a relational field, to serialize a track to a custom string representation, using its ordering, title, and duration. import time @@ -386,7 +386,7 @@ For more information see [the Django documentation on generic relations][generic By default, relational fields that target a ``ManyToManyField`` with a ``through`` model specified are set to read-only. -If you exlicitly specify a relational field pointing to a +If you explicitly specify a relational field pointing to a ``ManyToManyField`` with a through model, be sure to set ``read_only`` to ``True``. diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b434efe9a..bb3d20159 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -241,7 +241,7 @@ This renderer is used for rendering HTML multipart form data. **It is not suita To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method. -The method should return a bytestring, which wil be used as the body of the HTTP response. +The method should return a bytestring, which will be used as the body of the HTTP response. The arguments passed to the `.render()` method are: diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index 399b7c23f..5a42aa923 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -24,7 +24,7 @@ Unless you want to heavily customize REST framework for some reason, you should Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any Python primitives. -The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object. +The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primitive datatypes before creating the `Response` object. You can use REST framework's `Serializer` classes to perform this data serialization, or use your own custom serialization. @@ -54,7 +54,7 @@ The rendered content of the response. The `.render()` method must have been cal ## .template_name -The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the reponse. +The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the response. ## .accepted_renderer diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 19930dc3f..942623666 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -17,7 +17,7 @@ The advantages of doing so are: REST framework provides two utility functions to make it more simple to return absolute URIs from your Web API. -There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink it's output for you, which makes browsing the API much easier. +There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier. ## reverse diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 7f53f109d..072a2e79a 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -96,7 +96,7 @@ As with `SimpleRouter` the trailing slashes on the URL routes can be removed by # Custom Routers -Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view. +Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view. The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples. diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a1f0853e3..bbc8d019d 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -403,7 +403,7 @@ You can change the field that is used for object lookups by setting the `lookup_ Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. -For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example: +For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example: class AccountSerializer(serializers.HyperlinkedModelSerializer): url = serializers.HyperlinkedIdentityField( @@ -429,7 +429,7 @@ You can create customized subclasses of `ModelSerializer` or `HyperlinkedModelSe Doing so should be considered advanced usage, and will only be needed if you have some particular serializer requirements that you often need to repeat. -## Dynamically modifiying fields +## Dynamically modifying fields Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer. @@ -449,7 +449,7 @@ For example, if you wanted to be able to set which fields should be used by a se # Don't pass the 'fields' arg up to the superclass fields = kwargs.pop('fields', None) - # Instatiate the superclass normally + # Instantiate the superclass normally super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) if fields: diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 7b114983d..0be0eb24a 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -28,7 +28,7 @@ you should use the `api_settings` object. For example. print api_settings.DEFAULT_AUTHENTICATION_CLASSES -The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. +The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. --- @@ -165,7 +165,7 @@ Default: `'multipart'` The renderer classes that are supported when building test requests. -The format of any of these renderer classes may be used when contructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')` +The format of any of these renderer classes may be used when constructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')` Default: diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 40b077633..92f8d54aa 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -34,7 +34,7 @@ To support a wider set of request formats, or change the default format, [see th #### Explicitly encoding the request body -If you need to explictly encode the request body, you can do so by setting the `content_type` flag. For example: +If you need to explicitly encode the request body, you can do so by setting the `content_type` flag. For example: request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 683222d16..15581e098 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -110,7 +110,7 @@ You won't typically need to override this method. ### .finalize_response(self, request, response, \*args, **kwargs) -Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotation. +Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotiation. You won't typically need to override this method. diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 02cac1295..7d2760499 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -136,15 +136,15 @@ Now becomes: def has_object_permission(self, request, view, obj): return obj.owner == request.user -If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but it's use will raise a `PendingDeprecationWarning`. +If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but its use will raise a `PendingDeprecationWarning`. Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions. ### More explicit hyperlink relations behavior -When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not. +When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fall back to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not. -From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but it's use will raise a `PendingDeprecationWarning`. +From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`. [xordoquy]: https://github.com/xordoquy [django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md index 9fdebcd90..ba4351459 100644 --- a/docs/topics/2.3-announcement.md +++ b/docs/topics/2.3-announcement.md @@ -131,7 +131,7 @@ The `get_object` and `get_paginate_by` methods no longer take an optional querys Using an optional queryset with these methods continues to be supported, but will raise a `PendingDeprecationWarning`. -The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropraite page size, or returns `None`, if pagination is not configured for the view. +The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropriate page size, or returns `None`, if pagination is not configured for the view. Using the `page_size` argument is still supported and will trigger the old-style return type, but will raise a `PendingDeprecationWarning`. @@ -195,13 +195,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P 2.3 introduces a `DecimalField` serializer field, which returns `Decimal` instances. -For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implmentation. +For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation. ## ModelSerializers and reverse relationships The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed. -In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For revese relationships `model_field` will be `None`. +In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`. The old-style signature will continue to function but will raise a `PendingDeprecationWarning`. @@ -219,7 +219,7 @@ Note that the relevant methods have always been private APIs, and the docstrings ## More explicit style -The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explict `queryset` and `serializer_class` attributes. +The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explicit `queryset` and `serializer_class` attributes. For example, the following is now the recommended style for using generic views: @@ -227,7 +227,7 @@ For example, the following is now the recommended style for using generic views: queryset = MyModel.objects.all() serializer_class = MyModelSerializer -Using an explict `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute. +Using an explicit `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute. It also makes the usage of the `get_queryset()` or `get_serializer_class()` methods more obvious. @@ -246,7 +246,7 @@ It also makes the usage of the `get_queryset()` or `get_serializer_class()` meth ## Django 1.3 support -The 2.3.x release series will be the last series to provide compatiblity with Django 1.3. +The 2.3.x release series will be the last series to provide compatibility with Django 1.3. ## Version 2.2 API changes diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 7ee538f55..6291c924a 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -16,7 +16,7 @@ The most common way to document Web APIs today is to produce documentation that Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints. -The pacakge is fully documented, well supported, and comes highly recommended. +The package is fully documented, well supported, and comes highly recommended. Django REST Swagger supports REST framework versions 2.3 and above. @@ -42,7 +42,7 @@ There are various other online tools and services for providing API documentatio ## Self describing APIs -The browsable API that REST framwork provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser. +The browsable API that REST framework provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser. ![Screenshot - Self describing API][image-self-describing-api] @@ -93,11 +93,11 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me ## The hypermedia approach -To be fully RESTful an API should present it's available actions as hypermedia controls in the responses that it sends. +To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends. In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document. -To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documention includes pointers to background reading, as well as links to various hypermedia formats. +To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats. [cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 2b214d6a6..22d29285b 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -236,7 +236,7 @@ Edit the `snippet/views.py` file, and add the following. class JSONResponse(HttpResponse): """ - An HttpResponse that renders it's content into JSON. + An HttpResponse that renders its content into JSON. """ def __init__(self, data, **kwargs): content = JSONRenderer().render(data) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index c1b3d8f23..9fc424fee 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -81,7 +81,7 @@ Okay, we're done. If you run the development server everything should be workin One of the big wins of using class based views is that it allows us to easily compose reusable bits of behaviour. -The create/retrieve/update/delete operations that we've been using so far are going to be pretty simliar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. +The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. Let's take a look at how we can compose our views by using the mixin classes. From 2761d6a4724d3dc60f817ab0718446564ca62f80 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 7 Aug 2013 20:27:41 +0100 Subject: [PATCH 0031/1652] Added @kolvia. For multiple documentation fixes in #1026. Thank you! :) --- docs/topics/credits.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d1fa3681d..855ee479e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -152,7 +152,8 @@ The following people have helped make REST framework great. * Kevin Brown - [kevin-brown] * Rodrigo Martell - [coderigo] * James Rutherford - [jimr] -* ricky rosario - [rlr] +* Ricky Rosario - [rlr] +* Veronica Lynn - [kolvia] Many thanks to everyone who's contributed to the project. @@ -342,3 +343,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [kevin-brown]: https://github.com/kevin-brown [jimr]: https://github.com/jimr [rlr]: https://github.com/rlr +[kolvia]: https://github.com/kolvia From edd696292ca50cb9c10b50b0124a79135d007ea4 Mon Sep 17 00:00:00 2001 From: Dan Stephenson Date: Sat, 10 Aug 2013 01:12:36 +0100 Subject: [PATCH 0032/1652] Spelling correction on read_only_fields err msg --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 682a99a47..25825df4a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -683,7 +683,7 @@ class ModelSerializer(Serializer): # in the `read_only_fields` option for field_name in self.opts.read_only_fields: assert field_name not in self.base_fields.keys(), \ - "field '%s' on serializer '%s' specfied in " \ + "field '%s' on serializer '%s' specified in " \ "`read_only_fields`, but also added " \ "as an explict field. Remove it from `read_only_fields`." % \ (field_name, self.__class__.__name__) From bbdcbe945248c5448540a1ab746b8a4b8bc8f95b Mon Sep 17 00:00:00 2001 From: Dan Stephenson Date: Sat, 10 Aug 2013 01:22:47 +0100 Subject: [PATCH 0033/1652] Spelling correction on read_only_fields err msg just spotted extras --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 682a99a47..9e85cb463 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -685,10 +685,10 @@ class ModelSerializer(Serializer): assert field_name not in self.base_fields.keys(), \ "field '%s' on serializer '%s' specfied in " \ "`read_only_fields`, but also added " \ - "as an explict field. Remove it from `read_only_fields`." % \ + "as an explicit field. Remove it from `read_only_fields`." % \ (field_name, self.__class__.__name__) assert field_name in ret, \ - "Noexistant field '%s' specified in `read_only_fields` " \ + "Non-existant field '%s' specified in `read_only_fields` " \ "on serializer '%s'." % \ (field_name, self.__class__.__name__) ret[field_name].read_only = True From 54b7b6760c40d9820268207a44996e2118430744 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 12 Aug 2013 09:27:34 +0100 Subject: [PATCH 0034/1652] Added @etos. For fixes in #1029, #1030 - Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 855ee479e..c563971c1 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -154,6 +154,7 @@ The following people have helped make REST framework great. * James Rutherford - [jimr] * Ricky Rosario - [rlr] * Veronica Lynn - [kolvia] +* Dan Stephenson - [etos] Many thanks to everyone who's contributed to the project. @@ -344,3 +345,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [jimr]: https://github.com/jimr [rlr]: https://github.com/rlr [kolvia]: https://github.com/kolvia +[etos]: https://github.com/etos From cd5f1bb229f82cb1bf00c322fd5b688cf0638e97 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 12 Aug 2013 15:41:48 +0300 Subject: [PATCH 0035/1652] Fix PATCH button title in template --- rest_framework/templates/rest_framework/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 9d939e738..51f9c2916 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -196,7 +196,7 @@ {% endif %} {% if raw_data_patch_form %} - + {% endif %} From abe655e061871a568cccf473414e350f3eb61d8b Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 12 Aug 2013 21:01:37 +0300 Subject: [PATCH 0036/1652] Make OneToOneSource.target nullable --- rest_framework/tests/test_relations_nested.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py index 8325580f6..30229687c 100644 --- a/rest_framework/tests/test_relations_nested.py +++ b/rest_framework/tests/test_relations_nested.py @@ -10,7 +10,8 @@ class OneToOneTarget(models.Model): class OneToOneSource(models.Model): name = models.CharField(max_length=100) - target = models.OneToOneField(OneToOneTarget, related_name='source') + target = models.OneToOneField(OneToOneTarget, related_name='source', + null=True, blank=True) class OneToManyTarget(models.Model): @@ -21,7 +22,7 @@ class OneToManySource(models.Model): name = models.CharField(max_length=100) target = models.ForeignKey(OneToManyTarget, related_name='sources') - + class ReverseNestedOneToOneTests(TestCase): def setUp(self): class OneToOneSourceSerializer(serializers.ModelSerializer): From 901d2b0eb8270befa051510e190f3d5679086c7f Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 12 Aug 2013 21:02:59 +0300 Subject: [PATCH 0037/1652] Failing test case for nullifying nested object --- rest_framework/tests/test_relations_nested.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py index 30229687c..d393b0c35 100644 --- a/rest_framework/tests/test_relations_nested.py +++ b/rest_framework/tests/test_relations_nested.py @@ -180,6 +180,25 @@ class ForwardNestedOneToOneTests(TestCase): ] self.assertEqual(serializer.data, expected) + def test_one_to_one_update_to_null(self): + data = {'id': 3, 'name': 'source-3-updated', 'target': None} + instance = OneToOneSource.objects.get(pk=3) + serializer = self.Serializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + + self.assertEqual(serializer.data, data) + self.assertEqual(obj.name, 'source-3-updated') + self.assertEqual(obj.target, None) + + queryset = OneToOneSource.objects.all() + serializer = self.Serializer(queryset, many=True) + expected = [ + {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, + {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}}, + {'id': 3, 'name': 'source-3-updated', 'target': None} + ] + self.assertEqual(serializer.data, expected) # TODO: Nullable 1-1 tests # def test_one_to_one_delete(self): From ff1efcf60f0a9b66cdb736f8c0b2cfe2fc84cdf5 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 12 Aug 2013 18:08:23 +0300 Subject: [PATCH 0038/1652] If null or blank - don't save the nested object --- rest_framework/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d8f9145ec..2b260c256 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -522,7 +522,7 @@ class BaseSerializer(WritableField): if self.object._deleted: [self.delete_object(item) for item in self.object._deleted] else: - self.save_object(self.object, **kwargs) + self.save_object(self.object, **kwargs) return self.object @@ -891,7 +891,8 @@ class ModelSerializer(Serializer): # Nested relationships need to be saved before we can save the # parent instance. for field_name, sub_object in obj._nested_forward_relations.items(): - self.save_object(sub_object) + if sub_object: + self.save_object(sub_object) setattr(obj, field_name, sub_object) obj.save(**kwargs) From 770d496307f1f9d3c8a95a167a506e59302f0de4 Mon Sep 17 00:00:00 2001 From: martync Date: Tue, 13 Aug 2013 09:19:40 +0200 Subject: [PATCH 0039/1652] Small typo --- 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 f16add39d..8a1a1ae0c 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment, Let's take our current set of views, and refactor them into view sets. -First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class: +First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace them with a single class: from rest_framework import viewsets From 999056cde1c6355d5ca036f109b35b41cb9d47cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 13 Aug 2013 10:43:07 +0200 Subject: [PATCH 0040/1652] Add @martync for #1033 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index c563971c1..b91f051b8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -155,6 +155,7 @@ The following people have helped make REST framework great. * Ricky Rosario - [rlr] * Veronica Lynn - [kolvia] * Dan Stephenson - [etos] +* Martin Clement - [martync] Many thanks to everyone who's contributed to the project. @@ -346,3 +347,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [rlr]: https://github.com/rlr [kolvia]: https://github.com/kolvia [etos]: https://github.com/etos +[martync]: https://github.com/martync From e677f3ee5c9435594ce58a3256a119c08bdc1e42 Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Tue, 13 Aug 2013 13:26:30 +0200 Subject: [PATCH 0041/1652] PATCH requests should not be able to create objects. --- rest_framework/mixins.py | 13 ++++++++----- rest_framework/tests/test_generics.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f11def6d4..59d644694 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -142,11 +142,14 @@ class UpdateModelMixin(object): try: return self.get_object() except Http404: - # If this is a PUT-as-create operation, we need to ensure that - # we have relevant permissions, as if this was a POST request. - # This will either raise a PermissionDenied exception, - # or simply return None - self.check_permissions(clone_request(self.request, 'POST')) + if self.request.method == 'PUT': + # For PUT-as-create operation, we need to ensure that we have + # relevant permissions, as if this was a POST request. This + # will either raise a PermissionDenied exception, or simply + # return None. + self.check_permissions(clone_request(self.request, 'POST')) + else: + raise def pre_save(self, obj): """ diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 1550880b5..7a87d3892 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -338,6 +338,17 @@ class TestInstanceView(TestCase): new_obj = SlugBasedModel.objects.get(slug='test_slug') self.assertEqual(new_obj.text, 'foobar') + def test_patch_cannot_create_an_object(self): + """ + PATCH requests should not be able to create objects. + """ + data = {'text': 'foobar'} + request = factory.patch('/999', data, format='json') + with self.assertNumQueries(1): + response = self.view(request, pk=999).render() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertFalse(self.objects.filter(id=999).exists()) + class TestOverriddenGetObject(TestCase): """ From 1d8a80f5cc41f157403771439f15c9b7d615a43b Mon Sep 17 00:00:00 2001 From: Jeremy Satterfield Date: Tue, 13 Aug 2013 15:31:58 -0500 Subject: [PATCH 0042/1652] don't set X-Throttle-Wait-Second header if throttle wait is None --- rest_framework/tests/test_throttling.py | 33 ++++++++++++++++++++++++- rest_framework/views.py | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py index 19bc691ae..41bff6926 100644 --- a/rest_framework/tests/test_throttling.py +++ b/rest_framework/tests/test_throttling.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.core.cache import cache from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle +from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle from rest_framework.response import Response @@ -21,6 +21,14 @@ class User3MinRateThrottle(UserRateThrottle): scope = 'minutes' +class NonTimeThrottle(BaseThrottle): + def allow_request(self, request, view): + if not hasattr(self.__class__, 'called'): + self.__class__.called = True + return True + return False + + class MockView(APIView): throttle_classes = (User3SecRateThrottle,) @@ -35,6 +43,13 @@ class MockView_MinuteThrottling(APIView): return Response('foo') +class MockView_NonTimeThrottling(APIView): + throttle_classes = (NonTimeThrottle,) + + def get(self, request): + return Response('foo') + + class ThrottlingTests(TestCase): def setUp(self): """ @@ -140,6 +155,22 @@ class ThrottlingTests(TestCase): (80, None) )) + def test_non_time_throttle(self): + """ + Ensure for second based throttles. + """ + request = self.factory.get('/') + + self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called')) + + response = MockView_NonTimeThrottling.as_view()(request) + self.assertFalse('X-Throttle-Wait-Seconds' in response) + + self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called) + + response = MockView_NonTimeThrottling.as_view()(request) + self.assertFalse('X-Throttle-Wait-Seconds' in response) + class ScopedRateThrottleTests(TestCase): """ diff --git a/rest_framework/views.py b/rest_framework/views.py index 37bba7f02..d51233a93 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -269,7 +269,7 @@ class APIView(View): Handle any exception that occurs, by returning an appropriate response, or re-raising the error. """ - if isinstance(exc, exceptions.Throttled): + if isinstance(exc, exceptions.Throttled) and exc.wait is not None: # Throttle wait header self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait From 2f03870ae12479cbfdce68db1a30bad6fa15b311 Mon Sep 17 00:00:00 2001 From: JT Date: Tue, 13 Aug 2013 18:48:49 -0500 Subject: [PATCH 0043/1652] Fix for "No module named compat" --- 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 f99318877..add9d224d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -924,7 +924,7 @@ class ImageField(FileField): if f is None: return None - from compat import Image + from rest_framework.compat import Image assert Image is not None, 'PIL must be installed for ImageField support' # We need to get a file object for PIL. We might have a path or we might From 7bc63fbb11525c37fa73e1ffa9a6409a48aab4ac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 14 Aug 2013 13:17:30 +0100 Subject: [PATCH 0044/1652] Added @jsatt. For fix & tests in #1037. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b91f051b8..1b34d5e0c 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -156,6 +156,7 @@ The following people have helped make REST framework great. * Veronica Lynn - [kolvia] * Dan Stephenson - [etos] * Martin Clement - [martync] +* Jeremy Satterfield - [jsatt] Many thanks to everyone who's contributed to the project. @@ -348,3 +349,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [kolvia]: https://github.com/kolvia [etos]: https://github.com/etos [martync]: https://github.com/martync +[jsatt]: https://github.com/jsatt From 486f4c8047b18cc753e1969079b58ca3da6ab43f Mon Sep 17 00:00:00 2001 From: Chad Barrington Date: Wed, 14 Aug 2013 15:18:44 -0500 Subject: [PATCH 0045/1652] Update filters.py Here's a little cleanup for ya, brah! --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c058bc715..fbfdd099e 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -134,7 +134,7 @@ class OrderingFilter(BaseFilterBackend): ordering = self.remove_invalid_fields(queryset, ordering) if not ordering: - # Use 'ordering' attribtue by default + # Use 'ordering' attribute by default ordering = self.get_default_ordering(view) if ordering: From 99083baf25d3145d034336b2569b79488cf1662b Mon Sep 17 00:00:00 2001 From: Chad Barrington Date: Wed, 14 Aug 2013 16:01:25 -0500 Subject: [PATCH 0046/1652] Update filters.py Fixed cut n pasted get_ordering docstring for ya bro. --- rest_framework/filters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index fbfdd099e..4079e1bd5 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -109,8 +109,7 @@ class OrderingFilter(BaseFilterBackend): def get_ordering(self, request): """ - Search terms are set by a ?search=... query parameter, - and may be comma and/or whitespace delimited. + Ordering is set by a comma delimited ?ordering=... query parameter. """ params = request.QUERY_PARAMS.get(self.ordering_param) if params: From d07dae6e79d53fdcfc7d88e085fe7e6da97c9c74 Mon Sep 17 00:00:00 2001 From: Christopher Paolini Date: Thu, 15 Aug 2013 12:41:52 -0400 Subject: [PATCH 0047/1652] Ability to override name/description of view Added settings and additions to formatting.py --- rest_framework/settings.py | 4 ++++ rest_framework/utils/formatting.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8fd177d58..b65e42cfe 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -69,6 +69,10 @@ DEFAULTS = { 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, + # View configuration + 'VIEW_NAME_FUNCTION': None, + 'VIEW_DESCRIPTION_FUNCTION': None, + # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 4bec83877..c908ce664 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -7,6 +7,7 @@ from django.utils.html import escape from django.utils.safestring import mark_safe from rest_framework.compat import apply_markdown, smart_text import re +from rest_framework.settings import api_settings def _remove_trailing_string(content, trailing): @@ -48,7 +49,14 @@ def _camelcase_to_spaces(content): def get_view_name(cls, suffix=None): """ Return a formatted name for an `APIView` class or `@api_view` function. + If a VIEW_NAME_FUNCTION is set in settings the value of that will be used + if that value is "falsy" then the normal formatting will be used. """ + if api_settings.VIEW_NAME_FUNCTION: + name = api_settings.VIEW_NAME_FUNCTION(cls, suffix) + if name: + return name + name = cls.__name__ name = _remove_trailing_string(name, 'View') name = _remove_trailing_string(name, 'ViewSet') @@ -61,7 +69,14 @@ def get_view_name(cls, suffix=None): def get_view_description(cls, html=False): """ Return a description for an `APIView` class or `@api_view` function. + If a VIEW_DESCRIPTION_FUNCTION is set in settings the value of that will be used + if that value is "falsy" then the normal formatting will be used. """ + if api_settings.VIEW_DESCRIPTION_FUNCTION: + description = api_settings.VIEW_DESCRIPTION_FUNCTION(cls) + if description: + return markup_description(description) + description = cls.__doc__ or '' description = _remove_leading_indent(smart_text(description)) if html: From f34b9ff0498ca054bfe65b4da99b01d48ff052b8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 15 Aug 2013 21:36:45 +0100 Subject: [PATCH 0048/1652] AnonRateThrottle should always allow authenticated users. Closes #994 --- rest_framework/throttling.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index f6bb1cc84..65b455930 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -96,6 +96,9 @@ class SimpleRateThrottle(BaseThrottle): return True self.key = self.get_cache_key(request, view) + if self.key is None: + return True + self.history = cache.get(self.key, []) self.now = self.timer() From d5b56310b0cc99df1cd1e61fc35f251c46036d62 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Aug 2013 13:23:27 +0100 Subject: [PATCH 0049/1652] Update release notes --- docs/topics/release-notes.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d379ab74f..876f46d8c 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,17 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### master + +* Added `APITestClient`, `APIRequestFactory` and `APITestCase` etc... +* Refactor `SessionAuthentication` to allow esier override for CSRF exemption. +* Remove 'Hold down "Control" message from help_text". +* Added admin configuration for tokens. +* Bugfix: `AnonRateThrottle` fixed to not throttle authenticated users. +* Bugfix: Don't set `X-Throttle-Wait-Seconds` when throttle does not have `wait` value. +* Bugfix: Fixed `PATCH` button title in browsable API. +* Bugfix: Fix issue with OAuth2 provider naive datetimes. + ### 2.3.6 **Date**: 27th June 2013 From 13ca305b06a6d9bf982559640fa488f7ad31e2f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Aug 2013 13:27:21 +0100 Subject: [PATCH 0050/1652] Tweak docs. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 096ef3691..c6372f981 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -196,7 +196,7 @@ The following third party packages are also available. ## DRF Any Permissions -The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to the REST framework. Only one of the given permissions has to be true in order to get access to the view. +The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md From f6f69dc71d4db7492b5feecc69627dff0031e2b9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Aug 2013 14:03:20 +0100 Subject: [PATCH 0051/1652] Version 2.3.7 --- docs/topics/release-notes.md | 8 +++++--- rest_framework/__init__.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 876f46d8c..624d9acd4 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,12 +40,14 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### master +### 2.3.7 + +**Date**: 16th August 2013 * Added `APITestClient`, `APIRequestFactory` and `APITestCase` etc... * Refactor `SessionAuthentication` to allow esier override for CSRF exemption. -* Remove 'Hold down "Control" message from help_text". -* Added admin configuration for tokens. +* Remove 'Hold down "Control" message from help_text' widget messaging when not appropriate. +* Added admin configuration for auth tokens. * Bugfix: `AnonRateThrottle` fixed to not throttle authenticated users. * Bugfix: Don't set `X-Throttle-Wait-Seconds` when throttle does not have `wait` value. * Bugfix: Fixed `PATCH` button title in browsable API. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 776618ac3..087808e0b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.6' +__version__ = '2.3.7' VERSION = __version__ # synonym From c1ccd8b5b5aef1bd209862f9431c9c03e25ae755 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Aug 2013 14:12:24 +0100 Subject: [PATCH 0052/1652] Update docs to include testing. --- docs/index.md | 2 ++ docs/template.html | 1 + mkdocs.py | 1 + 3 files changed, 4 insertions(+) diff --git a/docs/index.md b/docs/index.md index 99cd6b882..a0ae2984d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -164,6 +164,7 @@ The API guide is your complete reference manual to all the functionality provide * [Returning URLs][reverse] * [Exceptions][exceptions] * [Status codes][status] +* [Testing][testing] * [Settings][settings] ## Topics @@ -288,6 +289,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [reverse]: api-guide/reverse.md [exceptions]: api-guide/exceptions.md [status]: api-guide/status-codes.md +[testing]: api-guide/testing.md [settings]: api-guide/settings.md [documenting-your-api]: topics/documenting-your-api.md diff --git a/docs/template.html b/docs/template.html index 27bc10622..a20c81110 100644 --- a/docs/template.html +++ b/docs/template.html @@ -89,6 +89,7 @@
  • Returning URLs
  • Exceptions
  • Status codes
  • +
  • Testing
  • Settings
  • diff --git a/mkdocs.py b/mkdocs.py index 1e3f1db3f..13228a0ce 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -69,6 +69,7 @@ path_list = [ 'api-guide/reverse.md', 'api-guide/exceptions.md', 'api-guide/status-codes.md', + 'api-guide/testing.md', 'api-guide/settings.md', 'topics/documenting-your-api.md', 'topics/ajax-csrf-cors.md', From 430f00847aece92172ad1ef3a0112897d8931019 Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Fri, 16 Aug 2013 09:20:49 -0400 Subject: [PATCH 0053/1652] Add test for BooleanField and required This tests setting required=True on a BooleanField. Test for issue #1004. --- rest_framework/tests/test_fields.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 6836ec86f..ebccba7d1 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -896,3 +896,12 @@ class CustomIntegerField(TestCase): self.assertFalse(serializer.is_valid()) +class BooleanField(TestCase): + """ + Tests for BooleanField + """ + def test_boolean_required(self): + class BooleanRequiredSerializer(serializers.Serializer): + bool_field = serializers.BooleanField(required=True) + + self.assertFalse(BooleanRequiredSerializer(data={}).is_valid()) From a95984e4d4b034c196d40f74fbdc6345e6d4124c Mon Sep 17 00:00:00 2001 From: Christopher Paolini Date: Fri, 16 Aug 2013 13:23:04 -0400 Subject: [PATCH 0054/1652] Settings now have default functions Updated the setting to have a default function. --- rest_framework/settings.py | 6 ++-- rest_framework/utils/formatting.py | 46 +++++++++++++----------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b65e42cfe..0b2bdb62d 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -70,8 +70,8 @@ DEFAULTS = { 'PAGINATE_BY_PARAM': None, # View configuration - 'VIEW_NAME_FUNCTION': None, - 'VIEW_DESCRIPTION_FUNCTION': None, + 'VIEW_NAME_FUNCTION': 'rest_framework.utils.formatting.view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.utils.formatting.view_description', # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', @@ -129,6 +129,8 @@ IMPORT_STRINGS = ( 'TEST_REQUEST_RENDERER_CLASSES', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', + 'VIEW_NAME_FUNCTION', + 'VIEW_DESCRIPTION_FUNCTION' ) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index c908ce664..4f59f6591 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -49,39 +49,15 @@ def _camelcase_to_spaces(content): def get_view_name(cls, suffix=None): """ Return a formatted name for an `APIView` class or `@api_view` function. - If a VIEW_NAME_FUNCTION is set in settings the value of that will be used - if that value is "falsy" then the normal formatting will be used. """ - if api_settings.VIEW_NAME_FUNCTION: - name = api_settings.VIEW_NAME_FUNCTION(cls, suffix) - if name: - return name - - name = cls.__name__ - name = _remove_trailing_string(name, 'View') - name = _remove_trailing_string(name, 'ViewSet') - name = _camelcase_to_spaces(name) - if suffix: - name += ' ' + suffix - return name + return api_settings.VIEW_NAME_FUNCTION(cls, suffix) def get_view_description(cls, html=False): """ Return a description for an `APIView` class or `@api_view` function. - If a VIEW_DESCRIPTION_FUNCTION is set in settings the value of that will be used - if that value is "falsy" then the normal formatting will be used. """ - if api_settings.VIEW_DESCRIPTION_FUNCTION: - description = api_settings.VIEW_DESCRIPTION_FUNCTION(cls) - if description: - return markup_description(description) - - description = cls.__doc__ or '' - description = _remove_leading_indent(smart_text(description)) - if html: - return markup_description(description) - return description + return api_settings.VIEW_DESCRIPTION_FUNCTION(cls) def markup_description(description): @@ -93,3 +69,21 @@ def markup_description(description): else: description = escape(description).replace('\n', '
    ') return mark_safe(description) + + +def view_name(cls, suffix=None): + name = cls.__name__ + name = _remove_trailing_string(name, 'View') + name = _remove_trailing_string(name, 'ViewSet') + name = _camelcase_to_spaces(name) + if suffix: + name += ' ' + suffix + + return name + +def view_description(cls, html=False): + description = cls.__doc__ or '' + description = _remove_leading_indent(smart_text(description)) + if html: + return markup_description(description) + return description \ No newline at end of file From e6662d434f0214d21d38e4388a40fd63e1f9dcc6 Mon Sep 17 00:00:00 2001 From: Christopher Paolini Date: Sat, 17 Aug 2013 17:44:51 -0400 Subject: [PATCH 0055/1652] Improved view/description function setting Now supports each View having its own name and description function and overriding the global default. --- rest_framework/renderers.py | 5 ++--- rest_framework/utils/breadcrumbs.py | 5 ++--- rest_framework/utils/formatting.py | 15 --------------- rest_framework/views.py | 27 ++++++++++++++++++++------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3a03ca332..1006e26cf 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,7 +24,6 @@ from rest_framework.settings import api_settings from rest_framework.request import clone_request from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.formatting import get_view_name, get_view_description from rest_framework import exceptions, parsers, status, VERSION @@ -498,10 +497,10 @@ class BrowsableAPIRenderer(BaseRenderer): return GenericContentForm() def get_name(self, view): - return get_view_name(view.__class__, getattr(view, 'suffix', None)) + return view.get_view_name() def get_description(self, view): - return get_view_description(view.__class__, html=True) + return view.get_view_description(html=True) def get_breadcrumbs(self, request): return get_breadcrumbs(request.path) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index d51374b0a..0384faba3 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from django.core.urlresolvers import resolve, get_script_prefix -from rest_framework.utils.formatting import get_view_name def get_breadcrumbs(url): @@ -29,8 +28,8 @@ def get_breadcrumbs(url): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - suffix = getattr(view, 'suffix', None) - name = get_view_name(view.cls, suffix) + instance = view.cls() + name = instance.get_view_name() breadcrumbs_list.insert(0, (name, prefix + url)) seen.append(view) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 4f59f6591..5780301af 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -45,21 +45,6 @@ def _camelcase_to_spaces(content): content = re.sub(camelcase_boundry, ' \\1', content).strip() return ' '.join(content.split('_')).title() - -def get_view_name(cls, suffix=None): - """ - Return a formatted name for an `APIView` class or `@api_view` function. - """ - return api_settings.VIEW_NAME_FUNCTION(cls, suffix) - - -def get_view_description(cls, html=False): - """ - Return a description for an `APIView` class or `@api_view` function. - """ - return api_settings.VIEW_DESCRIPTION_FUNCTION(cls) - - def markup_description(description): """ Apply HTML markup to the given description. diff --git a/rest_framework/views.py b/rest_framework/views.py index d51233a93..4553714a9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -12,7 +12,6 @@ from rest_framework.compat import View, HttpResponseBase from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings -from rest_framework.utils.formatting import get_view_name, get_view_description class APIView(View): @@ -25,6 +24,9 @@ class APIView(View): permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + view_name_function = api_settings.VIEW_NAME_FUNCTION + view_description_function = api_settings.VIEW_DESCRIPTION_FUNCTION + @classmethod def as_view(cls, **initkwargs): """ @@ -157,6 +159,21 @@ class APIView(View): self._negotiator = self.content_negotiation_class() return self._negotiator + def get_view_name(self): + """ + Get the view name + """ + # This is used by ViewSets to disambiguate instance vs list views + view_name_suffix = getattr(self, 'suffix', None) + + return self.view_name_function(self.__class__, view_name_suffix) + + def get_view_description(self, html=False): + """ + Get the view description + """ + return self.view_description_function(self.__class__, html) + # API policy implementation methods def perform_content_negotiation(self, request, force=False): @@ -342,16 +359,12 @@ class APIView(View): Return a dictionary of metadata about the view. Used to return responses for OPTIONS requests. """ - - # This is used by ViewSets to disambiguate instance vs list views - view_name_suffix = getattr(self, 'suffix', None) - # By default we can't provide any form-like information, however the # generic views override this implementation and add additional # information for POST and PUT methods, based on the serializer. ret = SortedDict() - ret['name'] = get_view_name(self.__class__, view_name_suffix) - ret['description'] = get_view_description(self.__class__) + ret['name'] = self.get_view_name() + ret['description'] = self.get_view_description() ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] ret['parses'] = [parser.media_type for parser in self.parser_classes] return ret From 11d7c1838a1146728528d762cdf6bf329321c1d1 Mon Sep 17 00:00:00 2001 From: Christopher Paolini Date: Sat, 17 Aug 2013 17:52:08 -0400 Subject: [PATCH 0056/1652] Updated default view name/description functions Forgot to update the default view name/description functions to the new setup. --- rest_framework/utils/formatting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 5780301af..89a89252e 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -56,8 +56,8 @@ def markup_description(description): return mark_safe(description) -def view_name(cls, suffix=None): - name = cls.__name__ +def view_name(instance, view, suffix=None): + name = view.__name__ name = _remove_trailing_string(name, 'View') name = _remove_trailing_string(name, 'ViewSet') name = _camelcase_to_spaces(name) @@ -66,8 +66,8 @@ def view_name(cls, suffix=None): return name -def view_description(cls, html=False): - description = cls.__doc__ or '' +def view_description(instance, view, html=False): + description = view.__doc__ or '' description = _remove_leading_indent(smart_text(description)) if html: return markup_description(description) From 5a374955b14e1f1fdc85f0fa68284dbf6b83ab5f Mon Sep 17 00:00:00 2001 From: Christopher Paolini Date: Sun, 18 Aug 2013 00:29:05 -0400 Subject: [PATCH 0057/1652] Updated tests for view name and description Updated the tests to use the default view_name and view_description functions in the formatter through the default in settings. --- rest_framework/tests/test_description.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index 8019f5eca..4c03c1ded 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -6,7 +6,6 @@ from rest_framework.compat import apply_markdown, smart_text from rest_framework.views import APIView from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring from rest_framework.tests.description import UTF8_TEST_DOCSTRING -from rest_framework.utils.formatting import get_view_name, get_view_description # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring @@ -58,7 +57,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(get_view_name(MockView), 'Mock') + self.assertEqual(MockView().get_view_name(), 'Mock') def test_view_description_uses_docstring(self): """Ensure view descriptions are based on the docstring.""" @@ -78,7 +77,7 @@ class TestViewNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEqual(get_view_description(MockView), DESCRIPTION) + self.assertEqual(MockView().get_view_description(), DESCRIPTION) def test_view_description_supports_unicode(self): """ @@ -86,7 +85,7 @@ class TestViewNamesAndDescriptions(TestCase): """ self.assertEqual( - get_view_description(ViewWithNonASCIICharactersInDocstring), + ViewWithNonASCIICharactersInDocstring().get_view_description(), smart_text(UTF8_TEST_DOCSTRING) ) @@ -97,7 +96,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(get_view_description(MockView), '') + self.assertEqual(MockView().get_view_description(), '') def test_markdown(self): """ From 89b0a539c389477cfd7df7df461868b85f618d95 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 08:24:27 +0100 Subject: [PATCH 0058/1652] Move view name/description functions into public space --- rest_framework/settings.py | 4 ++-- rest_framework/utils/formatting.py | 36 +++++++++--------------------- rest_framework/views.py | 21 ++++++++++++++++- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 0b2bdb62d..7d25e5131 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -70,8 +70,8 @@ DEFAULTS = { 'PAGINATE_BY_PARAM': None, # View configuration - 'VIEW_NAME_FUNCTION': 'rest_framework.utils.formatting.view_name', - 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.utils.formatting.view_description', + 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 89a89252e..4b59ba840 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -5,12 +5,13 @@ from __future__ import unicode_literals from django.utils.html import escape from django.utils.safestring import mark_safe -from rest_framework.compat import apply_markdown, smart_text -import re +from rest_framework.compat import apply_markdown from rest_framework.settings import api_settings +from textwrap import dedent +import re -def _remove_trailing_string(content, trailing): +def remove_trailing_string(content, trailing): """ Strip trailing component `trailing` from `content` if it exists. Used when generating names from view classes. @@ -20,10 +21,14 @@ def _remove_trailing_string(content, trailing): return content -def _remove_leading_indent(content): +def dedent(content): """ Remove leading indent from a block of text. Used when generating descriptions from docstrings. + + Note that python's `textwrap.dedent` doesn't quite cut it, + as it fails to dedent multiline docstrings that include + unindented text on the initial line. """ whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] @@ -32,11 +37,10 @@ def _remove_leading_indent(content): if whitespace_counts: whitespace_pattern = '^' + (' ' * min(whitespace_counts)) content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) - content = content.strip('\n') - return content + return content.strip() -def _camelcase_to_spaces(content): +def camelcase_to_spaces(content): """ Translate 'CamelCaseNames' to 'Camel Case Names'. Used when generating names from view classes. @@ -54,21 +58,3 @@ def markup_description(description): else: description = escape(description).replace('\n', '
    ') return mark_safe(description) - - -def view_name(instance, view, suffix=None): - name = view.__name__ - name = _remove_trailing_string(name, 'View') - name = _remove_trailing_string(name, 'ViewSet') - name = _camelcase_to_spaces(name) - if suffix: - name += ' ' + suffix - - return name - -def view_description(instance, view, html=False): - description = view.__doc__ or '' - description = _remove_leading_indent(smart_text(description)) - if html: - return markup_description(description) - return description \ No newline at end of file diff --git a/rest_framework/views.py b/rest_framework/views.py index 4553714a9..431e21f95 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -8,10 +8,29 @@ from django.http import Http404 from django.utils.datastructures import SortedDict from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions -from rest_framework.compat import View, HttpResponseBase +from rest_framework.compat import smart_text, HttpResponseBase, View from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings +from rest_framework.utils import formatting + + +def get_view_name(instance, view, suffix=None): + name = view.__name__ + name = formatting.remove_trailing_string(name, 'View') + name = formatting.remove_trailing_string(name, 'ViewSet') + name = formatting.camelcase_to_spaces(name) + if suffix: + name += ' ' + suffix + + return name + +def get_view_description(instance, view, html=False): + description = view.__doc__ or '' + description = formatting.dedent(smart_text(description)) + if html: + return formatting.markup_description(description) + return description class APIView(View): From 512067062419b736b65ca27bdb5663d863c775dd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 08:45:53 +0100 Subject: [PATCH 0059/1652] Document customizable view names/descriptions --- docs/api-guide/settings.md | 34 ++++++++++++++++++++++++++++++ rest_framework/views.py | 42 ++++++++++++++++++-------------------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 0be0eb24a..fe7925a5a 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -274,6 +274,40 @@ Default: `['iso-8601']` --- +## View names and descriptions + +**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.** + +#### VIEW_NAME_FUNCTION + +A string representing the function that should be used when generating view names. + +This should be a function with the following signature: + + view_name(cls, suffix=None) + +* `cls`: The view class. Typically the name function would inspect the name of the class when generating a descriptive name, by accessing `cls.__name__`. +* `suffix`: The optional suffix used when differentiating individual views in a viewset. + +Default: `'rest_framework.views.get_view_name'` + +#### VIEW_DESCRIPTION_FUNCTION + +A string representing the function that should be used when generating view descriptions. + +This setting can be changed to support markup styles other than the default markdown. For example, you can use it to support `rst` markup in your view docstrings being output in the browsable API. + +This should be a function with the following signature: + + view_description(cls, html=False) + +* `cls`: The view class. Typically the description function would inspect the docstring of the class when generating a description, by accessing `cls.__doc__` +* `html`: A boolean indicating if HTML output is required. `True` when used in the browsable API, and `False` when used in generating `OPTIONS` responses. + +Default: `'rest_framework.views.get_view_description'` + +--- + ## Miscellaneous settings #### FORMAT_SUFFIX_KWARG diff --git a/rest_framework/views.py b/rest_framework/views.py index 431e21f95..727a9f956 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -15,8 +15,8 @@ from rest_framework.settings import api_settings from rest_framework.utils import formatting -def get_view_name(instance, view, suffix=None): - name = view.__name__ +def get_view_name(cls, suffix=None): + name = cls.__name__ name = formatting.remove_trailing_string(name, 'View') name = formatting.remove_trailing_string(name, 'ViewSet') name = formatting.camelcase_to_spaces(name) @@ -25,8 +25,8 @@ def get_view_name(instance, view, suffix=None): return name -def get_view_description(instance, view, html=False): - description = view.__doc__ or '' +def get_view_description(cls, html=False): + description = cls.__doc__ or '' description = formatting.dedent(smart_text(description)) if html: return formatting.markup_description(description) @@ -43,9 +43,6 @@ class APIView(View): permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS - view_name_function = api_settings.VIEW_NAME_FUNCTION - view_description_function = api_settings.VIEW_DESCRIPTION_FUNCTION - @classmethod def as_view(cls, **initkwargs): """ @@ -131,6 +128,22 @@ class APIView(View): 'request': getattr(self, 'request', None) } + def get_view_name(self): + """ + Return the view name, as used in OPTIONS responses and in the + browsable API. + """ + func = api_settings.VIEW_NAME_FUNCTION + return func(self.__class__, getattr(self, 'suffix', None)) + + def get_view_description(self, html=False): + """ + Return some descriptive text for the view, as used in OPTIONS responses + and in the browsable API. + """ + func = api_settings.VIEW_DESCRIPTION_FUNCTION + return func(self.__class__, html) + # API policy instantiation methods def get_format_suffix(self, **kwargs): @@ -178,21 +191,6 @@ class APIView(View): self._negotiator = self.content_negotiation_class() return self._negotiator - def get_view_name(self): - """ - Get the view name - """ - # This is used by ViewSets to disambiguate instance vs list views - view_name_suffix = getattr(self, 'suffix', None) - - return self.view_name_function(self.__class__, view_name_suffix) - - def get_view_description(self, html=False): - """ - Get the view description - """ - return self.view_description_function(self.__class__, html) - # API policy implementation methods def perform_content_negotiation(self, request, force=False): From 3a99b0af5074bfae90ec3986f277720df5a13583 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 08:47:52 +0100 Subject: [PATCH 0060/1652] Added @chrispaolini. For customizable view names/descriptions in #1043. Thanks! :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 1b34d5e0c..e9b600749 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -157,6 +157,7 @@ The following people have helped make REST framework great. * Dan Stephenson - [etos] * Martin Clement - [martync] * Jeremy Satterfield - [jsatt] +* Christopher Paolini - [chrispaolini] Many thanks to everyone who's contributed to the project. @@ -350,3 +351,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [etos]: https://github.com/etos [martync]: https://github.com/martync [jsatt]: https://github.com/jsatt +[chrispaolini]: https://github.com/chrispaolini From 34d65119fc1c200b76a8af7213a92d6b279bd478 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 08:54:48 +0100 Subject: [PATCH 0061/1652] Update release notes. --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 624d9acd4..52abfc08e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. + ### 2.3.7 **Date**: 16th August 2013 From 4292cc18fa3e4b3f5e67c02c3780cdcbf901a0a1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 20:53:30 +0100 Subject: [PATCH 0062/1652] Docs tweaking --- docs/api-guide/routers.md | 11 +++++++---- docs/api-guide/viewsets.md | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 7884c2e94..c84654189 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -48,6 +48,8 @@ The following URL pattern would additionally be generated: * URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'` +For more information see the viewset documentation on [marking extra actions for routing][route-decorators]. + # API Guide ## SimpleRouter @@ -58,12 +60,12 @@ This router includes routes for the standard set of `list`, `create`, `retrieve` URL StyleHTTP MethodActionURL Name {prefix}/GETlist{basename}-list POSTcreate + {prefix}/{methodname}/GET, or as specified by `methods` argument`@list_route` decorated method{basename}-{methodname} {prefix}/{lookup}/GETretrieve{basename}-detail PUTupdate PATCHpartial_update DELETEdestroy - {prefix}/{lookup}/{methodname}/GET@detail_route decorated method{basename}-{methodname} - POST@detail_route decorated method + {prefix}/{lookup}/{methodname}/GET, or as specified by `methods` argument`@detail_route` decorated method{basename}-{methodname} By default the URLs created by `SimpleRouter` are appended with a trailing slash. @@ -82,12 +84,12 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d [.format]GETautomatically generated root viewapi-root {prefix}/[.format]GETlist{basename}-list POSTcreate + {prefix}/{methodname}/[.format]GET, or as specified by `methods` argument`@list_route` decorated method{basename}-{methodname} {prefix}/{lookup}/[.format]GETretrieve{basename}-detail PUTupdate PATCHpartial_update DELETEdestroy - {prefix}/{lookup}/{methodname}/[.format]GET@detail_route decorated method{basename}-{methodname} - POST@detail_route decorated method + {prefix}/{lookup}/{methodname}/[.format]GET, or as specified by `methods` argument`@detail_route` decorated method{basename}-{methodname} As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router. @@ -144,3 +146,4 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. [cite]: http://guides.rubyonrails.org/routing.html +[route-decorators]: viewsets.html#marking-extra-actions-for-routing \ No newline at end of file diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 95efc229f..9005e7cbc 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -61,7 +61,7 @@ There are two main advantages of using a `ViewSet` class over using a `View` cla Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout. -## Marking extra methods for routing +## Marking extra actions for routing The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below: From 8acee2e626746f3096c49b3ebb13aaf7dc882917 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 21:02:22 +0100 Subject: [PATCH 0063/1652] Commenting link/action decorators as pending deprecation --- rest_framework/decorators.py | 51 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 1ca176f2c..18e41a18d 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -108,6 +108,31 @@ def permission_classes(permission_classes): return decorator +def detail_route(methods=['get'], **kwargs): + """ + Used to mark a method on a ViewSet that should be routed for detail requests. + """ + def decorator(func): + func.bind_to_methods = methods + func.detail = True + func.kwargs = kwargs + return func + return decorator + + +def list_route(methods=['get'], **kwargs): + """ + Used to mark a method on a ViewSet that should be routed for list requests. + """ + def decorator(func): + func.bind_to_methods = methods + func.detail = False + func.kwargs = kwargs + return func + return decorator + +# These are now pending deprecation, in favor of `detail_route` and `list_route`. + def link(**kwargs): """ Used to mark a method on a ViewSet that should be routed for detail GET requests. @@ -133,28 +158,4 @@ def action(methods=['post'], **kwargs): func.detail = True func.kwargs = kwargs return func - return decorator - - -def detail_route(methods=['get'], **kwargs): - """ - Used to mark a method on a ViewSet that should be routed for detail requests. - """ - def decorator(func): - func.bind_to_methods = methods - func.detail = True - func.kwargs = kwargs - return func - return decorator - - -def list_route(methods=['get'], **kwargs): - """ - Used to mark a method on a ViewSet that should be routed for list requests. - """ - def decorator(func): - func.bind_to_methods = methods - func.detail = False - func.kwargs = kwargs - return func - return decorator + return decorator \ No newline at end of file From 28ff6fb1ec02b7a04c4a0db54885f3735b6dd43f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 21:44:47 +0100 Subject: [PATCH 0064/1652] Only HTML forms should have implicit default False for boolean fields --- rest_framework/fields.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index add9d224d..07779c472 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -16,6 +16,7 @@ from django.core import validators from django.core.exceptions import ValidationError from django.conf import settings from django.db.models.fields import BLANK_CHOICE_DASH +from django.http import QueryDict from django.forms import widgets from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ @@ -399,10 +400,15 @@ class BooleanField(WritableField): } empty = False - # 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 field_from_native(self, data, files, field_name, into): + # HTML checkboxes do not explicitly represent unchecked as `False` + # we deal with that here... + if isinstance(data, QueryDict): + self.default = False + + return super(BooleanField, self).field_from_native( + data, files, field_name, into + ) def from_native(self, value): if value in ('true', 't', 'True', '1'): From f84d4951bfcc8887d57ca5fa0321cfdbb18a9b6d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 21:46:34 +0100 Subject: [PATCH 0065/1652] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 52abfc08e..dfc4bfbb9 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. +* Bugfix: `required=True` argument fixed for boolean serializer fields. ### 2.3.7 From 1bf712341508b5d9aa07fb62f55b7e495278fabf Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 20 Aug 2013 16:24:13 -0300 Subject: [PATCH 0066/1652] improving documentation about object level permissions #1049 --- docs/api-guide/generic-views.md | 5 ++++- docs/api-guide/permissions.md | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 32a4feef4..2a585f9c2 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -108,7 +108,10 @@ For example: filter = {} for field in self.multiple_lookup_fields: filter[field] = self.kwargs[field] - return get_object_or_404(queryset, **filter) + + obj = get_object_or_404(queryset, **filter) + self.check_object_permissions(self.request, obj) + return obj #### `get_serializer_class(self)` diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index c6372f981..bb7343aff 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -28,6 +28,13 @@ If you're writing your own views and want to enforce object level permissions, you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object. This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions. +For example: + + def get_object(self): + obj = get_object_or_404(self.get_queryset()) + self.check_object_permissions(self.request, obj) + return obj + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. From 5e40e50f2b187fe2ff2e8ee63b4e39ece42f1521 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 19:46:09 +0100 Subject: [PATCH 0067/1652] Include import paths throughout docs. Closes #1051. Thanks to @pydanny for the report. --- docs/api-guide/authentication.md | 14 ++++++++++++++ docs/api-guide/content-negotiation.md | 6 ++++++ docs/api-guide/fields.md | 13 ++++++++----- docs/api-guide/filtering.md | 14 ++++++++++++++ docs/api-guide/generic-views.md | 5 +++++ docs/api-guide/pagination.md | 7 ++++++- docs/api-guide/parsers.md | 4 ++++ docs/api-guide/permissions.md | 6 ++++++ docs/api-guide/relations.md | 11 ++++++----- docs/api-guide/renderers.md | 9 +++++++-- docs/api-guide/reverse.md | 4 ++-- docs/api-guide/routers.md | 7 +++++++ docs/api-guide/serializers.md | 7 +++++++ docs/api-guide/status-codes.md | 1 + docs/api-guide/testing.md | 21 ++++++++++++++++++--- docs/api-guide/throttling.md | 4 ++++ docs/api-guide/viewsets.md | 9 +++++++++ 17 files changed, 124 insertions(+), 18 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index b1ab46227..f30b16ed5 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -46,6 +46,11 @@ The default authentication schemes may be set globally, using the `DEFAULT_AUTHE You can also set the authentication scheme on a per-view or per-viewset basis, using the `APIView` class based views. + from rest_framework.authentication import SessionAuthentication, BasicAuthentication + from rest_framework.permissions import IsAuthenticated + from rest_framework.response import Response + from rest_framework.views import APIView + class ExampleView(APIView): authentication_classes = (SessionAuthentication, BasicAuthentication) permission_classes = (IsAuthenticated,) @@ -157,11 +162,16 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. + from django.dispatch import receiver + from rest_framework.authtoken.models import Token + @receiver(post_save, sender=User) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) +Note that you'll want to ensure you place this code snippet in an installed `models.py` module, or some other location that will be imported by Django on startup. + If you've already created some users, you can generate tokens for all existing users like this: from django.contrib.auth.models import User @@ -336,6 +346,10 @@ If the `.authenticate_header()` method is not overridden, the authentication sch The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'. + from django.contrib.auth.models import User + from rest_framework import authentication + from rest_framework import exceptions + class ExampleAuthentication(authentication.BaseAuthentication): def authenticate(self, request): username = request.META.get('X_USERNAME') diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index 2a7742786..94dd59cac 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -54,6 +54,8 @@ The `select_renderer()` method should return a two-tuple of (renderer instance, The following is a custom content negotiation class which ignores the client request when selecting the appropriate parser or renderer. + from rest_framework.negotiation import BaseContentNegotiation + class IgnoreClientContentNegotiation(BaseContentNegotiation): def select_parser(self, request, parsers): """ @@ -77,6 +79,10 @@ The default content negotiation class may be set globally, using the `DEFAULT_CO You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views. + from myapp.negotiation import IgnoreClientContentNegotiation + from rest_framework.response import Response + from rest_framework.views import APIView + class NoNegotiationView(APIView): """ An example view that does not perform content negotiation. diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d69730c98..962c49e2a 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -78,6 +78,9 @@ A generic, **read-only** field. You can use this field for any attribute that d For example, using the following model. + from django.db import models + from django.utils.timezone import now + class Account(models.Model): owner = models.ForeignKey('auth.user') name = models.CharField(max_length=100) @@ -85,13 +88,14 @@ For example, using the following model. payment_expiry = models.DateTimeField() def has_expired(self): - now = datetime.datetime.now() - return now > self.payment_expiry + return now() > self.payment_expiry A serializer definition that looked like this: + from rest_framework import serializers + class AccountSerializer(serializers.HyperlinkedModelSerializer): - expired = Field(source='has_expired') + expired = serializers.Field(source='has_expired') class Meta: fields = ('url', 'owner', 'name', 'expired') @@ -125,12 +129,11 @@ The `ModelField` class is generally intended for internal use, but can be used b This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: - from rest_framework import serializers from django.contrib.auth.models import User from django.utils.timezone import now + from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): - days_since_joined = serializers.SerializerMethodField('get_days_since_joined') class Meta: diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 05c997a39..649462da7 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -20,6 +20,10 @@ You can do so by filtering based on the value of `request.user`. For example: + from myapp.models import Purchase + from myapp.serializers import PurchaseSerializer + from rest_framework import generics + class PurchaseList(generics.ListAPIView) serializer_class = PurchaseSerializer @@ -90,6 +94,11 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class based views. + from django.contrib.auth.models import User + from myapp.serializers import UserSerializer + from rest_framework import filters + from rest_framework import generics + class UserListView(generics.ListAPIView): queryset = User.objects.all() serializer = UserSerializer @@ -150,6 +159,11 @@ This will automatically create a `FilterSet` class for the given fields, and wil For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example: + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + class ProductFilter(django_filters.FilterSet): min_price = django_filters.NumberFilter(lookup_type='gte') max_price = django_filters.NumberFilter(lookup_type='lte') diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 32a4feef4..7f754df8c 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -17,6 +17,11 @@ If the generic views don't suit the needs of your API, you can drop down to usin Typically when using the generic views, you'll override the view, and set several class attributes. + from django.contrib.auth.models import User + from myapp.serializers import UserSerializer + from rest_framework import generics + from rest_framework.permissions import IsAdminUser + class UserList(generics.ListCreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 912ce41bd..ca0174b76 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -13,6 +13,7 @@ REST framework includes a `PaginationSerializer` class that makes it easy to ret Let's start by taking a look at an example from the Django documentation. from django.core.paginator import Paginator + objects = ['john', 'paul', 'george', 'ringo'] paginator = Paginator(objects, 2) page = paginator.page(1) @@ -22,6 +23,7 @@ Let's start by taking a look at an example from the Django documentation. At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results. from rest_framework.pagination import PaginationSerializer + serializer = PaginationSerializer(instance=page) serializer.data # {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']} @@ -114,6 +116,9 @@ You can also override the name used for the object list field, by setting the `r For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this. + from rest_framework import pagination + from rest_framework import serializers + class LinksSerializer(serializers.Serializer): next = pagination.NextPageField(source='*') prev = pagination.PreviousPageField(source='*') @@ -135,7 +140,7 @@ To have your custom pagination serializer be used by default, use the `DEFAULT_P Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view: - class PaginatedListView(ListAPIView): + class PaginatedListView(generics.ListAPIView): model = ExampleModel pagination_serializer_class = CustomPaginationSerializer paginate_by = 10 diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 5bd79a317..d3c42b1c2 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -37,6 +37,10 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE You can also set the renderers used for an individual view, or viewset, using the `APIView` class based views. + from rest_framework.parsers import YAMLParser + from rest_framework.response import Response + from rest_framework.views import APIView + class ExampleView(APIView): """ A view that can accept POST requests with YAML content. diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index c6372f981..a3d86ed49 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -47,6 +47,10 @@ If not specified, this setting defaults to allowing unrestricted access: You can also set the authentication policy on a per-view, or per-viewset basis, using the `APIView` class based views. + from rest_framework.permissions import IsAuthenticated + from rest_framework.responses import Response + from rest_framework.views import APIView + class ExampleView(APIView): permission_classes = (IsAuthenticated,) @@ -157,6 +161,8 @@ For more details see the [2.2 release announcement][2.2-announcement]. The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted. + from rest_framework import permissions + class BlacklistPermission(permissions.BasePermission): """ Global permission check for blacklisted IPs. diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 829a3c548..aa14bc725 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -76,7 +76,7 @@ This field is read only. For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = PrimaryKeyRelatedField(many=True, read_only=True) + tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Album @@ -110,8 +110,8 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = HyperlinkedRelatedField(many=True, read_only=True, - view_name='track-detail') + tracks = serializers.HyperlinkedRelatedField(many=True, read_only=True, + view_name='track-detail') class Meta: model = Album @@ -148,7 +148,8 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: class AlbumSerializer(serializers.ModelSerializer): - tracks = SlugRelatedField(many=True, read_only=True, slug_field='title') + tracks = serializers.SlugRelatedField(many=True, read_only=True, + slug_field='title') class Meta: model = Album @@ -183,7 +184,7 @@ When using `SlugRelatedField` as a read-write field, you will normally want to e This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer: class AlbumSerializer(serializers.HyperlinkedModelSerializer): - track_listing = HyperlinkedIdentityField(view_name='track-list') + track_listing = serializers.HyperlinkedIdentityField(view_name='track-list') class Meta: model = Album diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index bb3d20159..7fc1fc1fd 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -30,11 +30,16 @@ The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CL You can also set the renderers used for an individual view, or viewset, using the `APIView` class based views. + from django.contrib.auth.models import User + from rest_framework.renderers import JSONRenderer, YAMLRenderer + from rest_framework.response import Response + from rest_framework.views import APIView + class UserCountView(APIView): """ - A view that returns the count of active users, in JSON or JSONp. + A view that returns the count of active users, in JSON or YAML. """ - renderer_classes = (JSONRenderer, JSONPRenderer) + renderer_classes = (JSONRenderer, YAMLRenderer) def get(self, request, format=None): user_count = User.objects.filter(active=True).count() diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 942623666..383eca4ce 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -27,13 +27,13 @@ Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except t You should **include the request as a keyword argument** to the function, for example: - import datetime from rest_framework.reverse import reverse from rest_framework.views import APIView + from django.utils.timezone import now class APIRootView(APIView): def get(self, request): - year = datetime.datetime.now().year + year = now().year data = { ... 'year-summary-url': reverse('year-summary', args=[year], request=request) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 072a2e79a..fb48197e9 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -14,6 +14,8 @@ REST framework adds support for automatic URL routing to Django, and provides yo Here's an example of a simple URL conf, that uses `DefaultRouter`. + from rest_framework import routers + router = routers.SimpleRouter() router.register(r'users', UserViewSet) router.register(r'accounts', AccountViewSet) @@ -40,6 +42,9 @@ The example above would generate the following URL patterns: Any methods on the viewset decorated with `@link` or `@action` will also be routed. For example, given a method like this on the `UserViewSet` class: + from myapp.permissions import IsAdminOrIsSelf + from rest_framework.decorators import action + @action(permission_classes=[IsAdminOrIsSelf]) def set_password(self, request, pk=None): ... @@ -120,6 +125,8 @@ The arguments to the `Route` named tuple are: The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention. + from rest_framework.routers import Route, SimpleRouter + class ReadOnlyRouter(SimpleRouter): """ A router for read-only APIs, which doesn't use trailing slashes. diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index bbc8d019d..d9fd46437 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -28,6 +28,8 @@ We'll declare a serializer that we can use to serialize and deserialize `Comment Declaring a serializer looks very similar to declaring a form: + from rest_framework import serializers + class CommentSerializer(serializers.Serializer): email = serializers.EmailField() content = serializers.CharField(max_length=200) @@ -59,6 +61,8 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments. At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`. + from rest_framework.renderers import JSONRenderer + json = JSONRenderer().render(serializer.data) json # '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}' @@ -67,6 +71,9 @@ At this point we've translated the model instance into Python native datatypes. Deserialization is similar. First we parse a stream into Python native datatypes... + from StringIO import StringIO + from rest_framework.parsers import JSONParser + stream = StringIO(json) data = JSONParser().parse(stream) diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index db2e059c3..409f659b2 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -9,6 +9,7 @@ Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable. from rest_framework import status + from rest_framework.response import Response def empty_view(self): content = {'please move along': 'nothing to see here'} diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 92f8d54aa..b3880f8f0 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -16,6 +16,8 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. + from rest_framework.test import APIRequestFactory + # Using the standard RequestFactory API to create a form POST request factory = APIRequestFactory() request = factory.post('/notes/', {'title': 'new idea'}) @@ -49,6 +51,8 @@ For example, using `APIRequestFactory`, you can make a form PUT request like so: Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself: + from django.test.client import encode_multipart, RequestFactory + factory = RequestFactory() data = {'title': 'remember to email dave'} content = encode_multipart('BoUnDaRyStRiNg', data) @@ -72,6 +76,12 @@ To forcibly authenticate a request, use the `force_authenticate()` method. The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set. +For example, when forcibly authenticating using a token, you might do something like the following: + + user = User.objects.get(username='olivia') + request = factory.get('/accounts/django-superstars/') + force_authenticate(request, user=user, token=user.token) + --- **Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called. @@ -105,6 +115,8 @@ Extends [Django's existing `Client` class][client]. The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: + from rest_framework.test import APIClient + client = APIClient() client.post('/notes/', {'title': 'new idea'}, format='json') @@ -131,8 +143,11 @@ The `login` method is appropriate for testing APIs that use session authenticati The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. + from rest_framework.authtoken.models import Token + from rest_framework.test import APIClient + # Include an appropriate `Authorization:` header on all requests. - token = Token.objects.get(username='lauren') + token = Token.objects.get(user__username='lauren') client = APIClient() client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) @@ -190,10 +205,10 @@ You can use any of REST framework's test case classes as you would for the regul Ensure we can create a new account object. """ url = reverse('account-list') - data = {'name': 'DabApps'} + expected = {'name': 'DabApps'} response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, data) + self.assertEqual(response.data, expected) --- diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 56f32f58a..42f9c228d 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -43,6 +43,10 @@ The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `mi You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. + from rest_framework.response import Response + from rest_framework.throttling import UserRateThrottle + from rest_framework.views import APIView + class ExampleView(APIView): throttle_classes = (UserRateThrottle,) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 0c68afb0b..61f9d2f85 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -19,6 +19,12 @@ Typically, rather than explicitly registering the views in a viewset in the urlc Let's define a simple viewset that can be used to list or retrieve all the users in the system. + from django.contrib.auth.models import User + from django.shortcuts import get_object_or_404 + from myapps.serializers import UserSerializer + from rest_framework import viewsets + from rest_framewor.responses import Response + class UserViewSet(viewsets.ViewSet): """ A simple ViewSet that for listing or retrieving users. @@ -41,6 +47,9 @@ If we need to, we can bind this viewset into two separate views, like so: Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated. + from myapp.views import UserViewSet + from rest_framework.routers import DefaultRouter + router = DefaultRouter() router.register(r'users', UserViewSet) urlpatterns = router.urls From cf6ae397db1353370fef05df99a8d321806a6f58 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 19:57:30 +0100 Subject: [PATCH 0068/1652] Docs tweaking around `check_object_permissions` --- docs/api-guide/generic-views.md | 2 ++ docs/api-guide/permissions.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 281a0481f..931cae542 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -118,6 +118,8 @@ For example: self.check_object_permissions(self.request, obj) return obj +Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. + #### `get_serializer_class(self)` Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 6b80a98c2..12aa4c18b 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -25,7 +25,8 @@ Object level permissions are run by REST framework's generic views when `.get_ob As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object. If you're writing your own views and want to enforce object level permissions, -you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object. +or if you override the `get_object` method on a generic view, then you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object. + This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions. For example: From 4338e1e43fb6725cc89f3390943d9b86880bd678 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 20:00:35 +0100 Subject: [PATCH 0069/1652] Added @filipeximenes For work on #1050. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e9b600749..a894ee7cc 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -158,6 +158,7 @@ The following people have helped make REST framework great. * Martin Clement - [martync] * Jeremy Satterfield - [jsatt] * Christopher Paolini - [chrispaolini] +* Filipe A Ximenes - [filipeximenes] Many thanks to everyone who's contributed to the project. @@ -352,3 +353,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [martync]: https://github.com/martync [jsatt]: https://github.com/jsatt [chrispaolini]: https://github.com/chrispaolini +[filipeximenes]: https://github.com/filipeximenes From 815ef50735f50c7aff5255e60f1b484e75178e87 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 21:18:46 +0100 Subject: [PATCH 0070/1652] If page size query param <= 0, just use default page size. Closes #1028 --- rest_framework/generics.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 874a142c8..bcd62bf9d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,6 +14,15 @@ from rest_framework.settings import api_settings import warnings +def strict_positive_int(integer_string): + """ + Cast a string to a strictly positive integer. + """ + ret = int(integer_string) + if ret <= 0: + raise ValueError() + return ret + def get_object_or_404(queryset, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to raise 404 @@ -198,7 +207,7 @@ class GenericAPIView(views.APIView): if self.paginate_by_param: query_params = self.request.QUERY_PARAMS try: - return int(query_params[self.paginate_by_param]) + return strict_positive_int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass From 2bcad32dcb57ae9419f6a901e081f0dcdc1a6f87 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 21:22:12 +0100 Subject: [PATCH 0071/1652] If page size query param <= 0, just use default page size. Closes #1028. --- rest_framework/generics.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 99e9782e2..5ecf6310d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,6 +14,15 @@ from rest_framework.settings import api_settings import warnings +def strict_positive_int(integer_string): + """ + Cast a string to a strictly positive integer. + """ + ret = int(integer_string) + if ret <= 0: + raise ValueError() + return ret + def get_object_or_404(queryset, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to raise 404 @@ -135,7 +144,7 @@ class GenericAPIView(views.APIView): page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) page = page_kwarg or page_query_param or 1 try: - page_number = int(page) + page_number = strict_positive_int(page) except ValueError: if page == 'last': page_number = paginator.num_pages From 44ceef841543877a700c3fb4a0f84dfecbad0cbb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 21:30:25 +0100 Subject: [PATCH 0072/1652] Updating 2.4.0 release notes --- .travis.yml | 1 + docs/topics/release-notes.md | 5 +- rest_framework/compat.py | 2 +- rest_framework/six.py | 389 ----------------------------------- tox.ini | 2 + 5 files changed, 8 insertions(+), 391 deletions(-) delete mode 100644 rest_framework/six.py diff --git a/.travis.yml b/.travis.yml index 6a4532411..f8640db2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ install: - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" + - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install six --use-mirrors; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" - export PYTHONPATH=. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 52abfc08e..f3bb19c67 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,9 +40,12 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.4.0 +* `@detail_route` and `@list_route` decorators replace `@action` and `@link`. +* `six` no longer bundled. For Django <= 1.4.1, install `six` package. * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. +* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off. ### 2.3.7 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index baee3a9c2..178a697f3 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -14,7 +14,7 @@ from django.conf import settings try: from django.utils import six except ImportError: - from rest_framework import six + import six # location of patterns, url, include changes in 1.4 onwards try: diff --git a/rest_framework/six.py b/rest_framework/six.py deleted file mode 100644 index 9e3823128..000000000 --- a/rest_framework/six.py +++ /dev/null @@ -1,389 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.2.0" - - -# True if we are running on Python 3. -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform == "java": - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) - # This is a bit ugly, but it avoids running this again. - delattr(tp, self.name) - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - - -class _MovedItems(types.ModuleType): - """Lazy loading of moved objects""" - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("reload_module", "__builtin__", "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("winreg", "_winreg"), -] -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) -del attr - -moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_code = "__code__" - _func_defaults = "__defaults__" - - _iterkeys = "keys" - _itervalues = "values" - _iteritems = "items" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_code = "func_code" - _func_defaults = "func_defaults" - - _iterkeys = "iterkeys" - _itervalues = "itervalues" - _iteritems = "iteritems" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -if PY3: - def get_unbound_function(unbound): - return unbound - - Iterator = object - - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) -else: - def get_unbound_function(unbound): - return unbound.im_func - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) - - -def iterkeys(d): - """Return an iterator over the keys of a dictionary.""" - return iter(getattr(d, _iterkeys)()) - -def itervalues(d): - """Return an iterator over the values of a dictionary.""" - return iter(getattr(d, _itervalues)()) - -def iteritems(d): - """Return an iterator over the (key, value) pairs of a dictionary.""" - return iter(getattr(d, _iteritems)()) - - -if PY3: - def b(s): - return s.encode("latin-1") - def u(s): - return s - if sys.version_info[1] <= 1: - def int2byte(i): - return bytes((i,)) - else: - # This is about 2x faster than the implementation above on 3.2+ - int2byte = operator.methodcaller("to_bytes", 1, "big") - import io - StringIO = io.StringIO - BytesIO = io.BytesIO -else: - def b(s): - return s - def u(s): - return unicode(s, "unicode_escape") - int2byte = chr - import StringIO - StringIO = BytesIO = StringIO.StringIO -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -if PY3: - import builtins - exec_ = getattr(builtins, "exec") - - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - - print_ = getattr(builtins, "print") - del builtins - -else: - def exec_(code, globs=None, locs=None): - """Execute code in a namespace.""" - if globs is None: - frame = sys._getframe(1) - globs = frame.f_globals - if locs is None: - locs = frame.f_locals - del frame - elif locs is None: - locs = globs - exec("""exec code in globs, locs""") - - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - - def print_(*args, **kwargs): - """The new-style print function.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - def write(data): - if not isinstance(data, basestring): - data = str(data) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) - -_add_doc(reraise, """Reraise an exception.""") - - -def with_metaclass(meta, base=object): - """Create a base class with a metaclass.""" - return meta("NewBase", (base,), {}) - - -### Additional customizations for Django ### - -if PY3: - _iterlists = "lists" - _assertRaisesRegex = "assertRaisesRegex" -else: - _iterlists = "iterlists" - _assertRaisesRegex = "assertRaisesRegexp" - - -def iterlists(d): - """Return an iterator over the values of a MultiValueDict.""" - return getattr(d, _iterlists)() - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -add_move(MovedModule("_dummy_thread", "dummy_thread")) -add_move(MovedModule("_thread", "thread")) diff --git a/tox.ini b/tox.ini index aa97fd350..6ec400ddf 100644 --- a/tox.ini +++ b/tox.ini @@ -91,6 +91,7 @@ deps = django==1.3.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + six [testenv:py2.6-django1.3] basepython = python2.6 @@ -100,3 +101,4 @@ deps = django==1.3.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + six From f631f55f8ebdf3d4e478aa5ca435ad36e86bee0f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 21:35:17 +0100 Subject: [PATCH 0073/1652] Tweak comment --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 178a697f3..66be96a62 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -10,7 +10,7 @@ import django from django.core.exceptions import ImproperlyConfigured from django.conf import settings -# Try to import six from Django, fallback to included `six`. +# Try to import six from Django, fallback to external `six` package. try: from django.utils import six except ImportError: From ec5955101b4b15b828ac5b6fc54e8d10f2a7c64a Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 22 Aug 2013 12:40:12 -0300 Subject: [PATCH 0074/1652] Update parsers.md s/renderers/parsers/ --- docs/api-guide/parsers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index d3c42b1c2..1030fcb65 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -34,7 +34,7 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE ) } -You can also set the renderers used for an individual view, or viewset, +You can also set the parsers used for an individual view, or viewset, using the `APIView` class based views. from rest_framework.parsers import YAMLParser From b8561f41238e0ad79b2cc823518a93314d987979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 22 Aug 2013 17:52:22 +0200 Subject: [PATCH 0075/1652] Add @ramiro for #1056 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index a894ee7cc..16ea78c42 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -159,6 +159,7 @@ The following people have helped make REST framework great. * Jeremy Satterfield - [jsatt] * Christopher Paolini - [chrispaolini] * Filipe A Ximenes - [filipeximenes] +* Ramiro Morales - [ramiro] Many thanks to everyone who's contributed to the project. @@ -354,3 +355,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [jsatt]: https://github.com/jsatt [chrispaolini]: https://github.com/chrispaolini [filipeximenes]: https://github.com/filipeximenes +[ramiro]: https://github.com/ramiro From 19a774f97292444a48c5b7521e1b0c0ea48b6502 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 11:21:45 +0100 Subject: [PATCH 0076/1652] force_authenticate(None) also clears session info. Closes #1055. --- docs/topics/release-notes.md | 1 + rest_framework/test.py | 2 ++ rest_framework/tests/test_testing.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index dfc4bfbb9..af90b1ea3 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Bugfix: `required=True` argument fixed for boolean serializer fields. +* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. ### 2.3.7 diff --git a/rest_framework/test.py b/rest_framework/test.py index a18f5a293..234d10a4a 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -134,6 +134,8 @@ class APIClient(APIRequestFactory, DjangoClient): """ self.handler._force_user = user self.handler._force_token = token + if user is None: + self.logout() # Also clear any possible session info if required def request(self, **kwargs): # Ensure that any credentials set get added to every request. diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 49d45fc29..48b8956b5 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -17,8 +17,18 @@ def view(request): }) +@api_view(['GET', 'POST']) +def session_view(request): + active_session = request.session.get('active_session', False) + request.session['active_session'] = True + return Response({ + 'active_session': active_session + }) + + urlpatterns = patterns('', url(r'^view/$', view), + url(r'^session-view/$', session_view), ) @@ -46,6 +56,26 @@ class TestAPITestClient(TestCase): response = self.client.get('/view/') self.assertEqual(response.data['user'], 'example') + def test_force_authenticate_with_sessions(self): + """ + Setting `.force_authenticate()` forcibly authenticates each request. + """ + user = User.objects.create_user('example', 'example@example.com') + self.client.force_authenticate(user) + + # First request does not yet have an active session + response = self.client.get('/session-view/') + self.assertEqual(response.data['active_session'], False) + + # Subsequant requests have an active session + response = self.client.get('/session-view/') + self.assertEqual(response.data['active_session'], True) + + # Force authenticating as `None` should also logout the user session. + self.client.force_authenticate(None) + response = self.client.get('/session-view/') + self.assertEqual(response.data['active_session'], False) + def test_csrf_exempt_by_default(self): """ By default, the test client is CSRF exempt. From dba602781355f6ee0cbc34775209cd37a52ca4d4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 11:27:12 +0100 Subject: [PATCH 0077/1652] Add missing period. --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index b3880f8f0..35c1f7660 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -2,7 +2,7 @@ # Testing -> Code without tests is broken as designed +> Code without tests is broken as designed. > > — [Jacob Kaplan-Moss][cite] From 95b2bf50fbb9b95facebb23812bbbb2e27a76035 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 12:03:54 +0100 Subject: [PATCH 0078/1652] Add validation error test when passing non-file to FileField --- rest_framework/tests/test_files.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index 495c2a7fd..c13c38b86 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -69,3 +69,14 @@ class FileSerializerTests(TestCase): self.assertTrue(serializer.is_valid()) self.assertEqual(serializer.object.created, uploaded_file.created) self.assertIsNone(serializer.object.file) + + def test_validation_error_with_non_file(self): + """ + Passing non-files should raise a validation error. + """ + now = datetime.datetime.now() + errmsg = 'No file was submitted. Check the encoding type on the form.' + + serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'}) + self.assertFalse(serializer.is_valid()) + self.assertEqual(serializer.errors, {'file': [errmsg]}) From f2b190e3740e50508b3da1ec52048a3c90add3b1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 12:06:23 +0100 Subject: [PATCH 0079/1652] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index af90b1ea3..626831cbf 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -45,6 +45,7 @@ You can determine your currently installed version using `pip freeze`: * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. +* Bugfix: Client sending emptry string instead of file now clears `FileField`. ### 2.3.7 From e7927e9bca5bc0d0ac3b528e68244c713c5df97f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 13:35:50 +0100 Subject: [PATCH 0080/1652] Extra docs on PATCH with no object. --- rest_framework/mixins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 59d644694..426865ff9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -149,6 +149,8 @@ class UpdateModelMixin(object): # return None. self.check_permissions(clone_request(self.request, 'POST')) else: + # PATCH requests where the object does not exist should still + # return a 404 response. raise def pre_save(self, obj): From 7bbe0f868f02e3da902c6e0d11bf5b10bc55f616 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 13:37:25 +0100 Subject: [PATCH 0081/1652] Added @krzysiekj For work on #1034. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 16ea78c42..e6d09bc23 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -160,6 +160,7 @@ The following people have helped make REST framework great. * Christopher Paolini - [chrispaolini] * Filipe A Ximenes - [filipeximenes] * Ramiro Morales - [ramiro] +* Krzysztof Jurewicz - [krzysiekj] Many thanks to everyone who's contributed to the project. @@ -356,3 +357,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [chrispaolini]: https://github.com/chrispaolini [filipeximenes]: https://github.com/filipeximenes [ramiro]: https://github.com/ramiro +[krzysiekj]: https://github.com/krzysiekj From e03854ba6a74428675c40d469a7768cc5131035f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 14:06:14 +0100 Subject: [PATCH 0082/1652] Tweaks to display nested data in empty serializers --- rest_framework/relations.py | 9 +++++++-- rest_framework/serializers.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index edaf76d6e..7408758e6 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -134,9 +134,9 @@ class RelatedField(WritableField): value = obj for component in source.split('.'): - value = get_component(value, component) if value is None: break + value = get_component(value, component) except ObjectDoesNotExist: return None @@ -567,8 +567,13 @@ class HyperlinkedIdentityField(Field): May raise a `NoReverseMatch` if the `view_name` and `lookup_field` attributes are not configured to correctly match the URL conf. """ - lookup_field = getattr(obj, self.lookup_field) + lookup_field = getattr(obj, self.lookup_field, None) kwargs = {self.lookup_field: lookup_field} + + # Handle unsaved object case + if lookup_field is None: + return None + try: return reverse(view_name, kwargs=kwargs, request=request, format=format) except NoReverseMatch: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2b260c256..22525964c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -338,9 +338,9 @@ class BaseSerializer(WritableField): value = obj for component in source.split('.'): - value = get_component(value, component) if value is None: - break + return self.to_native(None) + value = get_component(value, component) except ObjectDoesNotExist: return None From 0966a2680ba02e6a4586bd2777ed593fcc66a453 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 14:38:31 +0100 Subject: [PATCH 0083/1652] First pass at HTMLFormRenderer --- rest_framework/renderers.py | 97 +++++++++++-------- .../templates/rest_framework/base.html | 10 +- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1006e26cf..a73b2d732 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -316,6 +316,59 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): return data +class HTMLFormRenderer(BaseRenderer): + template = 'rest_framework/form.html' + + def serializer_to_form_fields(self, serializer): + fields = {} + for k, v in serializer.get_fields().items(): + if getattr(v, 'read_only', True): + continue + + kwargs = {} + kwargs['required'] = v.required + + #if getattr(v, 'queryset', None): + # kwargs['queryset'] = v.queryset + + if getattr(v, 'choices', None) is not None: + kwargs['choices'] = v.choices + + if getattr(v, 'regex', None) is not None: + kwargs['regex'] = v.regex + + if getattr(v, 'widget', None): + widget = copy.deepcopy(v.widget) + kwargs['widget'] = widget + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + + if getattr(v, 'label', None) is not None: + kwargs['label'] = v.label + + if getattr(v, 'help_text', None) is not None: + kwargs['help_text'] = v.help_text + + fields[k] = v.form_field_class(**kwargs) + + return fields + + def render(self, serializer, obj, request): + 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(str("OnTheFlyForm"), (forms.Form,), fields) + data = (obj is not None) and serializer.data or None + form_instance = OnTheFlyForm(data) + + template = loader.get_template(self.template) + context = RequestContext(request, {'form': form_instance}) + + return template.render(context) + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. @@ -371,41 +424,6 @@ class BrowsableAPIRenderer(BaseRenderer): return False # Doesn't have permissions return True - def serializer_to_form_fields(self, serializer): - fields = {} - for k, v in serializer.get_fields().items(): - if getattr(v, 'read_only', True): - continue - - kwargs = {} - kwargs['required'] = v.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(v, 'choices', None) is not None: - kwargs['choices'] = v.choices - - if getattr(v, 'regex', None) is not None: - kwargs['regex'] = v.regex - - if getattr(v, 'widget', None): - widget = copy.deepcopy(v.widget) - kwargs['widget'] = widget - - if getattr(v, 'default', None) is not None: - kwargs['initial'] = v.default - - if getattr(v, 'label', None) is not None: - kwargs['label'] = v.label - - if getattr(v, 'help_text', None) is not None: - kwargs['help_text'] = v.help_text - - fields[k] = v.form_field_class(**kwargs) - - return fields - def _get_form(self, view, method, request): # We need to impersonate a request with the correct method, # so that eg. any dynamic get_serializer_class methods return the @@ -447,14 +465,7 @@ class BrowsableAPIRenderer(BaseRenderer): return 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(str("OnTheFlyForm"), (forms.Form,), fields) - data = (obj is not None) and serializer.data or None - form_instance = OnTheFlyForm(data) - return form_instance + return HTMLFormRenderer().render(serializer, obj, request) def get_raw_data_form(self, view, method, request, media_types): """ diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 51f9c2916..6ae47563d 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -136,9 +136,9 @@ {% if post_form %}
    {% with form=post_form %} -
    +
    - {% include "rest_framework/form.html" %} + {{ post_form }}
    @@ -174,16 +174,14 @@
    {% if put_form %}
    - {% with form=put_form %} - +
    - {% include "rest_framework/form.html" %} + {{ put_form }}
    - {% endwith %}
    {% endif %}
    From 005f475c6af023cc7c75cf38d3a89e22638e5d84 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 14:58:06 +0100 Subject: [PATCH 0084/1652] Don't consume .json style suffixes with routers. When trailing slash is false, the lookup regex should not consume '.' characters. Fixes #1057. --- rest_framework/routers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 930011d39..3fee1e494 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -189,7 +189,11 @@ class SimpleRouter(BaseRouter): Given a viewset, return the portion of URL regex that is used to match against a single instance. """ - base_regex = '(?P<{lookup_field}>[^/]+)' + if self.trailing_slash: + base_regex = '(?P<{lookup_field}>[^/]+)' + else: + # Don't consume `.json` style suffixes + base_regex = '(?P<{lookup_field}>[^/.]+)' lookup_field = getattr(viewset, 'lookup_field', 'pk') return base_regex.format(lookup_field=lookup_field) From 1c935cd3d271efd06f1621c9dddb9e1cd0333e20 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 15:18:47 +0100 Subject: [PATCH 0085/1652] Fix failing test for router with no trailing slash --- rest_framework/tests/test_routers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 5fcccb741..e723f7d45 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -146,7 +146,7 @@ class TestTrailingSlashRemoved(TestCase): self.urls = self.router.urls def test_urls_can_have_trailing_slash_removed(self): - expected = ['^notes$', '^notes/(?P[^/]+)$'] + expected = ['^notes$', '^notes/(?P[^/.]+)$'] for idx in range(len(expected)): self.assertEqual(expected[idx], self.urls[idx].regex.pattern) From 10d386ec6a4822402b5ffea46bdd9e7d72db519b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 16:10:20 +0100 Subject: [PATCH 0086/1652] Cleanup and dealing with empty form data. --- rest_framework/relations.py | 2 + rest_framework/renderers.py | 103 ++++++++++++++++++---------------- rest_framework/serializers.py | 3 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7408758e6..3ad16ee5e 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -244,6 +244,8 @@ class PrimaryKeyRelatedField(RelatedField): source = self.source or field_name queryset = obj for component in source.split('.'): + if queryset is None: + return [] queryset = get_component(queryset, component) # Forward relationship diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a73b2d732..a8670546b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -319,52 +319,53 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): class HTMLFormRenderer(BaseRenderer): template = 'rest_framework/form.html' - def serializer_to_form_fields(self, serializer): + def data_to_form_fields(self, data): fields = {} - for k, v in serializer.get_fields().items(): - if getattr(v, 'read_only', True): + for key, val in data.fields.items(): + if getattr(val, 'read_only', True): continue kwargs = {} - kwargs['required'] = v.required + kwargs['required'] = val.required #if getattr(v, 'queryset', None): # kwargs['queryset'] = v.queryset - if getattr(v, 'choices', None) is not None: - kwargs['choices'] = v.choices + if getattr(val, 'choices', None) is not None: + kwargs['choices'] = val.choices - if getattr(v, 'regex', None) is not None: - kwargs['regex'] = v.regex + if getattr(val, 'regex', None) is not None: + kwargs['regex'] = val.regex - if getattr(v, 'widget', None): - widget = copy.deepcopy(v.widget) + if getattr(val, 'widget', None): + widget = copy.deepcopy(val.widget) kwargs['widget'] = widget - if getattr(v, 'default', None) is not None: - kwargs['initial'] = v.default + if getattr(val, 'default', None) is not None: + kwargs['initial'] = val.default - if getattr(v, 'label', None) is not None: - kwargs['label'] = v.label + if getattr(val, 'label', None) is not None: + kwargs['label'] = val.label - if getattr(v, 'help_text', None) is not None: - kwargs['help_text'] = v.help_text + if getattr(val, 'help_text', None) is not None: + kwargs['help_text'] = val.help_text - fields[k] = v.form_field_class(**kwargs) + fields[key] = val.form_field_class(**kwargs) return fields - def render(self, serializer, obj, request): - fields = self.serializer_to_form_fields(serializer) + def render(self, data, accepted_media_type=None, renderer_context=None): + self.renderer_context = renderer_context or {} + request = renderer_context['request'] # Creating an on the fly form see: # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields) - data = (obj is not None) and serializer.data or None - form_instance = OnTheFlyForm(data) + fields = self.data_to_form_fields(data) + DynamicForm = type(str('DynamicForm'), (forms.Form,), fields) + data = None if data.empty else data template = loader.get_template(self.template) - context = RequestContext(request, {'form': form_instance}) + context = RequestContext(request, {'form': DynamicForm(data)}) return template.render(context) @@ -377,6 +378,7 @@ class BrowsableAPIRenderer(BaseRenderer): format = 'api' template = 'rest_framework/api.html' charset = 'utf-8' + form_renderer_class = HTMLFormRenderer def get_default_renderer(self, view): """ @@ -424,7 +426,7 @@ class BrowsableAPIRenderer(BaseRenderer): return False # Doesn't have permissions return True - def _get_form(self, view, method, request): + def _get_rendered_html_form(self, view, method, request): # We need to impersonate a request with the correct method, # so that eg. any dynamic get_serializer_class methods return the # correct form for each method. @@ -432,27 +434,16 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) view.request = request try: - return self.get_form(view, method, request) + return self.get_rendered_html_form(view, method, request) finally: view.request = restore - def _get_raw_data_form(self, view, method, request, media_types): - # We need to impersonate a request with the correct method, - # so that eg. any dynamic get_serializer_class methods return the - # correct form for each method. - restore = view.request - request = clone_request(request, method) - view.request = request - try: - return self.get_raw_data_form(view, method, request, media_types) - finally: - view.request = restore - - def get_form(self, view, method, request): + def get_rendered_html_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. + Return a string representing a rendered HTML form, possibly bound to + either the input or output data. + + In the absence of the View having an associated form then return None. """ obj = getattr(view, 'object', None) if not self.show_form_for_method(view, method, request, obj): @@ -465,7 +456,21 @@ class BrowsableAPIRenderer(BaseRenderer): return serializer = view.get_serializer(instance=obj) - return HTMLFormRenderer().render(serializer, obj, request) + data = serializer.data + form_renderer = self.form_renderer_class() + return form_renderer.render(data, self.accepted_media_type, self.renderer_context) + + def _get_raw_data_form(self, view, method, request, media_types): + # We need to impersonate a request with the correct method, + # so that eg. any dynamic get_serializer_class methods return the + # correct form for each method. + restore = view.request + request = clone_request(request, method) + view.request = request + try: + return self.get_raw_data_form(view, method, request, media_types) + finally: + view.request = restore def get_raw_data_form(self, view, method, request, media_types): """ @@ -520,8 +525,8 @@ class BrowsableAPIRenderer(BaseRenderer): """ Render the HTML for the browsable API representation. """ - accepted_media_type = accepted_media_type or '' - renderer_context = renderer_context or {} + self.accepted_media_type = accepted_media_type or '' + self.renderer_context = renderer_context or {} view = renderer_context['view'] request = renderer_context['request'] @@ -531,11 +536,11 @@ class BrowsableAPIRenderer(BaseRenderer): renderer = self.get_default_renderer(view) content = self.get_content(renderer, data, accepted_media_type, renderer_context) - put_form = self._get_form(view, 'PUT', request) - post_form = self._get_form(view, 'POST', request) - patch_form = self._get_form(view, 'PATCH', request) - delete_form = self._get_form(view, 'DELETE', request) - options_form = self._get_form(view, 'OPTIONS', request) + put_form = self._get_rendered_html_form(view, 'PUT', request) + post_form = self._get_rendered_html_form(view, 'POST', request) + patch_form = self._get_rendered_html_form(view, 'PATCH', request) + delete_form = self._get_rendered_html_form(view, 'DELETE', request) + options_form = self._get_rendered_html_form(view, 'OPTIONS', request) raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types) raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fde06d834..97e0a005f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -300,7 +300,8 @@ class BaseSerializer(WritableField): Serialize objects -> primitives. """ ret = self._dict_class() - ret.fields = {} + ret.fields = self._dict_class() + ret.empty = obj is None for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) From e23d5888522f98c30418452c0f833cf11589e0c1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 16:16:41 +0100 Subject: [PATCH 0087/1652] Adding standard renderer attributes and documenting --- rest_framework/renderers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a8670546b..9885c8ddc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -317,7 +317,18 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): class HTMLFormRenderer(BaseRenderer): + """ + Renderers serializer data into an HTML form. + + If the serializer was instantiated without an object then this will + return an HTML form not bound to any object, + otherwise it will return an HTML form with the appropriate initial data + populated from the object. + """ + media_type = 'text/html' + format = 'form' template = 'rest_framework/form.html' + charset = 'utf-8' def data_to_form_fields(self, data): fields = {} From 436e66a42db21b52fd5e1582011d2f0f7f81f9c7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 16:45:55 +0100 Subject: [PATCH 0088/1652] JSON responses should not include a charset --- docs/api-guide/renderers.md | 9 ++++++--- rest_framework/renderers.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 7fc1fc1fd..d46d05686 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -88,7 +88,7 @@ The client may additionally include an `'indent'` media type parameter, in which **.format**: `'.json'` -**.charset**: `utf-8` +**.charset**: `None` ## UnicodeJSONRenderer @@ -110,7 +110,7 @@ Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][r **.format**: `'.json'` -**.charset**: `utf-8` +**.charset**: `None` ## JSONPRenderer @@ -295,12 +295,15 @@ By default renderer classes are assumed to be using the `UTF-8` encoding. To us Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding. -If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string. +If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. + +In some cases you may also want to set the `render_style` attribute to `'binary'`. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string. class JPEGRenderer(renderers.BaseRenderer): media_type = 'image/jpeg' format = 'jpg' charset = None + render_style = 'binary' def render(self, data, media_type=None, renderer_context=None): return data diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1006e26cf..c87014e27 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -36,6 +36,7 @@ class BaseRenderer(object): media_type = None format = None charset = 'utf-8' + render_style = 'text' def render(self, data, accepted_media_type=None, renderer_context=None): raise NotImplemented('Renderer class requires .render() to be implemented') @@ -51,16 +52,17 @@ class JSONRenderer(BaseRenderer): format = 'json' encoder_class = encoders.JSONEncoder ensure_ascii = True - charset = 'utf-8' - # Note that JSON encodings must be utf-8, utf-16 or utf-32. + charset = None + # JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32. # See: http://www.ietf.org/rfc/rfc4627.txt + # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/ def render(self, data, accepted_media_type=None, renderer_context=None): """ Render `data` into JSON. """ if data is None: - return '' + return bytes() # If 'indent' is provided in the context, then pretty print the result. # E.g. If we're being called by the BrowsableAPIRenderer. @@ -85,13 +87,12 @@ class JSONRenderer(BaseRenderer): # and may (or may not) be unicode. # On python 3.x json.dumps() returns unicode strings. if isinstance(ret, six.text_type): - return bytes(ret.encode(self.charset)) + return bytes(ret.encode('utf-8')) return ret class UnicodeJSONRenderer(JSONRenderer): ensure_ascii = False - charset = 'utf-8' """ Renderer which serializes to JSON. Does *not* apply JSON's character escaping for non-ascii characters. @@ -108,6 +109,7 @@ class JSONPRenderer(JSONRenderer): format = 'jsonp' callback_parameter = 'callback' default_callback = 'callback' + charset = 'utf-8' def get_callback(self, renderer_context): """ @@ -348,7 +350,10 @@ class BrowsableAPIRenderer(BaseRenderer): renderer_context['indent'] = 4 content = renderer.render(data, accepted_media_type, renderer_context) - if renderer.charset is None: + render_style = getattr(renderer, 'render_style', 'text') + assert render_style in ['text', 'binary'], 'Expected .render_style ' \ + '"text" or "binary", but got "%s"' % render_style + if render_style == 'binary': return '[%d bytes of binary content]' % len(content) return content From be0f5850c398b7f7397d66eaed26d6b78163b259 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 16:51:34 +0100 Subject: [PATCH 0089/1652] Extra docs --- rest_framework/renderers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c07b1652c..b30f2ea9f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -326,6 +326,8 @@ class HTMLFormRenderer(BaseRenderer): return an HTML form not bound to any object, otherwise it will return an HTML form with the appropriate initial data populated from the object. + + Note that rendering of field and form errors is not currently supported. """ media_type = 'text/html' format = 'form' @@ -368,6 +370,18 @@ class HTMLFormRenderer(BaseRenderer): return fields def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render serializer data and return an HTML form, as a string. + """ + # The HTMLFormRenderer currently uses something of a hack to render + # the content, by translating each of the serializer fields into + # an html form field, creating a dynamic form using those fields, + # and then rendering that form. + + # This isn't strictly neccessary, as we could render the serilizer + # fields to HTML directly. The implementation is historical and will + # likely change at some point. + self.renderer_context = renderer_context or {} request = renderer_context['request'] From c7847ebc45f38e4d735b77c54ad1a55c87242fac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Aug 2013 17:10:50 +0100 Subject: [PATCH 0090/1652] Docs for HTMLFormRenderer --- docs/api-guide/renderers.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index d46d05686..c116ceda6 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -212,6 +212,18 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES See also: `TemplateHTMLRenderer` +## HTMLFormRenderer + +Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `
    ` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages. + +**.media_type**: `text/html` + +**.format**: `'.form'` + +**.charset**: `utf-8` + +**.template**: `'rest_framework/form.html'` + ## BrowsableAPIRenderer Renders data into HTML for the Browsable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page. @@ -222,6 +234,8 @@ Renders data into HTML for the Browsable API. This renderer will determine whic **.charset**: `utf-8` +**.template**: `'rest_framework/api.html'` + #### Customizing BrowsableAPIRenderer By default the response content will be rendered with the highest priority renderer apart from `BrowseableAPIRenderer`. If you need to customize this behavior, for example to use HTML as the default return format, but use JSON in the browsable API, you can do so by overriding the `get_default_renderer()` method. For example: From 9d3fae27fd9c3236dfd9c26ae9b830deb6fa4e9b Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Fri, 23 Aug 2013 16:48:32 +0000 Subject: [PATCH 0091/1652] parameterize identity field class to allow for easier subclassing --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa3447..abb969410 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -903,6 +903,7 @@ class HyperlinkedModelSerializer(ModelSerializer): _options_class = HyperlinkedModelSerializerOptions _default_view_name = '%(model_name)s-detail' _hyperlink_field_class = HyperlinkedRelatedField + _hyperlink_identify_field_class = HyperlinkedIdentityField def get_default_fields(self): fields = super(HyperlinkedModelSerializer, self).get_default_fields() @@ -911,7 +912,7 @@ class HyperlinkedModelSerializer(ModelSerializer): self.opts.view_name = self._get_default_view_name(self.opts.model) if 'url' not in fields: - url_field = HyperlinkedIdentityField( + url_field = self._hyperlink_identify_field_class( view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) From 53d60543c3a5c637491aaeb887269627ce9179ab Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 25 Aug 2013 20:31:04 +0100 Subject: [PATCH 0092/1652] Add warning against HTMLFormRenderer --- docs/api-guide/renderers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index c116ceda6..657377d92 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -216,6 +216,8 @@ See also: `TemplateHTMLRenderer` Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages. +Note that the template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely. + **.media_type**: `text/html` **.format**: `'.form'` From 23400357895dbbf1e91fa720af080eb1b6e23a00 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 25 Aug 2013 20:48:10 +0100 Subject: [PATCH 0093/1652] Added @ericbuehl For pull request #1058. Thank you! :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6d09bc23..09d91480b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -161,6 +161,7 @@ The following people have helped make REST framework great. * Filipe A Ximenes - [filipeximenes] * Ramiro Morales - [ramiro] * Krzysztof Jurewicz - [krzysiekj] +* Eric Buehl - [ericbuehl] Many thanks to everyone who's contributed to the project. @@ -358,3 +359,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [filipeximenes]: https://github.com/filipeximenes [ramiro]: https://github.com/ramiro [krzysiekj]: https://github.com/krzysiekj +[ericbuehl]: https://github.com/ericbuehl From afee470aca28c73fb0f107e99fdb98e5a2d5a135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20=C3=98llegaard?= Date: Mon, 26 Aug 2013 11:02:01 +0200 Subject: [PATCH 0094/1652] More information on how actions are mapped to URLs in viewsets --- docs/api-guide/viewsets.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 61f9d2f85..2e65b7a43 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -142,6 +142,10 @@ The `@action` decorator will route `POST` requests by default, but may also acce @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): ... + +The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` + + --- # API Reference From 316de3a8a314162e3d6ec081344eabca3a4d91b9 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 26 Aug 2013 20:05:36 +0400 Subject: [PATCH 0095/1652] Added max_paginate_by parameter --- rest_framework/generics.py | 10 ++++-- rest_framework/settings.py | 1 + rest_framework/tests/test_pagination.py | 46 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 5ecf6310d..33affee88 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -56,6 +56,7 @@ class GenericAPIView(views.APIView): # Pagination settings paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM + max_paginate_by = api_settings.MAX_PAGINATE_BY pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS page_kwarg = 'page' @@ -207,11 +208,16 @@ class GenericAPIView(views.APIView): if self.paginate_by_param: query_params = self.request.QUERY_PARAMS try: - return int(query_params[self.paginate_by_param]) + paginate_by_param = int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass + else: + if self.max_paginate_by: + return min(self.max_paginate_by, paginate_by_param) + else: + return paginate_by_param - return self.paginate_by + return min(self.max_paginate_by, self.paginate_by) or self.paginate_by def get_serializer_class(self): """ diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 7d25e5131..b8e40bfa5 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -68,6 +68,7 @@ DEFAULTS = { # Pagination 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, + 'MAX_PAGINATE_BY': None, # View configuration 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index 85d4640ea..cbed16047 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -42,6 +42,16 @@ class PaginateByParamView(generics.ListAPIView): paginate_by_param = 'page_size' +class MaxPaginateByView(generics.ListAPIView): + """ + View for testing custom max_paginate_by usage + """ + model = BasicModel + paginate_by = 5 + max_paginate_by = 3 + paginate_by_param = 'page_size' + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -313,6 +323,42 @@ class TestCustomPaginateByParam(TestCase): self.assertEqual(response.data['results'], self.data[:5]) +class TestMaxPaginateByParam(TestCase): + """ + Tests for list views with max_paginate_by kwarg + """ + + def setUp(self): + """ + Create 13 BasicModel instances. + """ + for i in range(13): + BasicModel(text=i).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = MaxPaginateByView.as_view() + + def test_max_paginate_by(self): + """ + If max_paginate_by is set and it less than paginate_by, new kwarg should limit requests for review. + """ + request = factory.get('/?page_size=10') + response = self.view(request).render() + self.assertEqual(response.data['count'], 13) + self.assertEqual(response.data['results'], self.data[:3]) + + def test_max_paginate_by_without_page_size_param(self): + """ + If max_paginate_by is set, new kwarg should limit requests for review. + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEqual(response.data['results'], self.data[:3]) + + ### Tests for context in pagination serializers class CustomField(serializers.Field): From c3e273a90e08cc5215f9e9ea0508b62809b181a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 09:16:20 +0100 Subject: [PATCH 0096/1652] Added @kristianoellegaard. For docs addition #1059. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 09d91480b..9b13131a6 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -162,6 +162,7 @@ The following people have helped make REST framework great. * Ramiro Morales - [ramiro] * Krzysztof Jurewicz - [krzysiekj] * Eric Buehl - [ericbuehl] +* Kristian Øllegaard - [kristianoellegaard] Many thanks to everyone who's contributed to the project. @@ -360,3 +361,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ramiro]: https://github.com/ramiro [krzysiekj]: https://github.com/krzysiekj [ericbuehl]: https://github.com/ericbuehl +[kristianoellegaard]: https://github.com/kristianoellegaard From 8d590ebfded0968e458f8e3a87efabec8384586e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 11:22:19 +0100 Subject: [PATCH 0097/1652] First hacky pass at displaying raw data --- rest_framework/parsers.py | 9 +++++++-- rest_framework/renderers.py | 27 +++++++++++++++++++++++-- rest_framework/serializers.py | 2 ++ rest_framework/tests/test_serializer.py | 2 +- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 96bfac84a..c635505a4 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -10,9 +10,9 @@ from django.core.files.uploadhandler import StopFutureHandlers from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter -from rest_framework.compat import yaml, etree +from rest_framework.compat import etree, six, yaml from rest_framework.exceptions import ParseError -from rest_framework.compat import six +from rest_framework.renderers import UnicodeJSONRenderer import json import datetime import decimal @@ -32,6 +32,8 @@ class BaseParser(object): media_type = None + supports_html_forms = False + def parse(self, stream, media_type=None, parser_context=None): """ Given a stream to read from, return the parsed representation. @@ -47,6 +49,7 @@ class JSONParser(BaseParser): """ media_type = 'application/json' + renderer_class = UnicodeJSONRenderer def parse(self, stream, media_type=None, parser_context=None): """ @@ -91,6 +94,7 @@ class FormParser(BaseParser): """ media_type = 'application/x-www-form-urlencoded' + supports_html_forms = True def parse(self, stream, media_type=None, parser_context=None): """ @@ -109,6 +113,7 @@ class MultiPartParser(BaseParser): """ media_type = 'multipart/form-data' + supports_html_forms = True def parse(self, stream, media_type=None, parser_context=None): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b30f2ea9f..cc8de9590 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,7 +24,7 @@ from rest_framework.settings import api_settings from rest_framework.request import clone_request from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework import exceptions, parsers, status, VERSION +from rest_framework import exceptions, status, VERSION class BaseRenderer(object): @@ -482,7 +482,7 @@ class BrowsableAPIRenderer(BaseRenderer): if method in ('DELETE', '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: + if not getattr(view, 'get_serializer', None) or not any(parser.supports_html_forms for parser in view.parser_classes): return serializer = view.get_serializer(instance=obj) @@ -561,6 +561,29 @@ class BrowsableAPIRenderer(BaseRenderer): view = renderer_context['view'] request = renderer_context['request'] response = renderer_context['response'] + + obj = getattr(view, 'object', None) + if getattr(view, 'get_serializer', None): + serializer = view.get_serializer(instance=obj) + else: + serializer = None + + parsers = [] + for parser_class in view.parser_classes: + content = None + renderer_class = getattr(parser_class, 'renderer_class', None) + if renderer_class and serializer: + renderer = renderer_class() + context = renderer_context.copy() + context['indent'] = 4 + content = renderer.render(serializer.data, accepted_media_type, context) + print content + parsers.append({ + 'media_type': parser_class.media_type, + 'content': content + }) + + media_types = [parser.media_type for parser in view.parser_classes] renderer = self.get_default_renderer(view) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index abff68983..202d3a096 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,6 +304,8 @@ class BaseSerializer(WritableField): ret.empty = obj is None for field_name, field in self.fields.items(): + if obj is None and field.read_only: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..7c2a276ed 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -158,7 +158,7 @@ class BasicTests(TestCase): 'email': '', 'content': '', 'created': None, - 'sub_comment': '' + #'sub_comment': '' } self.assertEqual(serializer.data, expected) From dce47a11d3d65a697ea8aa322455d626190bc1e5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:32:13 +0100 Subject: [PATCH 0098/1652] Move settings into more sensible ordering --- rest_framework/settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 7d25e5131..2ee15ac7c 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -48,7 +48,6 @@ DEFAULTS = { ), 'DEFAULT_THROTTLE_CLASSES': ( ), - 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', @@ -69,14 +68,14 @@ DEFAULTS = { 'PAGINATE_BY': None, 'PAGINATE_BY_PARAM': None, - # View configuration - 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', - 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', - # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # View configuration + 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', + # Testing 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework.renderers.MultiPartRenderer', From b430503fa657330b606a9c632ea0decc4254163e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:32:33 +0100 Subject: [PATCH 0099/1652] Move exception handler out of main view --- rest_framework/views.py | 79 +++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 22 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 727a9f956..7cb71ccf8 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -15,8 +15,14 @@ from rest_framework.settings import api_settings from rest_framework.utils import formatting -def get_view_name(cls, suffix=None): - name = cls.__name__ +def get_view_name(view_cls, suffix=None): + """ + Given a view class, return a textual name to represent the view. + This name is used in the browsable API, and in OPTIONS responses. + + This function is the default for the `VIEW_NAME_FUNCTION` setting. + """ + name = view_cls.__name__ name = formatting.remove_trailing_string(name, 'View') name = formatting.remove_trailing_string(name, 'ViewSet') name = formatting.camelcase_to_spaces(name) @@ -25,14 +31,53 @@ def get_view_name(cls, suffix=None): return name -def get_view_description(cls, html=False): - description = cls.__doc__ or '' +def get_view_description(view_cls, html=False): + """ + Given a view class, return a textual description to represent the view. + This name is used in the browsable API, and in OPTIONS responses. + + This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting. + """ + description = view_cls.__doc__ or '' description = formatting.dedent(smart_text(description)) if html: return formatting.markup_description(description) return description +def exception_handler(exc): + """ + Returns the response that should be used for any given exception. + + By default we handle the REST framework `APIException`, and also + Django's builtin `Http404` and `PermissionDenied` exceptions. + + Any unhandled exceptions may return `None`, which will cause a 500 error + to be raised. + """ + if isinstance(exc, exceptions.APIException): + headers = {} + if getattr(exc, 'auth_header', None): + headers['WWW-Authenticate'] = exc.auth_header + if getattr(exc, 'wait', None): + headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + + return Response({'detail': exc.detail}, + status=exc.status_code, + headers=headers) + + elif isinstance(exc, Http404): + return Response({'detail': 'Not found'}, + status=status.HTTP_404_NOT_FOUND) + + elif isinstance(exc, PermissionDenied): + return Response({'detail': 'Permission denied'}, + status=status.HTTP_403_FORBIDDEN) + + # Note: Unhandled exceptions will raise a 500 error. + return None + + class APIView(View): settings = api_settings @@ -303,33 +348,23 @@ class APIView(View): Handle any exception that occurs, by returning an appropriate response, or re-raising the error. """ - if isinstance(exc, exceptions.Throttled) and exc.wait is not None: - # Throttle wait header - self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait - if isinstance(exc, (exceptions.NotAuthenticated, exceptions.AuthenticationFailed)): # WWW-Authenticate header for 401 responses, else coerce to 403 auth_header = self.get_authenticate_header(self.request) if auth_header: - self.headers['WWW-Authenticate'] = auth_header + exc.auth_header = auth_header else: exc.status_code = status.HTTP_403_FORBIDDEN - if isinstance(exc, exceptions.APIException): - return Response({'detail': exc.detail}, - status=exc.status_code, - exception=True) - elif isinstance(exc, Http404): - return Response({'detail': 'Not found'}, - status=status.HTTP_404_NOT_FOUND, - exception=True) - elif isinstance(exc, PermissionDenied): - return Response({'detail': 'Permission denied'}, - status=status.HTTP_403_FORBIDDEN, - exception=True) - raise + response = exception_handler(exc) + + if response is None: + raise + + response.exception = True + return response # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. From b54cbd292c5680f4de0e028ff1cb2a9ab1cd34ff Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:36:06 +0100 Subject: [PATCH 0100/1652] Use view.settings for API settings, to make testing easier. --- rest_framework/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 7cb71ccf8..4cff04224 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -79,8 +79,8 @@ def exception_handler(exc): class APIView(View): - settings = api_settings + # The following policies may be set at either globally, or per-view. renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES parser_classes = api_settings.DEFAULT_PARSER_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES @@ -88,6 +88,9 @@ class APIView(View): permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + # Allow dependancy injection of other settings to make testing easier. + settings = api_settings + @classmethod def as_view(cls, **initkwargs): """ @@ -178,7 +181,7 @@ class APIView(View): Return the view name, as used in OPTIONS responses and in the browsable API. """ - func = api_settings.VIEW_NAME_FUNCTION + func = self.settings.VIEW_NAME_FUNCTION return func(self.__class__, getattr(self, 'suffix', None)) def get_view_description(self, html=False): @@ -186,7 +189,7 @@ class APIView(View): Return some descriptive text for the view, as used in OPTIONS responses and in the browsable API. """ - func = api_settings.VIEW_DESCRIPTION_FUNCTION + func = self.settings.VIEW_DESCRIPTION_FUNCTION return func(self.__class__, html) # API policy instantiation methods From ea6eee304c230a9277fdc76f4ac91654e0019b7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Aug 2013 12:37:55 +0100 Subject: [PATCH 0101/1652] Note 'request.session' as available on requests. --- docs/api-guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 39a34fcfb..0696fedf6 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -117,7 +117,7 @@ For more information see the [browser enhancements documentation]. # Standard HttpRequest attributes -As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` dictionary is available as normal. +As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` and `request.session` dictionaries are available as normal. Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition. From 7fb3f078f0973acc1d108d8c617b26b6845599f7 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 27 Aug 2013 17:38:41 +0400 Subject: [PATCH 0102/1652] fix for python3 --- rest_framework/generics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 33affee88..ce6c462a3 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -212,12 +212,15 @@ class GenericAPIView(views.APIView): except (KeyError, ValueError): pass else: - if self.max_paginate_by: + if self.max_paginate_by is not None: return min(self.max_paginate_by, paginate_by_param) else: return paginate_by_param - return min(self.max_paginate_by, self.paginate_by) or self.paginate_by + if self.max_paginate_by: + return min(self.max_paginate_by, self.paginate_by) + else: + return self.paginate_by def get_serializer_class(self): """ From 4c53fb883fe719c3ca6244aeb8c405a24eb89a40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:52:38 +0100 Subject: [PATCH 0103/1652] Tweak MAX_PAGINATE_BY behavior in edge case. Always respect `paginate_by` settings if client does not specify page size. (Even if the developer has misconfigured, so that `paginate_by > max`.) --- rest_framework/generics.py | 20 ++++++++------------ rest_framework/tests/test_pagination.py | 11 ++++++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ce6c462a3..14feed204 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,13 +14,15 @@ from rest_framework.settings import api_settings import warnings -def strict_positive_int(integer_string): +def strict_positive_int(integer_string, cutoff=None): """ Cast a string to a strictly positive integer. """ ret = int(integer_string) if ret <= 0: raise ValueError() + if cutoff: + ret = min(ret, cutoff) return ret def get_object_or_404(queryset, **filter_kwargs): @@ -206,21 +208,15 @@ class GenericAPIView(views.APIView): PendingDeprecationWarning, stacklevel=2) if self.paginate_by_param: - query_params = self.request.QUERY_PARAMS try: - paginate_by_param = int(query_params[self.paginate_by_param]) + return strict_positive_int( + self.request.QUERY_PARAMS[self.paginate_by_param], + cutoff=self.max_paginate_by + ) except (KeyError, ValueError): pass - else: - if self.max_paginate_by is not None: - return min(self.max_paginate_by, paginate_by_param) - else: - return paginate_by_param - if self.max_paginate_by: - return min(self.max_paginate_by, self.paginate_by) - else: - return self.paginate_by + return self.paginate_by def get_serializer_class(self): """ diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index cbed16047..4170d4b64 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -47,8 +47,8 @@ class MaxPaginateByView(generics.ListAPIView): View for testing custom max_paginate_by usage """ model = BasicModel - paginate_by = 5 - max_paginate_by = 3 + paginate_by = 3 + max_paginate_by = 5 paginate_by_param = 'page_size' @@ -343,16 +343,17 @@ class TestMaxPaginateByParam(TestCase): def test_max_paginate_by(self): """ - If max_paginate_by is set and it less than paginate_by, new kwarg should limit requests for review. + If max_paginate_by is set, it should limit page size for the view. """ request = factory.get('/?page_size=10') response = self.view(request).render() self.assertEqual(response.data['count'], 13) - self.assertEqual(response.data['results'], self.data[:3]) + self.assertEqual(response.data['results'], self.data[:5]) def test_max_paginate_by_without_page_size_param(self): """ - If max_paginate_by is set, new kwarg should limit requests for review. + If max_paginate_by is set, but client does not specifiy page_size, + standard `paginate_by` behavior should be used. """ request = factory.get('/') response = self.view(request).render() From 848567a0cd4f244bfe9fd68e97ae672bd259fd92 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:55:49 +0100 Subject: [PATCH 0104/1652] Docs for `MAX_PAGINATE_BY` setting & view attribute. --- docs/api-guide/pagination.md | 8 +++++--- docs/api-guide/settings.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index ca0174b76..0829589f8 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -85,11 +85,12 @@ We could now use our pagination serializer in a view like this. The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely. -The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example. +The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example. REST_FRAMEWORK = { - 'PAGINATE_BY': 10, - 'PAGINATE_BY_PARAM': 'page_size' + 'PAGINATE_BY': 10, # Default to 10 + 'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`. + 'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`. } You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view. @@ -99,6 +100,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie serializer_class = ExampleModelSerializer paginate_by = 10 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. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index fe7925a5a..542e8c5fa 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -127,6 +127,35 @@ Default: `None` The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size. +For example, given the following settings: + + REST_FRAMEWORK = { + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size', + } + +A client would be able to modify the pagination size by using the `page_size` query parameter. For example: + + GET http://example.com/api/accounts?page_size=25 + +Default: `None` + +#### MAX_PAGINATE_BY + +The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied. + +For example, given the following settings: + + REST_FRAMEWORK = { + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size', + 'MAX_PAGINATE_BY': 100 + } + +A client request like the following would return a paginated list of up to 100 items. + + GET http://example.com/api/accounts?page_size=999 + Default: `None` --- From d7224afe5458f0b1016a80feec31c410c335dbce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 12:57:29 +0100 Subject: [PATCH 0105/1652] Added @alexander-akhmetov. For work on MAX_PAGINATE_BY, #1063. Thanks! :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9b13131a6..49f06e785 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -163,6 +163,7 @@ The following people have helped make REST framework great. * Krzysztof Jurewicz - [krzysiekj] * Eric Buehl - [ericbuehl] * Kristian Øllegaard - [kristianoellegaard] +* Alexander Akhmetov - [alexander-akhmetov] Many thanks to everyone who's contributed to the project. @@ -362,3 +363,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [krzysiekj]: https://github.com/krzysiekj [ericbuehl]: https://github.com/ericbuehl [kristianoellegaard]: https://github.com/kristianoellegaard +[alexander-akhmetov]: htttps://github.com/alexander-akhmetov \ No newline at end of file From 97b52156cc0e96c2edb7e1b176838bfd9c22321a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 13:34:14 +0100 Subject: [PATCH 0106/1652] Added `.cache` attribute on throttles. Closes #1066. More localised than a new settings key, and more flexible in that different throttles can use different behavior. Thanks to @chicheng for the report! :) --- docs/api-guide/throttling.md | 7 +++++++ rest_framework/throttling.py | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 42f9c228d..cc4692171 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -70,6 +70,13 @@ Or, if you're using the `@api_view` decorator with function based views. The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. +If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example: + + class CustomAnonRateThrottle(AnonRateThrottle): + cache = get_cache('alternate') + +You'll need to rememeber to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. + --- # API Reference diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 65b455930..8943f22c1 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -2,7 +2,7 @@ Provides various throttling policies. """ from __future__ import unicode_literals -from django.core.cache import cache +from django.core.cache import cache as default_cache from django.core.exceptions import ImproperlyConfigured from rest_framework.settings import api_settings import time @@ -39,6 +39,7 @@ class SimpleRateThrottle(BaseThrottle): Previous request information used for throttling is stored in the cache. """ + cache = default_cache timer = time.time cache_format = 'throtte_%(scope)s_%(ident)s' scope = None @@ -99,7 +100,7 @@ class SimpleRateThrottle(BaseThrottle): if self.key is None: return True - self.history = cache.get(self.key, []) + self.history = self.cache.get(self.key, []) self.now = self.timer() # Drop any requests from the history which have now passed the @@ -116,7 +117,7 @@ class SimpleRateThrottle(BaseThrottle): into the cache. """ self.history.insert(0, self.now) - cache.set(self.key, self.history, self.duration) + self.cache.set(self.key, self.history, self.duration) return True def throttle_failure(self): From 711fb9761c9722a83c083257d15c0ec8f755ca7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 13:35:27 +0100 Subject: [PATCH 0107/1652] Update release notes. --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 626831cbf..516efdc85 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,8 @@ You can determine your currently installed version using `pip freeze`: ### Master * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. +* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. +* Added `cache` attribute to throttles to allow overriding of default cache. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending emptry string instead of file now clears `FileField`. From 2d5e14a8d39a53c8a2e6d28fb8ae7debb5fbd388 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 15:32:41 +0100 Subject: [PATCH 0108/1652] Throttles now use HTTP_X_FORWARDED_FOR, falling back to REMOTE_ADDR to identify anonymous requests --- rest_framework/throttling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 8943f22c1..a946d837f 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -152,7 +152,9 @@ class AnonRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): return None # Only throttle unauthenticated requests. - ident = request.META.get('REMOTE_ADDR', None) + ident = request.META.get('HTTP_X_FORWARDED_FOR') + if ident is None: + ident = request.META.get('REMOTE_ADDR') return self.cache_format % { 'scope': self.scope, From 18007d68464b0cfab970e2a60aed0d41c4de4dac Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 28 Aug 2013 21:52:56 +0100 Subject: [PATCH 0109/1652] Simplifying raw data renderering support --- rest_framework/parsers.py | 10 +++------- rest_framework/renderers.py | 10 ++++++++-- rest_framework/serializers.py | 2 -- rest_framework/tests/test_serializer.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index c635505a4..23387dffb 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -12,7 +12,7 @@ from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter from rest_framework.compat import etree, six, yaml from rest_framework.exceptions import ParseError -from rest_framework.renderers import UnicodeJSONRenderer +from rest_framework import renderers import json import datetime import decimal @@ -32,8 +32,6 @@ class BaseParser(object): media_type = None - supports_html_forms = False - def parse(self, stream, media_type=None, parser_context=None): """ Given a stream to read from, return the parsed representation. @@ -49,7 +47,7 @@ class JSONParser(BaseParser): """ media_type = 'application/json' - renderer_class = UnicodeJSONRenderer + renderer_class = renderers.UnicodeJSONRenderer def parse(self, stream, media_type=None, parser_context=None): """ @@ -94,7 +92,6 @@ class FormParser(BaseParser): """ media_type = 'application/x-www-form-urlencoded' - supports_html_forms = True def parse(self, stream, media_type=None, parser_context=None): """ @@ -113,7 +110,6 @@ class MultiPartParser(BaseParser): """ media_type = 'multipart/form-data' - supports_html_forms = True def parse(self, stream, media_type=None, parser_context=None): """ @@ -134,7 +130,7 @@ class MultiPartParser(BaseParser): data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % six.u(exc)) + raise ParseError('Multipart form parse error - %s' % six.u(exc.strerror)) class XMLParser(BaseParser): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index cc8de9590..cd55c7830 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -21,7 +21,7 @@ from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml from rest_framework.settings import api_settings -from rest_framework.request import clone_request +from rest_framework.request import clone_request, is_form_media_type from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework import exceptions, status, VERSION @@ -482,7 +482,7 @@ class BrowsableAPIRenderer(BaseRenderer): if method in ('DELETE', 'OPTIONS'): return True # Don't actually need to return a form - if not getattr(view, 'get_serializer', None) or not any(parser.supports_html_forms for parser in view.parser_classes): + if not getattr(view, 'get_serializer', None) or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes): return serializer = view.get_serializer(instance=obj) @@ -565,11 +565,16 @@ class BrowsableAPIRenderer(BaseRenderer): obj = getattr(view, 'object', None) if getattr(view, 'get_serializer', None): serializer = view.get_serializer(instance=obj) + for field_name, field in serializer.fields.items(): + if field.read_only: + del serializer.fields[field_name] else: serializer = None parsers = [] for parser_class in view.parser_classes: + if is_form_media_type(parser_class.media_type): + continue content = None renderer_class = getattr(parser_class, 'renderer_class', None) if renderer_class and serializer: @@ -650,3 +655,4 @@ class MultiPartRenderer(BaseRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): return encode_multipart(self.BOUNDARY, data) + diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 202d3a096..abff68983 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,8 +304,6 @@ class BaseSerializer(WritableField): ret.empty = obj is None for field_name, field in self.fields.items(): - if obj is None and field.read_only: - continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 7c2a276ed..c24976603 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -158,7 +158,7 @@ class BasicTests(TestCase): 'email': '', 'content': '', 'created': None, - #'sub_comment': '' + 'sub_comment': '' } self.assertEqual(serializer.data, expected) From 2d37952e7872f7f69f588b02941ba6f5d739cdb6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 29 Aug 2013 00:50:54 +0200 Subject: [PATCH 0110/1652] Add composed-permissions entry to the api-guide. --- docs/api-guide/permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 12aa4c18b..a7bf15556 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -212,6 +212,10 @@ The following third party packages are also available. The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view. +## Composed Permissions + +The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md @@ -222,3 +226,4 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per [2.2-announcement]: ../topics/2.2-announcement.md [filtering]: filtering.md [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions +[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions From 94cd1369437f84adefb46462439b46dc5208ab1d Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 29 Aug 2013 17:35:15 +1200 Subject: [PATCH 0111/1652] add transform_ methods to serializers, which basically do the opposite of validate_ on a per-field basis. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa3447..8ba1b1959 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,6 +304,9 @@ class BaseSerializer(WritableField): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) + method = getattr(self, 'transform_%s' % field_name, None) + if callable(method): + value = method(obj, value) ret[key] = value ret.fields[key] = field return ret From bf07b8e616bd92e4ae3c2c09b198181d7075e6bd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 08:53:19 +0100 Subject: [PATCH 0112/1652] Better docs for customizing dynamic routes. Refs #908 --- docs/api-guide/routers.md | 81 +++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index f083b3d45..730fa876a 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -123,28 +123,87 @@ The arguments to the `Route` named tuple are: **initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links. +## Customizing dynamic routes + +You can also customize how the `@list_route` and `@detail_route` decorators are routed. +To route either or both of these decorators, include a `DynamicListRoute` and/or `DynamicDetailRoute` named tuple in the `.routes` list. + +The arguments to `DynamicListRoute` and `DynamicDetailRoute` are: + +**url**: A string representing the URL to be routed. May include the same format strings as `Route`, and additionally accepts the `{methodname}` and `{methodnamehyphen}` format strings. + +**name**: The name of the URL as used in `reverse` calls. May include the following format strings: `{basename}`, `{methodname}` and `{methodnamehyphen}`. + +**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. + ## Example The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention. - from rest_framework.routers import Route, SimpleRouter + from rest_framework.routers import Route, DynamicDetailRoute, SimpleRouter - class ReadOnlyRouter(SimpleRouter): + class CustomReadOnlyRouter(SimpleRouter): """ A router for read-only APIs, which doesn't use trailing slashes. """ routes = [ - Route(url=r'^{prefix}$', - mapping={'get': 'list'}, - name='{basename}-list', - initkwargs={'suffix': 'List'}), - Route(url=r'^{prefix}/{lookup}$', - mapping={'get': 'retrieve'}, - name='{basename}-detail', - initkwargs={'suffix': 'Detail'}) + Route( + url=r'^{prefix}$', + mapping={'get': 'list'}, + name='{basename}-list', + initkwargs={'suffix': 'List'} + ), + Route( + url=r'^{prefix}/{lookup}$', + mapping={'get': 'retrieve'}, + name='{basename}-detail', + initkwargs={'suffix': 'Detail'} + ), + DynamicDetailRoute( + url=r'^{prefix}/{lookup}/{methodnamehyphen}$', + name='{basename}-{methodnamehyphen}', + initkwargs={} + ) ] -The `SimpleRouter` class provides another example of setting the `.routes` attribute. +Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a simple viewset. + +`views.py`: + + class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + A viewset that provides the standard actions + """ + queryset = User.objects.all() + serializer_class = UserSerializer + lookup_field = 'username' + + @detail_route() + def group_names(self, request): + """ + Returns a list of all the group names that the given + user belongs to. + """ + user = self.get_object() + groups = user.groups.all() + return Response([group.name for group in groups]) + +`urls.py`: + + router = CustomReadOnlyRouter() + router.register('users', UserViewSet) + urlpatterns = router.urls + +The following mappings would be generated... + + + + + + +
    URLHTTP MethodActionURL Name
    /usersGETlistuser-list
    /users/{username}GETretrieveuser-detail
    /users/{username}/group-namesGETgroup_namesuser-group-names
    + +For another example of setting the `.routes` attribute, see the source code for the `SimpleRouter` class. ## Advanced custom routers From 6f8acb5a768d5d79efd7b39c5229bc4262e467a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 09:31:12 +0100 Subject: [PATCH 0113/1652] Added @niwibe For docs addition #1070 - Thanks! --- docs/topics/credits.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 49f06e785..47807a0ed 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -164,6 +164,7 @@ The following people have helped make REST framework great. * Eric Buehl - [ericbuehl] * Kristian Øllegaard - [kristianoellegaard] * Alexander Akhmetov - [alexander-akhmetov] +* Andrey Antukh - [niwibe] Many thanks to everyone who's contributed to the project. @@ -363,4 +364,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [krzysiekj]: https://github.com/krzysiekj [ericbuehl]: https://github.com/ericbuehl [kristianoellegaard]: https://github.com/kristianoellegaard -[alexander-akhmetov]: htttps://github.com/alexander-akhmetov \ No newline at end of file +[alexander-akhmetov]: https://github.com/alexander-akhmetov +[niwibe]: https://github.com/niwibe From 37e2720a40d39688f5e6ebb3b5c5aad68b8c25d4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 12:55:56 +0100 Subject: [PATCH 0114/1652] Add `override_method` context manager and cleanup. --- rest_framework/renderers.py | 149 ++++++++++++------------------------ rest_framework/request.py | 23 ++++++ 2 files changed, 73 insertions(+), 99 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index cd55c7830..34860f6ac 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -21,7 +21,7 @@ from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml from rest_framework.settings import api_settings -from rest_framework.request import clone_request, is_form_media_type +from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework import exceptions, status, VERSION @@ -456,18 +456,6 @@ class BrowsableAPIRenderer(BaseRenderer): return False # Doesn't have permissions return True - def _get_rendered_html_form(self, view, method, request): - # We need to impersonate a request with the correct method, - # so that eg. any dynamic get_serializer_class methods return the - # correct form for each method. - restore = view.request - request = clone_request(request, method) - view.request = request - try: - return self.get_rendered_html_form(view, method, request) - finally: - view.request = restore - def get_rendered_html_form(self, view, method, request): """ Return a string representing a rendered HTML form, possibly bound to @@ -475,32 +463,22 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): - return + with override_method(view, request, method) as request: + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): + return - if method in ('DELETE', 'OPTIONS'): - return True # Don't actually need to return a form + if method in ('DELETE', 'OPTIONS'): + return True # Don't actually need to return a form - if not getattr(view, 'get_serializer', None) or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes): - return + if (not getattr(view, 'get_serializer', None) + or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): + return - serializer = view.get_serializer(instance=obj) - data = serializer.data - form_renderer = self.form_renderer_class() - return form_renderer.render(data, self.accepted_media_type, self.renderer_context) - - def _get_raw_data_form(self, view, method, request, media_types): - # We need to impersonate a request with the correct method, - # so that eg. any dynamic get_serializer_class methods return the - # correct form for each method. - restore = view.request - request = clone_request(request, method) - view.request = request - try: - return self.get_raw_data_form(view, method, request, media_types) - finally: - view.request = restore + serializer = view.get_serializer(instance=obj) + data = serializer.data + form_renderer = self.form_renderer_class() + return form_renderer.render(data, self.accepted_media_type, self.renderer_context) def get_raw_data_form(self, view, method, request, media_types): """ @@ -508,39 +486,39 @@ class BrowsableAPIRenderer(BaseRenderer): via standard HTML forms. (Which are typically application/x-www-form-urlencoded) """ + with override_method(view, request, method) as request: + # If we're not using content overloading there's no point in supplying a generic form, + # as the view won't treat the form's value as the content of the request. + if not (api_settings.FORM_CONTENT_OVERRIDE + and api_settings.FORM_CONTENTTYPE_OVERRIDE): + return None - # If we're not using content overloading there's no point in supplying a generic form, - # as the view won't treat the form's value as the content of the request. - if not (api_settings.FORM_CONTENT_OVERRIDE - and api_settings.FORM_CONTENTTYPE_OVERRIDE): - return None + # Check permissions + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): + return - # Check permissions - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): - return + content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE + content_field = api_settings.FORM_CONTENT_OVERRIDE + choices = [(media_type, media_type) for media_type in media_types] + initial = media_types[0] - content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE - content_field = api_settings.FORM_CONTENT_OVERRIDE - choices = [(media_type, media_type) for media_type in media_types] - initial = media_types[0] + # NB. http://jacobian.org/writing/dynamic-form-generation/ + class GenericContentForm(forms.Form): + def __init__(self): + super(GenericContentForm, self).__init__() - # NB. http://jacobian.org/writing/dynamic-form-generation/ - class GenericContentForm(forms.Form): - def __init__(self): - super(GenericContentForm, self).__init__() + self.fields[content_type_field] = forms.ChoiceField( + label='Media type', + choices=choices, + initial=initial + ) + self.fields[content_field] = forms.CharField( + label='Content', + widget=forms.Textarea + ) - self.fields[content_type_field] = forms.ChoiceField( - label='Media type', - choices=choices, - initial=initial - ) - self.fields[content_field] = forms.CharField( - label='Content', - widget=forms.Textarea - ) - - return GenericContentForm() + return GenericContentForm() def get_name(self, view): return view.get_view_name() @@ -562,47 +540,20 @@ class BrowsableAPIRenderer(BaseRenderer): request = renderer_context['request'] response = renderer_context['response'] - obj = getattr(view, 'object', None) - if getattr(view, 'get_serializer', None): - serializer = view.get_serializer(instance=obj) - for field_name, field in serializer.fields.items(): - if field.read_only: - del serializer.fields[field_name] - else: - serializer = None - - parsers = [] - for parser_class in view.parser_classes: - if is_form_media_type(parser_class.media_type): - continue - content = None - renderer_class = getattr(parser_class, 'renderer_class', None) - if renderer_class and serializer: - renderer = renderer_class() - context = renderer_context.copy() - context['indent'] = 4 - content = renderer.render(serializer.data, accepted_media_type, context) - print content - parsers.append({ - 'media_type': parser_class.media_type, - 'content': content - }) - - media_types = [parser.media_type for parser in view.parser_classes] renderer = self.get_default_renderer(view) content = self.get_content(renderer, data, accepted_media_type, renderer_context) - put_form = self._get_rendered_html_form(view, 'PUT', request) - post_form = self._get_rendered_html_form(view, 'POST', request) - patch_form = self._get_rendered_html_form(view, 'PATCH', request) - delete_form = self._get_rendered_html_form(view, 'DELETE', request) - options_form = self._get_rendered_html_form(view, 'OPTIONS', request) + put_form = self.get_rendered_html_form(view, 'PUT', request) + post_form = self.get_rendered_html_form(view, 'POST', request) + patch_form = self.get_rendered_html_form(view, 'PATCH', request) + delete_form = self.get_rendered_html_form(view, 'DELETE', request) + options_form = self.get_rendered_html_form(view, 'OPTIONS', request) - raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types) - raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types) - raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types) + raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types) + raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types) + raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form name = self.get_name(view) diff --git a/rest_framework/request.py b/rest_framework/request.py index 919716f49..977d4d965 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -28,6 +28,29 @@ def is_form_media_type(media_type): base_media_type == 'multipart/form-data') +class override_method(object): + """ + A context manager that temporarily overrides the method on a request, + additionally setting the `view.request` attribute. + + Usage: + + with override_method(view, request, 'POST') as request: + ... # Do stuff with `view` and `request` + """ + def __init__(self, view, request, method): + self.view = view + self.request = request + self.method = method + + def __enter__(self): + self.view.request = clone_request(self.request, self.method) + return self.view.request + + def __exit__(self, *args, **kwarg): + self.view.request = self.request + + class Empty(object): """ Placeholder for unset attributes. From da9c17067c3150897da4cab149f12dee08768346 Mon Sep 17 00:00:00 2001 From: Brett Koonce Date: Thu, 29 Aug 2013 09:23:34 -0500 Subject: [PATCH 0115/1652] minor sp --- docs/api-guide/generic-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 931cae542..7185b6b68 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -73,7 +73,7 @@ The following attributes control the basic view behavior. **Pagination**: -The following attibutes are used to control pagination when used with list views. +The following attributes are used to control pagination when used with list views. * `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`. * `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`. @@ -135,7 +135,7 @@ For example: #### `get_paginate_by(self)` -Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set. +Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set. You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response. From 11071499a777ecfee6edfb7e92ecf9a12d35eeb7 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Thu, 29 Aug 2013 18:10:47 +0200 Subject: [PATCH 0116/1652] Make ChoiceField.from_native() follow IntegerField behaviour on empty values --- rest_framework/fields.py | 5 +++++ rest_framework/tests/test_fields.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3e0ca1a18..210c2537d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -514,6 +514,11 @@ class ChoiceField(WritableField): return True return False + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + return super(ChoiceField, self).from_native(value) + class EmailField(CharField): type_name = 'EmailField' diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ebccba7d1..34fbab9c9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -688,6 +688,14 @@ class ChoiceFieldTests(TestCase): f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + def test_from_native_empty(self): + """ + Make sure from_native() returns None on empty param. + """ + f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES) + result = f.from_native('') + self.assertEqual(result, None) + class EmailFieldTests(TestCase): """ From c7f3b8bebef33093d4e949f797565c4cbcd2695d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 17:23:26 +0100 Subject: [PATCH 0117/1652] Include serialized content in raw data form. --- rest_framework/renderers.py | 43 +++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 34860f6ac..077d6ebec 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -480,15 +480,16 @@ class BrowsableAPIRenderer(BaseRenderer): form_renderer = self.form_renderer_class() return form_renderer.render(data, self.accepted_media_type, self.renderer_context) - def get_raw_data_form(self, view, method, request, media_types): + def get_raw_data_form(self, view, method, request): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms. (Which are typically application/x-www-form-urlencoded) """ with override_method(view, request, method) as request: - # If we're not using content overloading there's no point in supplying a generic form, - # as the view won't treat the form's value as the content of the request. + # If we're not using content overloading there's no point in + # supplying a generic form, as the view won't treat the form's + # value as the content of the request. if not (api_settings.FORM_CONTENT_OVERRIDE and api_settings.FORM_CONTENTTYPE_OVERRIDE): return None @@ -498,8 +499,33 @@ class BrowsableAPIRenderer(BaseRenderer): if not self.show_form_for_method(view, method, request, obj): return + # If possible, serialize the initial content for the generic form + default_parser = view.parser_classes[0] + renderer_class = getattr(default_parser, 'renderer_class', None) + if (hasattr(view, 'get_serializer') and renderer_class): + # View has a serializer defined and parser class has a + # corresponding renderer that can be used to render the data. + + # Get a read-only version of the serializer + serializer = view.get_serializer(instance=obj) + for field_name, field in serializer.fields.items(): + if field.read_only: + del serializer.fields[field_name] + + # Render the raw data content + renderer = renderer_class() + accepted = self.accepted_media_type + context = self.renderer_context.copy().update({'indent': 4}) + content = renderer.render(serializer.data, accepted, context) + else: + content = None + + # Generate a generic form that includes a content type field, + # and a content field. content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE content_field = api_settings.FORM_CONTENT_OVERRIDE + + media_types = [parser.media_type for parser in view.parser_classes] choices = [(media_type, media_type) for media_type in media_types] initial = media_types[0] @@ -515,7 +541,8 @@ class BrowsableAPIRenderer(BaseRenderer): ) self.fields[content_field] = forms.CharField( label='Content', - widget=forms.Textarea + widget=forms.Textarea, + initial=content ) return GenericContentForm() @@ -540,8 +567,6 @@ class BrowsableAPIRenderer(BaseRenderer): request = renderer_context['request'] response = renderer_context['response'] - media_types = [parser.media_type for parser in view.parser_classes] - renderer = self.get_default_renderer(view) content = self.get_content(renderer, data, accepted_media_type, renderer_context) @@ -551,9 +576,9 @@ class BrowsableAPIRenderer(BaseRenderer): delete_form = self.get_rendered_html_form(view, 'DELETE', request) options_form = self.get_rendered_html_form(view, 'OPTIONS', request) - raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types) - raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types) - raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types) + raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) + raw_data_post_form = self.get_raw_data_form(view, 'POST', request) + raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form name = self.get_name(view) From 4b46de7dcebb31e9f7de11926ab5a4ecaa80c770 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 17:27:00 +0100 Subject: [PATCH 0118/1652] Added @diox for fix #1074. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 47807a0ed..b2d3d5d29 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -165,6 +165,7 @@ The following people have helped make REST framework great. * Kristian Øllegaard - [kristianoellegaard] * Alexander Akhmetov - [alexander-akhmetov] * Andrey Antukh - [niwibe] +* Mathieu Pillard - [diox] Many thanks to everyone who's contributed to the project. @@ -366,3 +367,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [kristianoellegaard]: https://github.com/kristianoellegaard [alexander-akhmetov]: https://github.com/alexander-akhmetov [niwibe]: https://github.com/niwibe +[diox]: https://github.com/diox From ac0fb01be3f33fab8d94117daf84a065f67bc343 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 17:27:08 +0100 Subject: [PATCH 0119/1652] Update release notes. --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 516efdc85..a901412f3 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -48,6 +48,7 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending emptry string instead of file now clears `FileField`. +* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. ### 2.3.7 From 556b4bbba9a735cd372d5b12e9fdccd256643cb2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:04:00 +0100 Subject: [PATCH 0120/1652] Added note on botbot IRC archives --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a0ae2984d..e0a2e911b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -200,7 +200,7 @@ To run the tests against all supported configurations, first install [the tox te ## Support -For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. +For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. [Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options. @@ -307,6 +307,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tox]: http://testrun.org/tox/latest/ [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[botbot]: https://botbot.me/freenode/restframework/ [stack-overflow]: http://stackoverflow.com/ [django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework [django-tag]: http://stackoverflow.com/questions/tagged/django From 1fa2d823cc9f2dcf301b0e3ce7f47acfcdfcb305 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:35:59 +0100 Subject: [PATCH 0121/1652] Preserve tab preference in cookies. --- .../static/rest_framework/js/default.js | 45 ++++++++++++++++++- .../templates/rest_framework/base.html | 4 +- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index c74829d7d..a57b1cb81 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -1,13 +1,56 @@ +function getCookie(c_name) +{ + // From http://www.w3schools.com/js/js_cookies.asp + var c_value = document.cookie; + var c_start = c_value.indexOf(" " + c_name + "="); + if (c_start == -1) { + c_start = c_value.indexOf(c_name + "="); + } + if (c_start == -1) { + c_value = null; + } else { + c_start = c_value.indexOf("=", c_start) + 1; + var c_end = c_value.indexOf(";", c_start); + if (c_end == -1) { + c_end = c_value.length; + } + c_value = unescape(c_value.substring(c_start,c_end)); + } + return c_value; +} + +// JSON highlighting. prettyPrint(); +// Bootstrap tooltips. $('.js-tooltip').tooltip({ delay: 1000 }); +// Deal with rounded tab styling after tab clicks. $('a[data-toggle="tab"]:first').on('shown', function (e) { $(e.target).parents('.tabbable').addClass('first-tab-active'); }); $('a[data-toggle="tab"]:not(:first)').on('shown', function (e) { $(e.target).parents('.tabbable').removeClass('first-tab-active'); }); -$('.form-switcher a:first').tab('show'); + +$('a[data-toggle="tab"]').click(function(){ + document.cookie="tab=" + this.name; +}); + +// Store tab preference in cookies & display appropriate tab on load. +var selectedTab = null; +var selectedTabName = getCookie('tab'); + +if (selectedTabName) { + selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); +} + +if (selectedTab && selectedTab.length > 0) { + // Display whichever tab is selected. + selectedTab.tab('show'); +} else { + // If no tab selected, display rightmost tab. + $('.form-switcher a:first').tab('show'); +} diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 6ae47563d..816970634 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -128,8 +128,8 @@
    {% if post_form %} {% endif %}
    From 44f8d1bef22d5f308fdbdfc29e6418816c3c27dd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:38:55 +0100 Subject: [PATCH 0122/1652] Fix tab preferences on PUT forms --- rest_framework/templates/rest_framework/base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 816970634..aa90e90c4 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -167,8 +167,8 @@
    {% if put_form %} {% endif %}
    From e4d2f54529bcf538be93da5770e05b88a32da1c7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:39:05 +0100 Subject: [PATCH 0123/1652] Fix indenting on raw data forms --- rest_framework/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 077d6ebec..525e44d57 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -515,7 +515,8 @@ class BrowsableAPIRenderer(BaseRenderer): # Render the raw data content renderer = renderer_class() accepted = self.accepted_media_type - context = self.renderer_context.copy().update({'indent': 4}) + context = self.renderer_context.copy() + context['indent'] = 4 content = renderer.render(serializer.data, accepted, context) else: content = None From 02b6836ee88498861521dfff743467b0456ad109 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 20:51:51 +0100 Subject: [PATCH 0124/1652] Fix breadcrumb view names --- rest_framework/utils/breadcrumbs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 0384faba3..e6690d170 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -8,8 +8,11 @@ def get_breadcrumbs(url): tuple of (name, url). """ + from rest_framework.settings import api_settings from rest_framework.views import APIView + view_name_func = api_settings.VIEW_NAME_FUNCTION + def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): """ Add tuples of (name, url) to the breadcrumbs list, @@ -28,8 +31,8 @@ def get_breadcrumbs(url): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - instance = view.cls() - name = instance.get_view_name() + suffix = getattr(view, 'suffix', None) + name = view_name_func(cls, suffix) breadcrumbs_list.insert(0, (name, prefix + url)) seen.append(view) From 2247fd68e9b3bbc91075a11f44db16fc40497b2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 21:24:29 +0100 Subject: [PATCH 0125/1652] Fix multipart error when used via content-type overloading --- rest_framework/parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 23387dffb..98fc03417 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -122,7 +122,8 @@ class MultiPartParser(BaseParser): parser_context = parser_context or {} request = parser_context['request'] encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) - meta = request.META + meta = request.META.copy() + meta['CONTENT_TYPE'] = media_type upload_handlers = request.upload_handlers try: @@ -130,7 +131,7 @@ class MultiPartParser(BaseParser): data, files = parser.parse() return DataAndFiles(data, files) except MultiPartParserError as exc: - raise ParseError('Multipart form parse error - %s' % six.u(exc.strerror)) + raise ParseError('Multipart form parse error - %s' % str(exc)) class XMLParser(BaseParser): From 3fba60e99c75dda4e14f7fe4f941d6fc84e4c986 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Aug 2013 09:02:54 +0100 Subject: [PATCH 0126/1652] Drop broken placeholder serializations. --- rest_framework/renderers.py | 13 ++++++++++--- rest_framework/serializers.py | 3 ++- rest_framework/tests/test_serializer.py | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 525e44d57..fca67eeeb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -338,6 +338,11 @@ class HTMLFormRenderer(BaseRenderer): fields = {} for key, val in data.fields.items(): if getattr(val, 'read_only', True): + # Don't include read-only fields. + continue + + if getattr(val, 'fields', None): + # Nested data not supported by HTML forms. continue kwargs = {} @@ -476,6 +481,7 @@ class BrowsableAPIRenderer(BaseRenderer): return serializer = view.get_serializer(instance=obj) + data = serializer.data form_renderer = self.form_renderer_class() return form_renderer.render(data, self.accepted_media_type, self.renderer_context) @@ -508,9 +514,10 @@ class BrowsableAPIRenderer(BaseRenderer): # Get a read-only version of the serializer serializer = view.get_serializer(instance=obj) - for field_name, field in serializer.fields.items(): - if field.read_only: - del serializer.fields[field_name] + if obj is None: + for name, field in serializer.fields.items(): + if getattr(field, 'read_only', None): + del serializer.fields[name] # Render the raw data content renderer = renderer_class() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index abff68983..a63c7f6c2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -334,13 +334,14 @@ class BaseSerializer(WritableField): if self.source == '*': return self.to_native(obj) + # Get the raw field value try: source = self.source or field_name value = obj for component in source.split('.'): if value is None: - return self.to_native(None) + break value = get_component(value, component) except ObjectDoesNotExist: return None diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..957e3bd2b 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -158,7 +158,6 @@ class BasicTests(TestCase): 'email': '', 'content': '', 'created': None, - 'sub_comment': '' } self.assertEqual(serializer.data, expected) From cba972911a90bdc0050bc48397bc70e1a062040d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Aug 2013 09:12:39 +0100 Subject: [PATCH 0127/1652] Fix failing empty serializer test --- rest_framework/tests/test_serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 957e3bd2b..c24976603 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -158,6 +158,7 @@ class BasicTests(TestCase): 'email': '', 'content': '', 'created': None, + 'sub_comment': '' } self.assertEqual(serializer.data, expected) From f3ab0b2b1d5734314dbe3cdd13cd7c4f0531bf7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Aug 2013 09:20:12 +0100 Subject: [PATCH 0128/1652] Browsable API tab preferences should be site-wide --- rest_framework/static/rest_framework/js/default.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index a57b1cb81..bcb1964db 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -36,12 +36,12 @@ $('a[data-toggle="tab"]:not(:first)').on('shown', function (e) { }); $('a[data-toggle="tab"]').click(function(){ - document.cookie="tab=" + this.name; + document.cookie="tabstyle=" + this.name + "; path=/"; }); // Store tab preference in cookies & display appropriate tab on load. var selectedTab = null; -var selectedTabName = getCookie('tab'); +var selectedTabName = getCookie('tabstyle'); if (selectedTabName) { selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); From f8101114d1ec13e296cb393b43b0ebd9618fa997 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Aug 2013 09:31:35 +0100 Subject: [PATCH 0129/1652] Update release notes --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index a901412f3..708aef388 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -45,6 +45,8 @@ You can determine your currently installed version using `pip freeze`: * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `cache` attribute to throttles to allow overriding of default cache. +* 'Raw data' tab in browsable API now contains pre-populated data. +* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending emptry string instead of file now clears `FileField`. From 3063a50fc20f0bfb7308e668cf083c5ae0876dac Mon Sep 17 00:00:00 2001 From: Edmond Wong Date: Fri, 30 Aug 2013 18:03:44 -0700 Subject: [PATCH 0130/1652] Allow OPTIONS to retrieve PUT field metadata on empty objects This allows OPTIONS to return the PUT endpoint's object serializer metadata when the object hasn't been created yet. --- rest_framework/generics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 14feed204..4d909ef1c 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -356,8 +356,13 @@ class GenericAPIView(views.APIView): self.check_permissions(cloned_request) # Test object permissions if method == 'PUT': - self.get_object() - except (exceptions.APIException, PermissionDenied, Http404): + try: + self.get_object() + except Http404: + # Http404 should be acceptable and the serializer + # metadata should be populated. + pass + except (exceptions.APIException, PermissionDenied): pass else: # If user has appropriate permissions for the view, include From 85ab879a85ac4a7a3f6a965ab78839ac16aed912 Mon Sep 17 00:00:00 2001 From: tom-leys Date: Sat, 31 Aug 2013 19:40:53 +1200 Subject: [PATCH 0131/1652] Updated tutorial part 6: 2 examples were missing includes --- docs/tutorial/6-viewsets-and-routers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 8a1a1ae0c..870632f1b 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -61,6 +61,7 @@ To see what's going on under the hood let's first explicitly create a set of vie In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views. from snippets.views import SnippetViewSet, UserViewSet + from rest_framework import renderers snippet_list = SnippetViewSet.as_view({ 'get': 'list', @@ -101,6 +102,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do Here's our re-wired `urls.py` file. + from django.conf.urls import patterns, url, include from snippets import views from rest_framework.routers import DefaultRouter From a15cda4be4e14f5de5db41a4f664ee95107e0984 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Sat, 31 Aug 2013 17:10:15 +0300 Subject: [PATCH 0132/1652] Regression test for #1072 --- rest_framework/tests/test_relations_pk.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework/tests/test_relations_pk.py b/rest_framework/tests/test_relations_pk.py index e2a1b8152..3815afdd8 100644 --- a/rest_framework/tests/test_relations_pk.py +++ b/rest_framework/tests/test_relations_pk.py @@ -283,6 +283,15 @@ class PKForeignKeyTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'target': ['This field is required.']}) + def test_foreign_key_with_empty(self): + """ + Regression test for #1072 + + https://github.com/tomchristie/django-rest-framework/issues/1072 + """ + serializer = NullableForeignKeySourceSerializer() + self.assertEqual(serializer.data['target'], None) + class PKNullableForeignKeyTests(TestCase): def setUp(self): From 745ebeca77e6bcbec4eb94fb98206d6913e3d049 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Sat, 31 Aug 2013 17:20:49 +0300 Subject: [PATCH 0133/1652] Handle case where obj=None in PKRelatedField.field_to_native() Fixes #1072 --- rest_framework/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 3ad16ee5e..35c00bf1d 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -264,7 +264,7 @@ class PrimaryKeyRelatedField(RelatedField): # RelatedObject (reverse relationship) try: pk = getattr(obj, self.source or field_name).pk - except ObjectDoesNotExist: + except (ObjectDoesNotExist, AttributeError): return None # Forward relationship From 8b245fed14abff62a34e81f4ce8da1c396ba7712 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 2 Sep 2013 09:17:51 +0100 Subject: [PATCH 0134/1652] Add windows virtualenv activate instruction Closes #1075. --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index f15e75c04..06eec3c4e 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -12,7 +12,7 @@ Create a new Django project named `tutorial`, then start a new app called `quick # Create a virtualenv to isolate our package dependencies locally virtualenv env - source env/bin/activate + source env/bin/activate # On Windows use `env\Scripts\activate` # Install Django and Django REST framework into the virtualenv pip install django From d0123a1385b18f25da766c177056c308fbb74b67 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Mon, 2 Sep 2013 10:23:54 -0400 Subject: [PATCH 0135/1652] Changed DOAC documentation link --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index f30b16ed5..7caeac1e2 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -404,4 +404,4 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a [oauthlib]: https://github.com/idan/oauthlib [doac]: https://github.com/Rediker-Software/doac [rediker]: https://github.com/Rediker-Software -[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/markdown/integrations.md# +[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md# From 6e7e4fc01c5ddaf668f17f1d1f201a14a26f72f3 Mon Sep 17 00:00:00 2001 From: Edmond Wong Date: Tue, 3 Sep 2013 12:30:18 -0700 Subject: [PATCH 0136/1652] Added test for OPTIONS before object creation from a PUT --- rest_framework/generics.py | 4 ++- rest_framework/tests/test_generics.py | 42 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4d909ef1c..7d1bf7945 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -360,7 +360,9 @@ class GenericAPIView(views.APIView): self.get_object() except Http404: # Http404 should be acceptable and the serializer - # metadata should be populated. + # metadata should be populated. Except this so the + # outer "else" clause of the try-except-else block + # will be executed. pass except (exceptions.APIException, PermissionDenied): pass diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 7a87d3892..79cd99ac5 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -272,6 +272,48 @@ class TestInstanceView(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, expected) + def test_options_before_instance_create(self): + """ + OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata + before the instance has been created + """ + request = factory.options('/999') + with self.assertNumQueries(1): + response = self.view(request, pk=999).render() + expected = { + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'renders': [ + 'application/json', + 'text/html' + ], + 'name': 'Instance', + 'description': 'Example description for OPTIONS.', + 'actions': { + 'PUT': { + 'text': { + 'max_length': 100, + 'read_only': False, + 'required': True, + 'type': 'string', + 'label': 'Text comes here', + 'help_text': 'Text description.' + }, + 'id': { + 'read_only': True, + 'required': False, + 'type': 'integer', + 'label': 'ID', + }, + } + } + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + def test_get_instance_view_incorrect_arg(self): """ GET requests with an incorrect pk type, should raise 404, not 500. From c4cb26f73bee65b068f140f1f931ede43e41f58a Mon Sep 17 00:00:00 2001 From: Tyler Hayes Date: Wed, 4 Sep 2013 03:38:34 -0700 Subject: [PATCH 0137/1652] Tiny typo fix --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 5d7e2ac87..a3cd1d6ab 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -250,7 +250,7 @@ This allows you to write views that update or create multiple items when a `PUT` serializer = BookSerializer(queryset, data=data, many=True) serializer.is_valid() # True - serialize.save() # `.save()` will be called on each updated or newly created instance. + serializer.save() # `.save()` will be called on each updated or newly created instance. By default bulk updates will be limited to updating instances that already exist in the provided queryset. From b47f1b0257e8688acb67ffd806efe0ffc2c1915b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Sep 2013 20:25:45 +0100 Subject: [PATCH 0138/1652] Added @edmundwong for work on #1076. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b2d3d5d29..07e2ec47d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -166,6 +166,7 @@ The following people have helped make REST framework great. * Alexander Akhmetov - [alexander-akhmetov] * Andrey Antukh - [niwibe] * Mathieu Pillard - [diox] +* Edmond Wong - [edmondwong] Many thanks to everyone who's contributed to the project. @@ -368,3 +369,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexander-akhmetov]: https://github.com/alexander-akhmetov [niwibe]: https://github.com/niwibe [diox]: https://github.com/diox +[edmondwong]: https://github.com/edmondwong From 916d8ab37da2f0c4412507710649ba0f352f29bb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Sep 2013 12:19:51 +0100 Subject: [PATCH 0139/1652] Fix typo --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 15ba9a3a1..5ec4b22fc 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -421,7 +421,7 @@ For example, if all your object URLs used both a account and a slug in the the U def get_object(self, queryset, view_name, view_args, view_kwargs): account = view_kwargs['account'] slug = view_kwargs['slug'] - return queryset.get(account=account, slug=sug) + return queryset.get(account=account, slug=slug) --- From 4a9dcfa76089143bbeb5cd43fa3a406365d89e96 Mon Sep 17 00:00:00 2001 From: bwreilly Date: Fri, 6 Sep 2013 11:01:31 -0500 Subject: [PATCH 0140/1652] added guardian as optional requirement, stubbed out object-level permission class --- docs/index.md | 1 + rest_framework/compat.py | 6 ++++++ rest_framework/permissions.py | 7 ++++++- rest_framework/tests/test_permissions.py | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index e0a2e911b..d83fbff1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,6 +42,7 @@ The following packages are optional: * [django-filter][django-filter] (0.5.4+) - Filtering support. * [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support. * [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support. +* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. **Note**: The `oauth2` Python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6f7447add..b9d1dae6b 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -47,6 +47,12 @@ try: except ImportError: django_filters = None +# guardian is optional +try: + import guardian +except ImportError: + guardian = None + # cStringIO only if it's available, otherwise StringIO try: diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 1036663e0..6d213ba12 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -7,7 +7,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] -from rest_framework.compat import oauth2_provider_scope, oauth2_constants +from rest_framework.compat import oauth2_provider_scope, oauth2_constants, guardian class BasePermission(object): @@ -151,6 +151,11 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): authenticated_users_only = False +class DjangoObjectLevelModelPermissions(DjangoModelPermissions): + def __init__(self): + assert guardian, 'Using DjangoObjectLevelModelPermissions, but guardian is not installed' + + class TokenHasReadWriteScope(BasePermission): """ The request is authenticated as a user and the token used has the right scope diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index e2cca3808..d1171cce5 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -4,6 +4,7 @@ from django.db import models from django.test import TestCase from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.test import APIRequestFactory +from rest_framework.compat import guardian import base64 factory = APIRequestFactory() From b07de86ad372a185c05c1dd77ccd7bee3801996e Mon Sep 17 00:00:00 2001 From: bwreilly Date: Fri, 6 Sep 2013 12:35:06 -0500 Subject: [PATCH 0141/1652] some properly failing tests, set up for standard permissions --- rest_framework/permissions.py | 2 +- rest_framework/runtests/settings.py | 11 +++ rest_framework/tests/test_permissions.py | 99 ++++++++++++++---------- tox.ini | 8 ++ 4 files changed, 79 insertions(+), 41 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6d213ba12..b67be4141 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -153,7 +153,7 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): class DjangoObjectLevelModelPermissions(DjangoModelPermissions): def __init__(self): - assert guardian, 'Using DjangoObjectLevelModelPermissions, but guardian is not installed' + assert guardian, 'Using DjangoObjectLevelModelPermissions, but django-guardian is not installed' class TokenHasReadWriteScope(BasePermission): diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index b3702d0bf..6750376f0 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -123,6 +123,17 @@ else: 'provider.oauth2', ) +# guardian is optional +try: + import guardian +except ImportError: + pass +else: + ANONYMOUS_USER_ID = -1 + INSTALLED_APPS += ( + 'guardian', + ) + STATIC_URL = '/static/' PASSWORD_HASHERS = ( diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d1171cce5..dcdb4eea6 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -3,17 +3,14 @@ from django.contrib.auth.models import User, Permission from django.db import models from django.test import TestCase from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING -from rest_framework.test import APIRequestFactory from rest_framework.compat import guardian +from rest_framework.test import APIRequestFactory +from rest_framework.tests.models import BasicModel +from rest_framework.settings import api_settings import base64 factory = APIRequestFactory() - -class BasicModel(models.Model): - text = models.CharField(max_length=100) - - class RootView(generics.ListCreateAPIView): model = BasicModel authentication_classes = [authentication.BasicAuthentication] @@ -145,45 +142,67 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(list(response.data['actions'].keys()), ['PUT']) -class OwnerModel(models.Model): - text = models.CharField(max_length=100) - owner = models.ForeignKey(User) +class BasicPermModel(BasicModel): + class Meta: + app_label = 'tests' + permissions = ( + ('read_basicpermmodel', "Can view basic perm model"), + # add, change, delete built in to django + ) -class IsOwnerPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return request.user == obj.owner - - -class OwnerInstanceView(generics.RetrieveUpdateDestroyAPIView): - model = OwnerModel +class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): + model = BasicModel authentication_classes = [authentication.BasicAuthentication] - permission_classes = [IsOwnerPermission] + permission_classes = [permissions.DjangoObjectLevelModelPermissions] -owner_instance_view = OwnerInstanceView.as_view() +object_permissions_view = ObjectPermissionInstanceView.as_view() + +if guardian: + class ObjectPermissionsIntegrationTests(TestCase): + """ + Integration tests for the object level permissions API. + """ + + def setUp(self): + # create users + User.objects.create_user('no_permission', 'no_permission@example.com', 'password') + reader = User.objects.create_user('reader', 'reader@example.com', 'password') + writer = User.objects.create_user('writer', 'writer@example.com', 'password') + full_access = User.objects.create_user('full_access', 'full_access@example.com', 'password') + + model = BasicPermModel.objects.create(text='foo') + + # assign permissions appropriately + from guardian.shortcuts import assign_perm + + read = "read_basicpermmodel" + write = "change_basicpermmodel" + delete = "delete_basicpermmodel" + app_label = 'tests.' + # model level permissions + assign_perm(app_label + delete, full_access, obj=model) + (assign_perm(app_label + write, user, obj=model) for user in (writer, full_access)) + (assign_perm(app_label + read, user, obj=model) for user in (reader, writer, full_access)) + + # object level permissions + assign_perm(delete, full_access, obj=model) + (assign_perm(write, user, obj=model) for user in (writer, full_access)) + (assign_perm(read, user, obj=model) for user in (reader, writer, full_access)) + + self.no_permission_credentials = basic_auth_header('no_permission', 'password') + self.reader_credentials = basic_auth_header('reader', 'password') + self.writer_credentials = basic_auth_header('writer', 'password') + self.full_access_credentials = basic_auth_header('full_access', 'password') -class ObjectPermissionsIntegrationTests(TestCase): - """ - Integration tests for the object level permissions API. - """ + def test_has_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.full_access_credentials) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - def setUp(self): - User.objects.create_user('not_owner', 'not_owner@example.com', 'password') - user = User.objects.create_user('owner', 'owner@example.com', 'password') - - self.not_owner_credentials = basic_auth_header('not_owner', 'password') - self.owner_credentials = basic_auth_header('owner', 'password') - - OwnerModel(text='foo', owner=user).save() - - def test_owner_has_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.owner_credentials) - response = owner_instance_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_non_owner_does_not_have_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.not_owner_credentials) - response = owner_instance_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_no_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.writer_credentials) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/tox.ini b/tox.ini index aa97fd350..6e3b8e0a8 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.4 + django-guardian==1.1.1 [testenv:py2.6-django1.6] basepython = python2.6 @@ -34,6 +35,7 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.4 + django-guardian==1.1.1 [testenv:py3.3-django1.5] basepython = python3.3 @@ -55,6 +57,7 @@ deps = django==1.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 [testenv:py2.6-django1.5] basepython = python2.6 @@ -64,6 +67,7 @@ deps = django==1.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 [testenv:py2.7-django1.4] basepython = python2.7 @@ -73,6 +77,7 @@ deps = django==1.4.3 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 [testenv:py2.6-django1.4] basepython = python2.6 @@ -82,6 +87,7 @@ deps = django==1.4.3 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 [testenv:py2.7-django1.3] basepython = python2.7 @@ -91,6 +97,7 @@ deps = django==1.3.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 [testenv:py2.6-django1.3] basepython = python2.6 @@ -100,3 +107,4 @@ deps = django==1.3.5 django-oauth-plus==2.0 oauth2==1.5.211 django-oauth2-provider==0.2.3 + django-guardian==1.1.1 From b5523bcc7ddab97620fd7b49e385b44c664ca899 Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Fri, 6 Sep 2013 11:40:34 -0500 Subject: [PATCH 0142/1652] Support customizable view EXCEPTION_HANDLER Add `api_settings.EXCEPTION_HANDLER` to support custom error responses. Fixes #907. --- docs/api-guide/settings.md | 16 +++++++++++- rest_framework/settings.py | 4 +++ rest_framework/tests/test_views.py | 41 ++++++++++++++++++++++++++++++ rest_framework/views.py | 2 +- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 542e8c5fa..13f96f9a3 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -25,7 +25,7 @@ If you need to access the values of REST framework's API settings in your projec you should use the `api_settings` object. For example. from rest_framework.settings import api_settings - + print api_settings.DEFAULT_AUTHENTICATION_CLASSES The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal. @@ -339,6 +339,20 @@ Default: `'rest_framework.views.get_view_description'` ## Miscellaneous settings +#### EXCEPTION_HANDLER + +A string representing the function that should be used when returning a response for any given exception. If the function returns `None`, a 500 error will be raised. + +This setting can be changed to support error responses other than the default `{"detail": "Failure..."}` responses. For example, you can use it to provide API responses like `{"errors": [{"message": "Failure...", "code": ""} ...]}`. + +This should be a function with the following signature: + + exception_handler(exc) + +* `exc`: The exception. + +Default: `'rest_framework.views.exception_handler'` + #### FORMAT_SUFFIX_KWARG The name of a parameter in the URL conf that may be used to provide a format suffix. diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8c0847512..8abaf1409 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -77,6 +77,9 @@ DEFAULTS = { 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description', + # Exception handling + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + # Testing 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework.renderers.MultiPartRenderer', @@ -125,6 +128,7 @@ IMPORT_STRINGS = ( 'DEFAULT_MODEL_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_FILTER_BACKENDS', + 'EXCEPTION_HANDLER', 'FILTER_BACKEND', 'TEST_REQUEST_RENDERER_CLASSES', 'UNAUTHENTICATED_USER', diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py index c0bec5aed..65c7e50ea 100644 --- a/rest_framework/tests/test_views.py +++ b/rest_framework/tests/test_views.py @@ -32,6 +32,16 @@ def basic_view(request): return {'method': 'PATCH', 'data': request.DATA} +class ErrorView(APIView): + def get(self, request, *args, **kwargs): + raise Exception + + +@api_view(['GET']) +def error_view(request): + raise Exception + + def sanitise_json_error(error_dict): """ Exact contents of JSON error messages depend on the installed version @@ -99,3 +109,34 @@ class FunctionBasedViewIntegrationTests(TestCase): } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) + + +class TestCustomExceptionHandler(TestCase): + def setUp(self): + self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER + + def exception_handler(exc): + return Response('Error!', status=status.HTTP_400_BAD_REQUEST) + + api_settings.EXCEPTION_HANDLER = exception_handler + + def tearDown(self): + api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER + + def test_class_based_view_exception_handler(self): + view = ErrorView.as_view() + + request = factory.get('/', content_type='application/json') + response = view(request) + expected = 'Error!' + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, expected) + + def test_function_based_view_exception_handler(self): + view = error_view + + request = factory.get('/', content_type='application/json') + response = view(request) + expected = 'Error!' + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, expected) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4cff04224..853e64614 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -361,7 +361,7 @@ class APIView(View): else: exc.status_code = status.HTTP_403_FORBIDDEN - response = exception_handler(exc) + response = self.settings.EXCEPTION_HANDLER(exc) if response is None: raise From bae0ef6b5dcb0abf2be865340e5476aeab5ce137 Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Fri, 6 Sep 2013 13:57:32 -0500 Subject: [PATCH 0143/1652] Add EXCEPTION_HANDLER docs to exception docs --- docs/api-guide/exceptions.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 8b3e50f1e..fa5053dfe 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -30,9 +30,27 @@ Might receive an error response indicating that the `DELETE` method is not allow HTTP/1.1 405 Method Not Allowed Content-Type: application/json; charset=utf-8 Content-Length: 42 - + {"detail": "Method 'DELETE' not allowed."} +## Custom exception handling + +To implement custom exception handling (e.g. to handle additional exception classes or to override the error response format), create an exception handler function with the following signature: + + exception_handler(exc) + +* `exc`: The exception. + +If the function returns `None`, a 500 error will be raised. + +The exception handler is set globally, using the `EXCEPTION_HANDLER` setting. For example: + + 'EXCEPTION_HANDLER': 'project.app.module.function' + +If not specified, this setting defaults to the exception handler described above: + + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' + --- # API Reference From b6c0c815aa75b3f2fe0fae3a2221e7d0e976418b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 7 Sep 2013 20:45:43 +0100 Subject: [PATCH 0144/1652] Extra docs on custom exception handling. --- docs/api-guide/exceptions.md | 43 ++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index fa5053dfe..0c48783a3 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -28,28 +28,53 @@ For example, the following request: Might receive an error response indicating that the `DELETE` method is not allowed on that resource: HTTP/1.1 405 Method Not Allowed - Content-Type: application/json; charset=utf-8 + Content-Type: application/json Content-Length: 42 {"detail": "Method 'DELETE' not allowed."} ## Custom exception handling -To implement custom exception handling (e.g. to handle additional exception classes or to override the error response format), create an exception handler function with the following signature: +You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. - exception_handler(exc) +The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. -* `exc`: The exception. +For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: -If the function returns `None`, a 500 error will be raised. + HTTP/1.1 405 Method Not Allowed + Content-Type: application/json + Content-Length: 62 -The exception handler is set globally, using the `EXCEPTION_HANDLER` setting. For example: + {"status_code": 405, "detail": "Method 'DELETE' not allowed."} - 'EXCEPTION_HANDLER': 'project.app.module.function' +In order to alter the style of the response, you could write the following custom exception handler: -If not specified, this setting defaults to the exception handler described above: + from rest_framework.views import exception_handler - 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' + def custom_exception_handler(exc): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc) + + # Now add the HTTP status code to the response. + if response is not None: + response.data['status_code'] = response.status_code + + return response + +The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler' + } + +If not specified, the `'EXCEPTION_HANDLER'` setting defaults to the standard exception handler provided by REST framework: + + REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' + } + +Note that the exception handler will only be called for responses generated by raised exceptions. It will not be used for any responses returned directly by the view, such as the `HTTP_400_BAD_REQUEST` responses that are returned by the generic views when serializer validation fails. --- From 57d6b5fb7c2652bb4c68edd1bcc95be736b06b31 Mon Sep 17 00:00:00 2001 From: bwreilly Date: Sat, 7 Sep 2013 23:16:43 -0500 Subject: [PATCH 0145/1652] necessary test settings for guardian --- rest_framework/runtests/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 6750376f0..be7216580 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -130,6 +130,10 @@ except ImportError: pass else: ANONYMOUS_USER_ID = -1 + AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', # default + 'guardian.backends.ObjectPermissionBackend', + ) INSTALLED_APPS += ( 'guardian', ) From 118645e4806effaa35726012a983676b2c55b6dd Mon Sep 17 00:00:00 2001 From: bwreilly Date: Sat, 7 Sep 2013 23:18:52 -0500 Subject: [PATCH 0146/1652] first pass at object level permissions and tests --- rest_framework/permissions.py | 46 +++++++ rest_framework/tests/test_permissions.py | 148 +++++++++++++++++------ 2 files changed, 157 insertions(+), 37 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index b67be4141..2d8a30e91 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -7,6 +7,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] +from django.http import Http404 from rest_framework.compat import oauth2_provider_scope, oauth2_constants, guardian @@ -152,9 +153,54 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): class DjangoObjectLevelModelPermissions(DjangoModelPermissions): + """ + Basic object level permissions utilizing django-guardian. + """ + def __init__(self): assert guardian, 'Using DjangoObjectLevelModelPermissions, but django-guardian is not installed' + action_perm_map = { + 'list': 'read', + 'retrieve': 'read', + 'create': 'add', + 'partial_update': 'change', + 'update': 'change', + 'destroy': 'delete', + } + + def _get_names(self, view): + model_cls = getattr(view, 'model', None) + queryset = getattr(view, 'queryset', None) + + if model_cls is None and queryset is not None: + model_cls = queryset.model + if not model_cls: # no model, no model based permissions + return None + model_name = model_cls._meta.module_name + return model_name + + def has_permission(self, request, view): + if view.action == 'list': + user = request.user + queryset = view.get_queryset() + model_name = self._get_names(view) + view.queryset = guardian.shortcuts.get_objects_for_user(user, 'read_' + model_name, queryset) #TODO: move to filter + return super(DjangoObjectLevelModelPermissions, self).has_permission(request, view) + + def has_object_permission(self, request, view, obj): + user = request.user + model_name = self._get_names(view) + action = self.action_perm_map.get(view.action) + + assert action, "Tried to determine object permissions but no action specified in view" + + perm = "{action}_{model_name}".format(action=action, model_name=model_name) + check = user.has_perm(perm, obj) + if not check: + raise Http404 + return user.has_perm(perm, obj) + class TokenHasReadWriteScope(BasePermission): """ diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index dcdb4eea6..d64ab04eb 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -1,12 +1,11 @@ from __future__ import unicode_literals -from django.contrib.auth.models import User, Permission +from django.contrib.auth.models import User, Permission, Group from django.db import models from django.test import TestCase from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.compat import guardian from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel -from rest_framework.settings import api_settings import base64 factory = APIRequestFactory() @@ -142,67 +141,142 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(list(response.data['actions'].keys()), ['PUT']) -class BasicPermModel(BasicModel): +class BasicPermModel(models.Model): + text = models.CharField(max_length=100) class Meta: app_label = 'tests' permissions = ( - ('read_basicpermmodel', "Can view basic perm model"), + ('read_basicpermmodel', 'Can view basic perm model'), # add, change, delete built in to django ) class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): - model = BasicModel + model = BasicPermModel authentication_classes = [authentication.BasicAuthentication] permission_classes = [permissions.DjangoObjectLevelModelPermissions] - object_permissions_view = ObjectPermissionInstanceView.as_view() +class ObjectPermissionListView(generics.ListAPIView): + model = BasicPermModel + authentication_classes = [authentication.BasicAuthentication] + permission_classes = [permissions.DjangoObjectLevelModelPermissions] + +object_permissions_list_view = ObjectPermissionListView.as_view() + if guardian: + from guardian.shortcuts import assign_perm + class ObjectPermissionsIntegrationTests(TestCase): """ Integration tests for the object level permissions API. """ + @classmethod + def setUpClass(cls): + # create users + create = User.objects.create_user + users = { + 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'), + 'readonly': create('readonly', 'readonly@example.com', 'password'), + 'writeonly': create('writeonly', 'writeonly@example.com', 'password'), + 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'), + } + + # give everyone model level permissions, as we are not testing those + everyone = Group.objects.create(name='everyone') + model_name = BasicPermModel._meta.module_name + app_label = BasicPermModel._meta.app_label + f = '{0}_{1}'.format + perms = { + 'read': f('read', model_name), + 'change': f('change', model_name), + 'delete': f('delete', model_name) + } + for perm in perms.values(): + perm = '{0}.{1}'.format(app_label, perm) + assign_perm(perm, everyone) + everyone.user_set.add(*users.values()) + + cls.perms = perms + cls.users = users def setUp(self): - # create users - User.objects.create_user('no_permission', 'no_permission@example.com', 'password') - reader = User.objects.create_user('reader', 'reader@example.com', 'password') - writer = User.objects.create_user('writer', 'writer@example.com', 'password') - full_access = User.objects.create_user('full_access', 'full_access@example.com', 'password') - + perms = self.perms + users = self.users + + # appropriate object level permissions + readers = Group.objects.create(name='readers') + writers = Group.objects.create(name='writers') + deleters = Group.objects.create(name='deleters') + model = BasicPermModel.objects.create(text='foo') + + assign_perm(perms['read'], readers, model) + assign_perm(perms['change'], writers, model) + assign_perm(perms['delete'], deleters, model) - # assign permissions appropriately - from guardian.shortcuts import assign_perm + readers.user_set.add(users['fullaccess'], users['readonly']) + writers.user_set.add(users['fullaccess'], users['writeonly']) + deleters.user_set.add(users['fullaccess'], users['deleteonly']) - read = "read_basicpermmodel" - write = "change_basicpermmodel" - delete = "delete_basicpermmodel" - app_label = 'tests.' - # model level permissions - assign_perm(app_label + delete, full_access, obj=model) - (assign_perm(app_label + write, user, obj=model) for user in (writer, full_access)) - (assign_perm(app_label + read, user, obj=model) for user in (reader, writer, full_access)) + self.credentials = {} + for user in users.values(): + self.credentials[user.username] = basic_auth_header(user.username, 'password') - # object level permissions - assign_perm(delete, full_access, obj=model) - (assign_perm(write, user, obj=model) for user in (writer, full_access)) - (assign_perm(read, user, obj=model) for user in (reader, writer, full_access)) - - self.no_permission_credentials = basic_auth_header('no_permission', 'password') - self.reader_credentials = basic_auth_header('reader', 'password') - self.writer_credentials = basic_auth_header('writer', 'password') - self.full_access_credentials = basic_auth_header('full_access', 'password') - - - def test_has_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.full_access_credentials) + # Delete + def test_can_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) + object_permissions_view.cls.action = 'destroy' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - def test_no_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.writer_credentials) + def test_cannot_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + object_permissions_view.cls.action = 'destroy' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Update + def test_can_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['writeonly']) + object_permissions_view.cls.action = 'partial_update' + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('text'), 'foobar') + + def test_cannot_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['deleteonly']) + object_permissions_view.cls.action = 'partial_update' + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Read + def test_can_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + object_permissions_view.cls.action = 'retrieve' + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_cannot_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) + object_permissions_view.cls.action = 'retrieve' + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Read list + def test_can_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) + object_permissions_list_view.cls.action = 'list' + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0].get('id'), 1) + + def test_cannot_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) + object_permissions_list_view.cls.action = 'list' + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual(response.data, []) \ No newline at end of file From 9ff0f6d3bff3c1d02d2ccaf4f1500e25cb97620d Mon Sep 17 00:00:00 2001 From: bwreilly Date: Sat, 7 Sep 2013 23:48:03 -0500 Subject: [PATCH 0147/1652] switch to a dedicated filter for read list object permissions --- rest_framework/filters.py | 18 +++++++++++++++++- rest_framework/permissions.py | 13 ++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 4079e1bd5..6d46ad233 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -4,7 +4,7 @@ returned by list views. """ from __future__ import unicode_literals from django.db import models -from rest_framework.compat import django_filters, six +from rest_framework.compat import django_filters, six, guardian from functools import reduce import operator @@ -23,6 +23,22 @@ class BaseFilterBackend(object): raise NotImplementedError(".filter_queryset() must be overridden.") +class ObjectPermissionReaderFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + def __init__(self): + assert guardian, 'Using ObjectPermissionReaderFilter, but django-guardian is not installed' + + def filter_queryset(self, request, queryset, view): + user = request.user + model_cls = queryset.model + model_name = model_cls._meta.module_name + permission = 'read_' + model_name + return guardian.shortcuts.get_objects_for_user(user, permission, queryset) + + class DjangoFilterBackend(BaseFilterBackend): """ A filter backend that uses django-filter. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 2d8a30e91..0d5e0e787 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -9,6 +9,7 @@ SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 from rest_framework.compat import oauth2_provider_scope, oauth2_constants, guardian +from rest_framework.filters import ObjectPermissionReaderFilter class BasePermission(object): @@ -169,7 +170,7 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): 'destroy': 'delete', } - def _get_names(self, view): + def _get_model_name(self, view): model_cls = getattr(view, 'model', None) queryset = getattr(view, 'queryset', None) @@ -182,19 +183,17 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): def has_permission(self, request, view): if view.action == 'list': - user = request.user queryset = view.get_queryset() - model_name = self._get_names(view) - view.queryset = guardian.shortcuts.get_objects_for_user(user, 'read_' + model_name, queryset) #TODO: move to filter + view.queryset = ObjectPermissionReaderFilter().filter_queryset(request, queryset, view) return super(DjangoObjectLevelModelPermissions, self).has_permission(request, view) def has_object_permission(self, request, view, obj): - user = request.user - model_name = self._get_names(view) action = self.action_perm_map.get(view.action) - assert action, "Tried to determine object permissions but no action specified in view" + user = request.user + model_name = self._get_model_name(view) + perm = "{action}_{model_name}".format(action=action, model_name=model_name) check = user.has_perm(perm, obj) if not check: From 0183c69538de7b6dc4e9b0602fc364e789e0cab6 Mon Sep 17 00:00:00 2001 From: bwreilly Date: Mon, 9 Sep 2013 08:39:09 -0700 Subject: [PATCH 0148/1652] removed unnecessary guardian req and view.action parsing --- rest_framework/permissions.py | 52 +++++++++++------------- rest_framework/tests/test_permissions.py | 11 ++--- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 0d5e0e787..61a33bdd3 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,8 +8,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import oauth2_provider_scope, oauth2_constants, guardian -from rest_framework.filters import ObjectPermissionReaderFilter +from rest_framework.compat import oauth2_provider_scope, oauth2_constants class BasePermission(object): @@ -158,47 +157,42 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): Basic object level permissions utilizing django-guardian. """ - def __init__(self): - assert guardian, 'Using DjangoObjectLevelModelPermissions, but django-guardian is not installed' - - action_perm_map = { - 'list': 'read', - 'retrieve': 'read', - 'create': 'add', - 'partial_update': 'change', - 'update': 'change', - 'destroy': 'delete', + actions_map = { + 'GET': ['read_%(model_name)s'], + 'OPTIONS': ['read_%(model_name)s'], + 'HEAD': ['read_%(model_name)s'], + 'POST': ['add_%(model_name)s'], + 'PUT': ['change_%(model_name)s'], + 'PATCH': ['change_%(model_name)s'], + 'DELETE': ['delete_%(model_name)s'], } - def _get_model_name(self, view): - model_cls = getattr(view, 'model', None) - queryset = getattr(view, 'queryset', None) - - if model_cls is None and queryset is not None: - model_cls = queryset.model - if not model_cls: # no model, no model based permissions - return None - model_name = model_cls._meta.module_name - return model_name + def get_required_object_permissions(self, method, model_cls): + kwargs = { + 'model_name': model_cls._meta.module_name + } + return [perm % kwargs for perm in self.actions_map[method]] def has_permission(self, request, view): - if view.action == 'list': + if getattr(view, 'action', None) == 'list': queryset = view.get_queryset() view.queryset = ObjectPermissionReaderFilter().filter_queryset(request, queryset, view) return super(DjangoObjectLevelModelPermissions, self).has_permission(request, view) def has_object_permission(self, request, view, obj): - action = self.action_perm_map.get(view.action) - assert action, "Tried to determine object permissions but no action specified in view" + model_cls = getattr(view, 'model', None) + queryset = getattr(view, 'queryset', None) + if model_cls is None and queryset is not None: + model_cls = queryset.model + + perms = self.get_required_object_permissions(request.method, model_cls) user = request.user - model_name = self._get_model_name(view) - perm = "{action}_{model_name}".format(action=action, model_name=model_name) - check = user.has_perm(perm, obj) + check = user.has_perms(perms, obj) if not check: raise Http404 - return user.has_perm(perm, obj) + return user.has_perms(perms, obj) class TokenHasReadWriteScope(BasePermission): diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d64ab04eb..2d866cd09 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -4,6 +4,7 @@ from django.db import models from django.test import TestCase from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.compat import guardian +from rest_framework.filters import ObjectPermissionReaderFilter from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel import base64 @@ -227,13 +228,11 @@ if guardian: # Delete def test_can_delete_permissions(self): request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) - object_permissions_view.cls.action = 'destroy' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) def test_cannot_delete_permissions(self): request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - object_permissions_view.cls.action = 'destroy' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -241,7 +240,6 @@ if guardian: def test_can_update_permissions(self): request = factory.patch('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.credentials['writeonly']) - object_permissions_view.cls.action = 'partial_update' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data.get('text'), 'foobar') @@ -249,34 +247,31 @@ if guardian: def test_cannot_update_permissions(self): request = factory.patch('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.credentials['deleteonly']) - object_permissions_view.cls.action = 'partial_update' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Read def test_can_read_permissions(self): request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - object_permissions_view.cls.action = 'retrieve' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) def test_cannot_read_permissions(self): request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) - object_permissions_view.cls.action = 'retrieve' response = object_permissions_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Read list def test_can_read_list_permissions(self): request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) - object_permissions_list_view.cls.action = 'list' + object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) response = object_permissions_list_view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data[0].get('id'), 1) def test_cannot_read_list_permissions(self): request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) - object_permissions_list_view.cls.action = 'list' + object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) response = object_permissions_list_view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertListEqual(response.data, []) \ No newline at end of file From 23fc9dd53fcd9cc25e2c77e5ffae395f04d4990d Mon Sep 17 00:00:00 2001 From: bwreilly Date: Mon, 9 Sep 2013 09:32:29 -0700 Subject: [PATCH 0149/1652] better doc for object permissions, drop redundant has_permission call --- rest_framework/permissions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 61a33bdd3..70bf9c61e 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -154,7 +154,14 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): class DjangoObjectLevelModelPermissions(DjangoModelPermissions): """ - Basic object level permissions utilizing django-guardian. + The request is authenticated using `django.contrib.auth` permissions. + See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions + + It ensures that the user is authenticated, and has the appropriate + `add`/`change`/`delete` permissions on the object using .has_perms. + + This permission can only be applied against view classes that + provide a `.model` or `.queryset` attribute. """ actions_map = { @@ -173,12 +180,6 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): } return [perm % kwargs for perm in self.actions_map[method]] - def has_permission(self, request, view): - if getattr(view, 'action', None) == 'list': - queryset = view.get_queryset() - view.queryset = ObjectPermissionReaderFilter().filter_queryset(request, queryset, view) - return super(DjangoObjectLevelModelPermissions, self).has_permission(request, view) - def has_object_permission(self, request, view, obj): model_cls = getattr(view, 'model', None) queryset = getattr(view, 'queryset', None) From f5c34926d6a4b4b29fb083d25b99b10d7431eee4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Sep 2013 20:41:54 +0100 Subject: [PATCH 0150/1652] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 708aef388..1f363310d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `cache` attribute to throttles to allow overriding of default cache. From 222c1d1122b13940fa6072a1dfb89b25491ee6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ociepka?= Date: Tue, 10 Sep 2013 12:02:14 +0200 Subject: [PATCH 0151/1652] Add order_by to the AutoFilterSet `AutoFilterSet` should contains `order_by` set to all by default. --- rest_framework/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 4079e1bd5..1e58d173d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -53,6 +53,7 @@ class DjangoFilterBackend(BaseFilterBackend): class Meta: model = queryset.model fields = filter_fields + order_by = True return AutoFilterSet return None From 5970baa20112921217ae4f2c2a9f175df25922db Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Sep 2013 21:00:13 +0100 Subject: [PATCH 0152/1652] Tweaks and docs to object-level model permissions. --- docs/api-guide/filtering.md | 46 +++++ docs/api-guide/permissions.md | 18 +- rest_framework/filters.py | 37 ++-- rest_framework/permissions.py | 47 +++-- rest_framework/tests/test_permissions.py | 217 +++++++++++++---------- 5 files changed, 235 insertions(+), 130 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 649462da7..859e8d52a 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -257,6 +257,49 @@ The `ordering` attribute may be either a string or a list/tuple of strings. --- +## DjangoObjectPermissionsFilter + +The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission. + +This filter class must be used with views that provide either a `queryset` or a `model` attribute. + +If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute. + +A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this. + +**permissions.py**: + + class CustomObjectPermissions(permissions.DjangoObjectPermissions): + """ + Similar to `DjangoObjectPermissions`, but adding 'view' permissions. + """ + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + +**views.py**: + + class EventViewSet(viewsets.ModelViewSet): + """ + Viewset that only lists events if user has 'view' permissions, and only + allows operations on individual events if user has appropriate 'view', 'add', + 'change' or 'delete' permissions. + """ + queryset = Event.objects.all() + serializer = EventSerializer + filter_backends = (filters.DjangoObjectPermissionsFilter,) + permission_classes = (myapp.permissions.CustomObjectPermissions,) + +For more information on adding `'view'` permissions for models, see the [relevant section][view-permissions] of the `django-guardian` documentation, and [this blogpost][view-permissions-blogpost]. + +--- + # Custom generic filtering You can also provide your own generic filtering backend, or write an installable app for other developers to use. @@ -281,5 +324,8 @@ We could achieve the same behavior by overriding `get_queryset()` on the views, [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html +[guardian]: http://pythonhosted.org/django-guardian/ +[view-permissions]: http://pythonhosted.org/django-guardian/userguide/assign.html +[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index a7bf15556..871de84ec 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -120,7 +120,21 @@ To use custom model permissions, override `DjangoModelPermissions` and set the ` ## DjangoModelPermissionsOrAnonReadOnly -Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. +Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. + +## DjangoObjectPermissions + +This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian]. + +When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned. + +* `POST` requests require the user to have the `add` permission on the model instance. +* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance. +* `DELETE` requests require the user to have the `delete` permission on the model instance. + +Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well. + +As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. Note that if you add a custom `view` permission for `GET`, `HEAD` and `OPTIONS` requests, you'll probably also want to consider adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions. ## TokenHasReadWriteScope @@ -220,7 +234,9 @@ The [Composed Permissions][composed-permissions] package provides a simple way t [authentication]: authentication.md [throttling]: throttling.md [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions +[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian +[get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user [django-oauth-plus]: http://code.larlet.fr/django-oauth-plus [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [2.2-announcement]: ../topics/2.2-announcement.md diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 6d46ad233..1693bcd24 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -23,22 +23,6 @@ class BaseFilterBackend(object): raise NotImplementedError(".filter_queryset() must be overridden.") -class ObjectPermissionReaderFilter(BaseFilterBackend): - """ - A filter backend that limits results to those where the requesting user - has read object level permissions. - """ - def __init__(self): - assert guardian, 'Using ObjectPermissionReaderFilter, but django-guardian is not installed' - - def filter_queryset(self, request, queryset, view): - user = request.user - model_cls = queryset.model - model_name = model_cls._meta.module_name - permission = 'read_' + model_name - return guardian.shortcuts.get_objects_for_user(user, permission, queryset) - - class DjangoFilterBackend(BaseFilterBackend): """ A filter backend that uses django-filter. @@ -156,3 +140,24 @@ class OrderingFilter(BaseFilterBackend): return queryset.order_by(*ordering) return queryset + + +class DjangoObjectPermissionsFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + def __init__(self): + assert guardian, 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed' + + perm_format = '%(app_label)s.view_%(model_name)s' + + def filter_queryset(self, request, queryset, view): + user = request.user + model_cls = queryset.model + kwargs = { + 'app_label': model_cls._meta.app_label, + 'model_name': model_cls._meta.module_name + } + permission = self.perm_format % kwargs + return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 70bf9c61e..531847988 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -152,10 +152,10 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): authenticated_users_only = False -class DjangoObjectLevelModelPermissions(DjangoModelPermissions): +class DjangoObjectPermissions(DjangoModelPermissions): """ - The request is authenticated using `django.contrib.auth` permissions. - See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions + The request is authenticated using Django's object-level permissions. + It requires an object-permissions-enabled backend, such as Django Guardian. It ensures that the user is authenticated, and has the appropriate `add`/`change`/`delete` permissions on the object using .has_perms. @@ -164,21 +164,22 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): provide a `.model` or `.queryset` attribute. """ - actions_map = { - 'GET': ['read_%(model_name)s'], - 'OPTIONS': ['read_%(model_name)s'], - 'HEAD': ['read_%(model_name)s'], - 'POST': ['add_%(model_name)s'], - 'PUT': ['change_%(model_name)s'], - 'PATCH': ['change_%(model_name)s'], - 'DELETE': ['delete_%(model_name)s'], + perms_map = { + 'GET': [], + 'OPTIONS': [], + 'HEAD': [], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], } def get_required_object_permissions(self, method, model_cls): kwargs = { + 'app_label': model_cls._meta.app_label, 'model_name': model_cls._meta.module_name } - return [perm % kwargs for perm in self.actions_map[method]] + return [perm % kwargs for perm in self.perms_map[method]] def has_object_permission(self, request, view, obj): model_cls = getattr(view, 'model', None) @@ -190,10 +191,24 @@ class DjangoObjectLevelModelPermissions(DjangoModelPermissions): perms = self.get_required_object_permissions(request.method, model_cls) user = request.user - check = user.has_perms(perms, obj) - if not check: - raise Http404 - return user.has_perms(perms, obj) + if not user.has_perms(perms, obj): + # If the user does not have permissions we need to determine if + # they have read permissions to see 403, or not, and simply see + # a 404 reponse. + + if request.method in ('GET', 'OPTIONS', 'HEAD'): + # Read permissions already checked and failed, no need + # to make another lookup. + raise Http404 + + read_perms = self.get_required_object_permissions('GET', model_cls) + if not user.has_perms(read_perms, obj): + raise Http404 + + # Has read permissions. + return False + + return True class TokenHasReadWriteScope(BasePermission): diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index 2d866cd09..d08124f48 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -2,9 +2,10 @@ from __future__ import unicode_literals from django.contrib.auth.models import User, Permission, Group from django.db import models from django.test import TestCase +from django.utils import unittest from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework.compat import guardian -from rest_framework.filters import ObjectPermissionReaderFilter +from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel import base64 @@ -148,130 +149,152 @@ class BasicPermModel(models.Model): class Meta: app_label = 'tests' permissions = ( - ('read_basicpermmodel', 'Can view basic perm model'), + ('view_basicpermmodel', 'Can view basic perm model'), # add, change, delete built in to django ) +# Custom object-level permission, that includes 'view' permissions +class ViewObjectPermissions(permissions.DjangoObjectPermissions): + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + + class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicPermModel authentication_classes = [authentication.BasicAuthentication] - permission_classes = [permissions.DjangoObjectLevelModelPermissions] + permission_classes = [ViewObjectPermissions] object_permissions_view = ObjectPermissionInstanceView.as_view() + class ObjectPermissionListView(generics.ListAPIView): model = BasicPermModel authentication_classes = [authentication.BasicAuthentication] - permission_classes = [permissions.DjangoObjectLevelModelPermissions] + permission_classes = [ViewObjectPermissions] object_permissions_list_view = ObjectPermissionListView.as_view() -if guardian: - from guardian.shortcuts import assign_perm - class ObjectPermissionsIntegrationTests(TestCase): - """ - Integration tests for the object level permissions API. - """ - @classmethod - def setUpClass(cls): - # create users - create = User.objects.create_user - users = { - 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'), - 'readonly': create('readonly', 'readonly@example.com', 'password'), - 'writeonly': create('writeonly', 'writeonly@example.com', 'password'), - 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'), - } +@unittest.skipUnless(guardian, 'django-guardian not installed') +class ObjectPermissionsIntegrationTests(TestCase): + """ + Integration tests for the object level permissions API. + """ + @classmethod + def setUpClass(cls): + from guardian.shortcuts import assign_perm - # give everyone model level permissions, as we are not testing those - everyone = Group.objects.create(name='everyone') - model_name = BasicPermModel._meta.module_name - app_label = BasicPermModel._meta.app_label - f = '{0}_{1}'.format - perms = { - 'read': f('read', model_name), - 'change': f('change', model_name), - 'delete': f('delete', model_name) - } - for perm in perms.values(): - perm = '{0}.{1}'.format(app_label, perm) - assign_perm(perm, everyone) - everyone.user_set.add(*users.values()) + # create users + create = User.objects.create_user + users = { + 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'), + 'readonly': create('readonly', 'readonly@example.com', 'password'), + 'writeonly': create('writeonly', 'writeonly@example.com', 'password'), + 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'), + } - cls.perms = perms - cls.users = users + # give everyone model level permissions, as we are not testing those + everyone = Group.objects.create(name='everyone') + model_name = BasicPermModel._meta.module_name + app_label = BasicPermModel._meta.app_label + f = '{0}_{1}'.format + perms = { + 'view': f('view', model_name), + 'change': f('change', model_name), + 'delete': f('delete', model_name) + } + for perm in perms.values(): + perm = '{0}.{1}'.format(app_label, perm) + assign_perm(perm, everyone) + everyone.user_set.add(*users.values()) - def setUp(self): - perms = self.perms - users = self.users + cls.perms = perms + cls.users = users - # appropriate object level permissions - readers = Group.objects.create(name='readers') - writers = Group.objects.create(name='writers') - deleters = Group.objects.create(name='deleters') + def setUp(self): + from guardian.shortcuts import assign_perm + perms = self.perms + users = self.users - model = BasicPermModel.objects.create(text='foo') - - assign_perm(perms['read'], readers, model) - assign_perm(perms['change'], writers, model) - assign_perm(perms['delete'], deleters, model) + # appropriate object level permissions + readers = Group.objects.create(name='readers') + writers = Group.objects.create(name='writers') + deleters = Group.objects.create(name='deleters') - readers.user_set.add(users['fullaccess'], users['readonly']) - writers.user_set.add(users['fullaccess'], users['writeonly']) - deleters.user_set.add(users['fullaccess'], users['deleteonly']) + model = BasicPermModel.objects.create(text='foo') + + assign_perm(perms['view'], readers, model) + assign_perm(perms['change'], writers, model) + assign_perm(perms['delete'], deleters, model) - self.credentials = {} - for user in users.values(): - self.credentials[user.username] = basic_auth_header(user.username, 'password') + readers.user_set.add(users['fullaccess'], users['readonly']) + writers.user_set.add(users['fullaccess'], users['writeonly']) + deleters.user_set.add(users['fullaccess'], users['deleteonly']) - # Delete - def test_can_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.credentials = {} + for user in users.values(): + self.credentials[user.username] = basic_auth_header(user.username, 'password') - def test_cannot_delete_permissions(self): - request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + # Delete + def test_can_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - # Update - def test_can_update_permissions(self): - request = factory.patch('/1', {'text': 'foobar'}, format='json', - HTTP_AUTHORIZATION=self.credentials['writeonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data.get('text'), 'foobar') + def test_cannot_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_cannot_update_permissions(self): - request = factory.patch('/1', {'text': 'foobar'}, format='json', - HTTP_AUTHORIZATION=self.credentials['deleteonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + # Update + def test_can_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['writeonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data.get('text'), 'foobar') - # Read - def test_can_read_permissions(self): - request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_cannot_update_permissions(self): + request = factory.patch('/1', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_cannot_read_permissions(self): - request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) - response = object_permissions_view(request, pk='1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_cannot_update_permissions_non_existing(self): + request = factory.patch('/999', {'text': 'foobar'}, format='json', + HTTP_AUTHORIZATION=self.credentials['deleteonly']) + response = object_permissions_view(request, pk='999') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - # Read list - def test_can_read_list_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) - object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) - response = object_permissions_list_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0].get('id'), 1) + # Read + def test_can_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_cannot_read_list_permissions(self): - request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) - object_permissions_list_view.cls.filter_backends = (ObjectPermissionReaderFilter,) - response = object_permissions_list_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertListEqual(response.data, []) \ No newline at end of file + def test_cannot_read_permissions(self): + request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly']) + response = object_permissions_view(request, pk='1') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Read list + def test_can_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly']) + object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,) + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0].get('id'), 1) + + def test_cannot_read_list_permissions(self): + request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly']) + object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,) + response = object_permissions_list_view(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual(response.data, []) From 101da4581083d75636b24c50638e7f288d1fe240 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Sep 2013 21:06:42 +0100 Subject: [PATCH 0153/1652] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 1f363310d..ff3dae097 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Added `DjangoObjectPermissions`, and `DjangoObjectPermissionsFilter`. * Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. From a1d7ed20d256730492659f6ba6193faf1f12a581 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Sep 2013 21:06:53 +0100 Subject: [PATCH 0154/1652] Add Django Guardian to travis testing --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6a4532411..d12479e9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ install: - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" - export PYTHONPATH=. From e021472a1667c4902000bb40e0c19f64160b1584 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Sep 2013 21:07:20 +0100 Subject: [PATCH 0155/1652] Added @bwreilly for awesome work on #1093. Thanks!!! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 07e2ec47d..8269580ef 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -167,6 +167,7 @@ The following people have helped make REST framework great. * Andrey Antukh - [niwibe] * Mathieu Pillard - [diox] * Edmond Wong - [edmondwong] +* Ben Reilly - [bwreilly] Many thanks to everyone who's contributed to the project. @@ -370,3 +371,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [niwibe]: https://github.com/niwibe [diox]: https://github.com/diox [edmondwong]: https://github.com/edmondwong +[bwreilly]: https://github.com/bwreilly From 195790e60b117eff421eb8f04a9f9f3527e797b8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 11 Sep 2013 09:09:30 +0100 Subject: [PATCH 0156/1652] Version 2.3.8 --- docs/topics/release-notes.md | 4 +++- rest_framework/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index ff3dae097..3b35d9ed6 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,7 +40,9 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.8 + +**Date**: 11th September 2013 * Added `DjangoObjectPermissions`, and `DjangoObjectPermissionsFilter`. * Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 087808e0b..2bd2991ba 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.7' +__version__ = '2.3.8' VERSION = __version__ # synonym From 2a6a2013df4fcb8e09425e9fa758b91b3a23b751 Mon Sep 17 00:00:00 2001 From: Diego Ponciano Date: Wed, 11 Sep 2013 17:25:57 -0300 Subject: [PATCH 0157/1652] small typo correction on ViewSet example code --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 2e65b7a43..1062cb32c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -23,7 +23,7 @@ Let's define a simple viewset that can be used to list or retrieve all the users from django.shortcuts import get_object_or_404 from myapps.serializers import UserSerializer from rest_framework import viewsets - from rest_framewor.responses import Response + from rest_framework.response import Response class UserViewSet(viewsets.ViewSet): """ From 59cce01b3359aa009e697a99eabbf2ef322b28e2 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:03:20 +0100 Subject: [PATCH 0158/1652] Fix error when serializer gets files but no data --- rest_framework/serializers.py | 2 +- rest_framework/tests/test_files.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..778e72d1a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -156,7 +156,7 @@ class BaseSerializer(WritableField): self.context = context or {} - self.init_data = data + self.init_data = data or {} self.init_files = files self.object = instance self.fields = self.get_fields() diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index c13c38b86..127e379e9 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -80,3 +80,16 @@ class FileSerializerTests(TestCase): serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'}) self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'file': [errmsg]}) + + def test_validation_with_no_data(self): + """ + Validation should still function when no data dictionary is provided. + """ + now = datetime.datetime.now() + file = BytesIO(six.b('stuff')) + file.name = 'stuff.txt' + file.size = len(file.getvalue()) + uploaded_file = UploadedFile(file=file, created=now) + + serializer = UploadedFileSerializer(files={'file': file}) + self.assertFalse(serializer.is_valid()) \ No newline at end of file From 6e4bdb55969171c87296aba9711dbc77f8a1e366 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:04:33 +0100 Subject: [PATCH 0159/1652] Add missing newline at the end of test file --- rest_framework/tests/test_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index 127e379e9..78f4cf425 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -92,4 +92,4 @@ class FileSerializerTests(TestCase): uploaded_file = UploadedFile(file=file, created=now) serializer = UploadedFileSerializer(files={'file': file}) - self.assertFalse(serializer.is_valid()) \ No newline at end of file + self.assertFalse(serializer.is_valid()) From dfc430cabaf76a1b3382a614cc692e4a52b09bcd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Sep 2013 20:27:23 +0100 Subject: [PATCH 0160/1652] Fix django guardian link --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index d83fbff1f..bb2129f6f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -251,6 +251,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [oauth2]: https://github.com/simplegeo/python-oauth2 [django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider +[django-guardian]: https://github.com/lukaszb/django-guardian [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [image]: img/quickstart.png [index]: . From 895beb89c60cea534f85b8a7749615755c4d43b5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Sep 2013 21:41:21 +0100 Subject: [PATCH 0161/1652] Note on '.model' as default only, with 'serializer_class', and 'queryset' attributes prefered. Closes #1100 --- docs/api-guide/generic-views.md | 2 +- docs/topics/writable-nested-serializers.md | 47 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 docs/topics/writable-nested-serializers.md diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 7185b6b68..dc0076dff 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -69,7 +69,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. +* `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. **Pagination**: diff --git a/docs/topics/writable-nested-serializers.md b/docs/topics/writable-nested-serializers.md new file mode 100644 index 000000000..66ea78150 --- /dev/null +++ b/docs/topics/writable-nested-serializers.md @@ -0,0 +1,47 @@ +> To save HTTP requests, it may be convenient to send related documents along with the request. +> +> — [JSON API specification for Ember Data][cite]. + +# Writable nested serializers + +Although flat data structures serve to properly delineate between the individual entities in your service, there are cases where it may be more appropriate or convenient to use nested data structures. + +Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependancies between the various model instances, and the need to save or delete multiple instances in a single action. + +## One-to-many data structures + +*Example of a **read-only** nested serializer. Nothing complex to worry about here.* + + class ToDoItemSerializer(serializers.ModelSerializer): + class Meta: + model = ToDoItem + fields = ('text', 'is_completed') + + class ToDoListSerializer(serializers.ModelSerializer): + items = ToDoItemSerializer(many=True, read_only=True) + + class Meta: + model = ToDoList + fields = ('title', 'items') + +Some example output from our serializer. + + { + 'title': 'Leaving party preperations', + 'items': { + {'text': 'Compile playlist', 'is_completed': True}, + {'text': 'Send invites', 'is_completed': False}, + {'text': 'Clean house', 'is_completed': False} + } + } + +Let's take a look at updating our nested one-to-many data structure. + +### Validation errors + +### Adding and removing items + +### Making PATCH requests + + +[cite]: http://jsonapi.org/format/#url-based-json-api \ No newline at end of file From d489c5c88144a25ef0d61fb8deb0b77f3a061480 Mon Sep 17 00:00:00 2001 From: David Pretty Date: Fri, 13 Sep 2013 13:36:18 +1000 Subject: [PATCH 0162/1652] Let JSONEncoder handle Numpy data types. json.JSONEncoder cannot serialize Numpy data types. Numpy arrays and array scalars have a tolist() method which casts the object to a standard python data type. --- rest_framework/utils/encoders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index b26a2085a..7efd5417b 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -42,6 +42,8 @@ class JSONEncoder(json.JSONEncoder): return str(o.total_seconds()) elif isinstance(o, decimal.Decimal): return str(o) + elif hasattr(o, 'tolist'): + return o.tolist() elif hasattr(o, '__iter__'): return [i for i in o] return super(JSONEncoder, self).default(o) From 272a6abf91c51b44781d27af5352c7e36c8fa91c Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Fri, 13 Sep 2013 10:46:24 +0100 Subject: [PATCH 0163/1652] Try a more localised fix to the data=None problem --- rest_framework/fields.py | 1 + rest_framework/serializers.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 210c2537d..0c3817b57 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -306,6 +306,7 @@ class WritableField(Field): return try: + data = data or {} if self.use_files: files = files or {} try: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 778e72d1a..a63c7f6c2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -156,7 +156,7 @@ class BaseSerializer(WritableField): self.context = context or {} - self.init_data = data or {} + self.init_data = data self.init_files = files self.object = instance self.fields = self.get_fields() From 0de1a1a0ad0ff96292b14d707730dc47b1943c26 Mon Sep 17 00:00:00 2001 From: Rajiv Bose Date: Fri, 13 Sep 2013 11:55:16 +0100 Subject: [PATCH 0164/1652] Typo in strings referring to Python package, django-filter. On skip of django_filters related unit-tests the reason given states the Python package 'django-filters' is not install. However, the Python package required to run django_filters related tests is 'django-filter'. --- rest_framework/tests/test_filters.py | 14 +++++++------- rest_framework/tests/test_pagination.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index c9d9e7ffa..379db29d8 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -113,7 +113,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): Integration tests for filtered list views. """ - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): """ GET requests to paginated ListCreateAPIView should return paginated results. @@ -142,7 +142,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): expected_data = [f for f in self.data if f['date'] == search_date] self.assertEqual(response.data, expected_data) - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_queryset(self): """ Regression test for #814. @@ -157,7 +157,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): expected_data = [f for f in self.data if f['decimal'] == search_decimal] self.assertEqual(response.data, expected_data) - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_get_queryset_only(self): """ Regression test for #834. @@ -168,7 +168,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Used to raise "issubclass() arg 2 must be a class or tuple of classes" # here when neither `model' nor `queryset' was specified. - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_class_root_view(self): """ GET requests to filtered ListCreateAPIView that have a filter_class set @@ -216,7 +216,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): f['decimal'] < search_decimal] self.assertEqual(response.data, expected_data) - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_incorrectly_configured_filter(self): """ An error should be displayed when the filter class is misconfigured. @@ -226,7 +226,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/') self.assertRaises(AssertionError, view, request) - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_unknown_filter(self): """ GET requests with filters that aren't configured should return 200. @@ -248,7 +248,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): def _get_url(self, item): return reverse('detail-view', kwargs=dict(pk=item.pk)) - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_detail_view(self): """ GET requests to filtered RetrieveAPIView that have a filter_class set diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index 4170d4b64..d6bc7895c 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -122,7 +122,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): for obj in self.objects.all() ] - @unittest.skipUnless(django_filters, 'django-filters not installed') + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_django_filter_paginated_filtered_root_view(self): """ GET requests to paginated filtered ListCreateAPIView should return From bb3261ca489104e3dea434aa11d76f370e938ca8 Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Fri, 13 Sep 2013 22:51:11 +1000 Subject: [PATCH 0165/1652] Fixed #1105 -- Add hook for custom context in `BrowsableAPIRenderer`. Replace hard coded response status check with `allow_form` context variable, so that it can be overridden in a custom renderer class. --- rest_framework/renderers.py | 47 +++++++++---------- .../templates/rest_framework/base.html | 2 +- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fca67eeeb..6597123f8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -564,9 +564,9 @@ class BrowsableAPIRenderer(BaseRenderer): def get_breadcrumbs(self, request): return get_breadcrumbs(request.path) - def render(self, data, accepted_media_type=None, renderer_context=None): + def get_context(self, data, accepted_media_type, renderer_context): """ - Render the HTML for the browsable API representation. + Returns the context used to render. """ self.accepted_media_type = accepted_media_type or '' self.renderer_context = renderer_context or {} @@ -576,55 +576,52 @@ class BrowsableAPIRenderer(BaseRenderer): response = renderer_context['response'] renderer = self.get_default_renderer(view) - content = self.get_content(renderer, data, accepted_media_type, renderer_context) - - put_form = self.get_rendered_html_form(view, 'PUT', request) - post_form = self.get_rendered_html_form(view, 'POST', request) - patch_form = self.get_rendered_html_form(view, 'PATCH', request) - delete_form = self.get_rendered_html_form(view, 'DELETE', request) - options_form = self.get_rendered_html_form(view, 'OPTIONS', request) raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) - raw_data_post_form = self.get_raw_data_form(view, 'POST', request) raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form - name = self.get_name(view) - description = self.get_description(view) - breadcrumb_list = self.get_breadcrumbs(request) - - template = loader.get_template(self.template) context = RequestContext(request, { - 'content': content, + 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, 'request': request, 'response': response, - 'description': description, - 'name': name, + 'description': self.get_description(view), + 'name': self.get_name(view), 'version': VERSION, - 'breadcrumblist': breadcrumb_list, + 'breadcrumblist': self.get_breadcrumbs(request), 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], - 'put_form': put_form, - 'post_form': post_form, - 'patch_form': patch_form, - 'delete_form': delete_form, - 'options_form': options_form, + 'put_form': self.get_rendered_html_form(view, 'PUT', request), + 'post_form': self.get_rendered_html_form(view, 'POST', request), + 'patch_form': self.get_rendered_html_form(view, 'PATCH', request), + 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), + 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, - 'raw_data_post_form': raw_data_post_form, + 'raw_data_post_form': self.get_raw_data_form(view, 'POST', request), 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, + 'allow_form': bool(response.status_code != 403), + 'api_settings': api_settings }) + return context + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render the HTML for the browsable API representation. + """ + template = loader.get_template(self.template) + context = self.get_context(data, accepted_media_type, renderer_context) ret = template.render(context) # Munge DELETE Response code to allow us to return content # (Do this *after* we've rendered the template so that we include # the normal deletion response code in the output) + response = renderer_context['response'] if response.status_code == status.HTTP_204_NO_CONTENT: response.status_code = status.HTTP_200_OK diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index aa90e90c4..88e58deb0 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -122,7 +122,7 @@
    - {% if response.status_code != 403 %} + {% if allow_form %} {% if post_form or raw_data_post_form %}
    From a9dbd46c9470003a1dd41e66a113d50b0217a110 Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Sat, 14 Sep 2013 00:54:44 +1000 Subject: [PATCH 0166/1652] Refs #1109 -- Update docs. Integrate changes from feedback. --- docs/topics/browsable-api.md | 3 +++ rest_framework/renderers.py | 13 +++++++------ rest_framework/templates/rest_framework/base.html | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index b2c78f3c6..e32db6958 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -115,6 +115,7 @@ The context that's available to the template: * `name` : The name of the resource * `post_form` : A form instance for use by the POST form (if allowed) * `put_form` : A form instance for use by the PUT form (if allowed) +* `display_edit_forms` : A boolean indicating whether or not POST, PUT and PATCH forms will be displayed * `request` : The request object * `response` : The response object * `version` : The version of Django REST Framework @@ -122,6 +123,8 @@ The context that's available to the template: * `FORMAT_PARAM` : The view can accept a format override * `METHOD_PARAM` : The view can accept a method override +You can override the `BrowsableAPIRenderer.get_context()` method to customise the context that gets passed to the template. + #### Not using base.html For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6597123f8..2ce51e97b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -568,9 +568,6 @@ class BrowsableAPIRenderer(BaseRenderer): """ Returns the context used to render. """ - self.accepted_media_type = accepted_media_type or '' - self.renderer_context = renderer_context or {} - view = renderer_context['view'] request = renderer_context['request'] response = renderer_context['response'] @@ -581,7 +578,7 @@ class BrowsableAPIRenderer(BaseRenderer): raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form - context = RequestContext(request, { + context = { 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, 'request': request, @@ -604,18 +601,22 @@ class BrowsableAPIRenderer(BaseRenderer): 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, - 'allow_form': bool(response.status_code != 403), + 'display_edit_forms': bool(response.status_code != 403), 'api_settings': api_settings - }) + } return context def render(self, data, accepted_media_type=None, renderer_context=None): """ Render the HTML for the browsable API representation. """ + self.accepted_media_type = accepted_media_type or '' + self.renderer_context = renderer_context or {} + template = loader.get_template(self.template) context = self.get_context(data, accepted_media_type, renderer_context) + context = RequestContext(renderer_context['request'], context) ret = template.render(context) # Munge DELETE Response code to allow us to return content diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 88e58deb0..2776d5500 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -122,7 +122,7 @@
    - {% if allow_form %} + {% if display_edit_forms %} {% if post_form or raw_data_post_form %}
    From d75ecb3d69d01849685864341c89d59e6a3121cd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Sep 2013 19:40:58 +0100 Subject: [PATCH 0167/1652] Added @mrmachine. Thanks! For work on #1109. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 8269580ef..4483f1707 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -168,6 +168,7 @@ The following people have helped make REST framework great. * Mathieu Pillard - [diox] * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] +* Tai Lee - [mrmachine] Many thanks to everyone who's contributed to the project. @@ -372,3 +373,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [diox]: https://github.com/diox [edmondwong]: https://github.com/edmondwong [bwreilly]: https://github.com/bwreilly +[mrmachine]: https://github.com/mrmachine From e8c6cd5622f62fcf2d4cf2b28b504fe5ff5228f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Sep 2013 19:43:15 +0100 Subject: [PATCH 0168/1652] Update release notes. --- docs/topics/release-notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3b35d9ed6..e4294ae33 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* Added JSON renderer support for numpy scalars. +* Added `get_context` hook in `BrowsableAPIRenderer`. + ### 2.3.8 **Date**: 11th September 2013 From b74c5235c509738c7afea0be0dd8283bb8339ebe Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 21:56:43 -0700 Subject: [PATCH 0169/1652] [Add]: CustomValidationTests.test_partial_update This test is to make sure that validate_ is not called when partial=True and is not found in .data. --- rest_framework/tests/test_serializer.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..9792685ec 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -496,6 +496,33 @@ class CustomValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']}) + def test_partial_update(self): + """ + Make sure that validate_email isn't called when partial=True and email + isn't found in data. + """ + initial_data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = self.CommentSerializerWithFieldValidator(data=initial_data) + self.assertEqual(serializer.is_valid(), True) + instance = serializer.object + + new_content = 'An *updated* test comment' + partial_data = { + 'content': new_content + } + + serializer = self.CommentSerializerWithFieldValidator(instance=instance, + data=partial_data, + partial=True) + self.assertEqual(serializer.is_valid(), True) + instance = serializer.object + self.assertEqual(instance.content, new_content) + class PositiveIntegerAsChoiceTests(TestCase): def test_positive_integer_in_json_is_correctly_parsed(self): From 3d8fad04446110db93ed2a13866e91beb9604934 Mon Sep 17 00:00:00 2001 From: Braulio Soncco Date: Wed, 18 Sep 2013 00:33:05 -0500 Subject: [PATCH 0170/1652] Fixing simple typo --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 30966a106..6ff97f37d 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -147,7 +147,7 @@ Similarly, we can control the format of the request that we send, using the `Con # POST using form data curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" - {"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"} + {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"} # POST using JSON curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" From f07a4f4ca3812fbe45d698e4ba0f9ff9099b6887 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:10:16 +0200 Subject: [PATCH 0171/1652] Clear cached serializer data on `save()` + test. Fixes #1116. --- rest_framework/serializers.py | 3 +++ rest_framework/tests/test_serializer.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..8d2e0feb8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -518,6 +518,9 @@ class BaseSerializer(WritableField): """ Save the deserialized object and return it. """ + # Clear cached _data, which may be invalidated by `save()` + self._data = None + if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..43d244119 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH @@ -136,6 +137,7 @@ class BasicTests(TestCase): 'Happy new year!', datetime.datetime(2012, 1, 1) ) + self.actionitem = ActionItem(title='Some to do item',) self.data = { 'email': 'tom@example.com', 'content': 'Happy new year!', @@ -264,6 +266,20 @@ class BasicTests(TestCase): """ self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, []) + def test_serializer_data_is_cleared_on_save(self): + """ + Check _data attribute is cleared on `save()` + + Regression test for #1116 + — id field is not populated is `data` is accessed prior to `save()` + """ + serializer = ActionItemSerializer(self.actionitem) + self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') + serializer.save() + self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') + + + class DictStyleSerializer(serializers.Serializer): """ From b82c44af48f25c0a60880b8d702caf6a74d80baa Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:20:21 +0200 Subject: [PATCH 0172/1652] Correct typo in doc string. --- rest_framework/tests/test_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 43d244119..8d246b010 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -271,7 +271,7 @@ class BasicTests(TestCase): Check _data attribute is cleared on `save()` Regression test for #1116 - — id field is not populated is `data` is accessed prior to `save()` + — id field is not populated if `data` is accessed prior to `save()` """ serializer = ActionItemSerializer(self.actionitem) self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') From de6e7accef0f144f4bcae9618604d462b1d9d321 Mon Sep 17 00:00:00 2001 From: John Mee Date: Mon, 23 Sep 2013 14:03:09 +1000 Subject: [PATCH 0173/1652] Mindnumbingly trivial single-char typo. --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 06eec3c4e..80bb9abb4 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -85,7 +85,7 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty queryset = Group.objects.all() serializer_class = GroupSerializer -Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`. +Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. From fda0c520bd3fb81c7eec06da70bb7abfed519d93 Mon Sep 17 00:00:00 2001 From: Grzegorz Kapkowski Date: Mon, 23 Sep 2013 16:10:46 +0200 Subject: [PATCH 0174/1652] Match docs to current code. --- docs/api-guide/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 859e8d52a..784aa585e 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -195,9 +195,9 @@ For more details on using filter sets see the [django-filter documentation][djan ## SearchFilter -The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. +The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. class UserListView(generics.ListAPIView): queryset = User.objects.all() From abbe9213f98b5e1d3b53db2c1711d9221c5b257f Mon Sep 17 00:00:00 2001 From: Markus Kaiserswerth Date: Mon, 23 Sep 2013 17:48:25 +0200 Subject: [PATCH 0175/1652] Address pending deprecation of Model._meta.module_name in Django 1.6 --- rest_framework/compat.py | 8 ++++++++ rest_framework/filters.py | 4 ++-- rest_framework/permissions.py | 7 ++++--- rest_framework/tests/test_permissions.py | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b9d1dae6b..581e29fc7 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -80,6 +80,14 @@ except ImportError: Image = None +def get_model_name(model_cls): + try: + return model_cls._meta.model_name + except AttributeError: + # < 1.6 used module_name instead of model_name + return model_cls._meta.module_name + + def get_concrete_model(model_cls): try: return model_cls._meta.concrete_model diff --git a/rest_framework/filters.py b/rest_framework/filters.py index b8fe7f77e..e287a1683 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -4,7 +4,7 @@ returned by list views. """ from __future__ import unicode_literals from django.db import models -from rest_framework.compat import django_filters, six, guardian +from rest_framework.compat import django_filters, six, guardian, get_model_name from functools import reduce import operator @@ -158,7 +158,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): model_cls = queryset.model kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } permission = self.perm_format % kwargs return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 531847988..ab6655e7b 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,7 +8,8 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import oauth2_provider_scope, oauth2_constants +from rest_framework.compat import (get_model_name, oauth2_provider_scope, + oauth2_constants) class BasePermission(object): @@ -116,7 +117,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } return [perm % kwargs for perm in self.perms_map[method]] @@ -177,7 +178,7 @@ class DjangoObjectPermissions(DjangoModelPermissions): def get_required_object_permissions(self, method, model_cls): kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d08124f48..6e3a63034 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -4,7 +4,7 @@ from django.db import models from django.test import TestCase from django.utils import unittest from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING -from rest_framework.compat import guardian +from rest_framework.compat import guardian, get_model_name from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel @@ -202,7 +202,7 @@ class ObjectPermissionsIntegrationTests(TestCase): # give everyone model level permissions, as we are not testing those everyone = Group.objects.create(name='everyone') - model_name = BasicPermModel._meta.module_name + model_name = get_model_name(BasicPermModel) app_label = BasicPermModel._meta.app_label f = '{0}_{1}'.format perms = { From 57a51f790bdff6c1cf34f3c6d64b0bef3033d89e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Sep 2013 09:21:55 +0100 Subject: [PATCH 0176/1652] Added @mkai, for work on #1126. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 4483f1707..b5dce5040 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -169,6 +169,7 @@ The following people have helped make REST framework great. * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] * Tai Lee - [mrmachine] +* Markus Kaiserswerth - [mkai] Many thanks to everyone who's contributed to the project. @@ -374,3 +375,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [edmondwong]: https://github.com/edmondwong [bwreilly]: https://github.com/bwreilly [mrmachine]: https://github.com/mrmachine +[mkai]: https://github.com/mkai From e441f85109e64345a12e65062fc0e51c5787e67f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Sep 2013 10:30:04 +0100 Subject: [PATCH 0177/1652] Drop 1.3 support --- .travis.yml | 12 +- rest_framework/authentication.py | 2 +- rest_framework/compat.py | 381 +----------------- rest_framework/fields.py | 4 +- rest_framework/routers.py | 2 +- rest_framework/runtests/settings.py | 5 +- rest_framework/runtests/urls.py | 2 +- rest_framework/serializers.py | 6 +- .../templates/rest_framework/base.html | 1 + .../templates/rest_framework/login_base.html | 1 + rest_framework/templatetags/rest_framework.py | 87 +--- rest_framework/tests/test_authentication.py | 2 +- rest_framework/tests/test_breadcrumbs.py | 2 +- rest_framework/tests/test_filters.py | 3 +- rest_framework/tests/test_htmlrenderer.py | 2 +- .../tests/test_hyperlinkedserializers.py | 4 +- .../tests/test_relations_hyperlink.py | 2 +- rest_framework/tests/test_renderers.py | 3 +- rest_framework/tests/test_request.py | 2 +- rest_framework/tests/test_response.py | 2 +- rest_framework/tests/test_reverse.py | 2 +- rest_framework/tests/test_routers.py | 2 +- rest_framework/tests/test_testing.py | 2 +- rest_framework/tests/test_urlpatterns.py | 2 +- rest_framework/urlpatterns.py | 2 +- rest_framework/urls.py | 2 +- rest_framework/utils/encoders.py | 3 +- tox.ini | 22 +- 28 files changed, 56 insertions(+), 506 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7ebe715a0..456f8e9c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,18 +10,15 @@ env: - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/" - DJANGO="django==1.5.1 --use-mirrors" - DJANGO="django==1.4.5 --use-mirrors" - - DJANGO="django==1.3.7 --use-mirrors" install: - pip install $DJANGO - - pip install defusedxml==0.3 + - pip install defusedxml==0.3 --use-mirrors + - pip install django-filter==0.6 --use-mirrors - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" - - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install six --use-mirrors; fi" - - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" - export PYTHONPATH=. script: @@ -31,10 +28,5 @@ matrix: exclude: - python: "3.2" env: DJANGO="django==1.4.5 --use-mirrors" - - python: "3.2" - env: DJANGO="django==1.3.7 --use-mirrors" - python: "3.3" env: DJANGO="django==1.4.5 --use-mirrors" - - python: "3.3" - env: DJANGO="django==1.3.7 --use-mirrors" - diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cf001a24d..db5cce40d 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,8 +6,8 @@ import base64 from django.contrib.auth import authenticate from django.core.exceptions import ImproperlyConfigured +from django.middleware.csrf import CsrfViewMiddleware from rest_framework import exceptions, HTTP_HEADER_ENCODING -from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import oauth, oauth_provider, oauth_provider_store from rest_framework.compat import oauth2_provider, provider_now from rest_framework.authtoken.models import Token diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 1238f043a..f048b10ad 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,25 +5,19 @@ versions of django/python, and compatibility wrappers around optional packages. # flake8: noqa from __future__ import unicode_literals - import django from django.core.exceptions import ImproperlyConfigured from django.conf import settings + # Try to import six from Django, fallback to external `six` package. try: from django.utils import six except ImportError: import six -# location of patterns, url, include changes in 1.4 onwards -try: - from django.conf.urls import patterns, url, include -except ImportError: - from django.conf.urls.defaults import patterns, url, include - -# Handle django.utils.encoding rename: -# smart_unicode -> smart_text +# Handle django.utils.encoding rename in 1.5 onwards. +# smart_unicode -> smart_text # force_unicode -> force_text try: from django.utils.encoding import smart_text @@ -41,13 +35,15 @@ try: except ImportError: from django.http import HttpResponse as HttpResponseBase + # django-filter is optional try: import django_filters except ImportError: django_filters = None -# guardian is optional + +# django-guardian is optional try: import guardian except ImportError: @@ -80,14 +76,6 @@ except ImportError: Image = None -def get_concrete_model(model_cls): - try: - return model_cls._meta.concrete_model - except AttributeError: - # 1.3 does not include concrete model - return model_cls - - # Django 1.5 add support for custom auth user model if django.VERSION >= (1, 5): AUTH_USER_MODEL = settings.AUTH_USER_MODEL @@ -95,46 +83,13 @@ else: AUTH_USER_MODEL = 'auth.User' +# View._allowed_methods only present from 1.5 onwards if django.VERSION >= (1, 5): from django.views.generic import View else: - from django.views.generic import View as _View - from django.utils.decorators import classonlymethod - from django.utils.functional import update_wrapper + from django.views.generic import View as DjangoView - class View(_View): - # 1.3 does not include head method in base View class - # See: https://code.djangoproject.com/ticket/15668 - @classonlymethod - def as_view(cls, **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) - 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 - - # _allowed_methods only present from 1.5 onwards + class View(DjangoView): def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] @@ -144,316 +99,16 @@ if 'patch' not in View.http_method_names: View.http_method_names = View.http_method_names + ['patch'] -# PUT, DELETE do not require CSRF until 1.4. They should. Make it better. -if django.VERSION >= (1, 4): - from django.middleware.csrf import CsrfViewMiddleware -else: - import hashlib - import re - import random - import logging - - from django.conf import settings - from django.core.urlresolvers import get_callable - - try: - from logging import NullHandler - except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass - - logger = logging.getLogger('django.request') - - if not logger.handlers: - logger.addHandler(NullHandler()) - - def same_origin(url1, url2): - """ - Checks if two URLs are 'same-origin' - """ - p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2) - return p1[0:2] == p2[0:2] - - def constant_time_compare(val1, val2): - """ - Returns True if the two strings are equal, False otherwise. - - The time taken is independent of the number of characters that match. - """ - if len(val1) != len(val2): - return False - result = 0 - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - return result == 0 - - # Use the system (hardware-based) random number generator if it exists. - if hasattr(random, 'SystemRandom'): - randrange = random.SystemRandom().randrange - else: - randrange = random.randrange - - _MAX_CSRF_KEY = 18446744073709551616 # 2 << 63 - - REASON_NO_REFERER = "Referer checking failed - no Referer." - REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." - REASON_NO_CSRF_COOKIE = "CSRF cookie not set." - REASON_BAD_TOKEN = "CSRF token missing or incorrect." - - def _get_failure_view(): - """ - Returns the view to be used for CSRF rejections - """ - return get_callable(settings.CSRF_FAILURE_VIEW) - - def _get_new_csrf_key(): - return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - - def get_token(request): - """ - Returns the the CSRF token required for a POST form. The token is an - alphanumeric value. - - A side effect of calling this function is to make the the csrf_protect - decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' - header to the outgoing response. For this reason, you may need to use this - function lazily, as is done by the csrf context processor. - """ - request.META["CSRF_COOKIE_USED"] = True - return request.META.get("CSRF_COOKIE", None) - - def _sanitize_token(token): - # Allow only alphanum, and ensure we return a 'str' for the sake of the post - # processing middleware. - token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore'))) - if token == "": - # In case the cookie has been truncated to nothing at some point. - return _get_new_csrf_key() - else: - return token - - class CsrfViewMiddleware(object): - """ - Middleware that requires a present and correct csrfmiddlewaretoken - for POST requests that have a CSRF cookie, and sets an outgoing - CSRF cookie. - - This middleware should be used in conjunction with the csrf_token template - tag. - """ - # The _accept and _reject methods currently only exist for the sake of the - # requires_csrf_token decorator. - def _accept(self, request): - # Avoid checking the request twice by adding a custom attribute to - # request. This will be relevant when both decorator and middleware - # are used. - request.csrf_processing_done = True - return None - - def _reject(self, request, reason): - return _get_failure_view()(request, reason=reason) - - def process_view(self, request, callback, callback_args, callback_kwargs): - - if getattr(request, 'csrf_processing_done', False): - return None - - try: - csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME]) - # Use same token next time - request.META['CSRF_COOKIE'] = csrf_token - except KeyError: - csrf_token = None - # Generate token and store it in the request, so it's available to the view. - request.META["CSRF_COOKIE"] = _get_new_csrf_key() - - # Wait until request.META["CSRF_COOKIE"] has been manipulated before - # bailing out, so that get_token still works - if getattr(callback, 'csrf_exempt', False): - return None - - # Assume that anything not defined as 'safe' by RC2616 needs protection. - if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): - if getattr(request, '_dont_enforce_csrf_checks', False): - # Mechanism to turn off CSRF checks for test suite. It comes after - # the creation of CSRF cookies, so that everything else continues to - # work exactly the same (e.g. cookies are sent etc), but before the - # any branches that call reject() - return self._accept(request) - - if request.is_secure(): - # Suppose user visits http://example.com/ - # An active network attacker,(man-in-the-middle, MITM) sends a - # POST form which targets https://example.com/detonate-bomb/ and - # submits it via javascript. - # - # The attacker will need to provide a CSRF cookie and token, but - # that is no problem for a MITM and the session independent - # nonce we are using. So the MITM can circumvent the CSRF - # protection. This is true for any HTTP connection, but anyone - # using HTTPS expects better! For this reason, for - # https://example.com/ we need additional protection that treats - # http://example.com/ as completely untrusted. Under HTTPS, - # Barth et al. found that the Referer header is missing for - # same-domain requests in only about 0.2% of cases or less, so - # we can use strict Referer checking. - referer = request.META.get('HTTP_REFERER') - if referer is None: - logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_NO_REFERER) - - # Note that request.get_host() includes the port - good_referer = 'https://%s/' % request.get_host() - if not same_origin(referer, good_referer): - reason = REASON_BAD_REFERER % (referer, good_referer) - logger.warning('Forbidden (%s): %s' % (reason, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, reason) - - if csrf_token is None: - # No CSRF cookie. For POST requests, we insist on a CSRF cookie, - # and in this way we can avoid all CSRF attacks, including login - # CSRF. - logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_NO_CSRF_COOKIE) - - # check non-cookie token for match - request_csrf_token = "" - if request.method == "POST": - request_csrf_token = request.POST.get('csrfmiddlewaretoken', '') - - if request_csrf_token == "": - # Fall back to X-CSRFToken, to make things easier for AJAX, - # and possible for PUT/DELETE - request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') - - if not constant_time_compare(request_csrf_token, csrf_token): - logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path), - extra={ - 'status_code': 403, - 'request': request, - } - ) - return self._reject(request, REASON_BAD_TOKEN) - - return self._accept(request) - -# timezone support is new in Django 1.4 -try: - from django.utils import timezone -except ImportError: - timezone = None - -# dateparse is ALSO new in Django 1.4 -try: - from django.utils.dateparse import parse_date, parse_datetime, parse_time -except ImportError: - import datetime - import re - - date_re = re.compile( - r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})$' - ) - - datetime_re = re.compile( - r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' - r'[T ](?P\d{1,2}):(?P\d{1,2})' - r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' - r'(?PZ|[+-]\d{1,2}:\d{1,2})?$' - ) - - time_re = re.compile( - r'(?P\d{1,2}):(?P\d{1,2})' - r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' - ) - - def parse_date(value): - match = date_re.match(value) - if match: - kw = dict((k, int(v)) for k, v in match.groupdict().iteritems()) - return datetime.date(**kw) - - def parse_time(value): - match = time_re.match(value) - if match: - kw = match.groupdict() - if kw['microsecond']: - kw['microsecond'] = kw['microsecond'].ljust(6, '0') - kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) - return datetime.time(**kw) - - def parse_datetime(value): - """Parse datetime, but w/o the timezone awareness in 1.4""" - match = datetime_re.match(value) - if match: - kw = match.groupdict() - if kw['microsecond']: - kw['microsecond'] = kw['microsecond'].ljust(6, '0') - kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None) - return datetime.datetime(**kw) - - -# smart_urlquote is new on Django 1.4 -try: - from django.utils.html import smart_urlquote -except ImportError: - import re - from django.utils.encoding import smart_str - try: - from urllib.parse import quote, urlsplit, urlunsplit - except ImportError: # Python 2 - from urllib import quote - from urlparse import urlsplit, urlunsplit - - unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})') - - def smart_urlquote(url): - "Quotes a URL if it isn't already quoted." - # Handle IDN before quoting. - scheme, netloc, path, query, fragment = urlsplit(url) - try: - netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE - except UnicodeError: # invalid domain part - pass - else: - url = urlunsplit((scheme, netloc, path, query, fragment)) - - # An URL is considered unquoted if it contains no % characters or - # contains a % not followed by two hexadecimal digits. See #9655. - if '%' not in url or unquoted_percents_re.search(url): - # See http://bugs.python.org/issue2637 - url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~') - - return force_text(url) - - -# RequestFactory only provide `generic` from 1.5 onwards - +# RequestFactory only provides `generic` from 1.5 onwards from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import FakePayload try: # In 1.5 the test client uses force_bytes from django.utils.encoding import force_bytes_or_smart_bytes except ImportError: - # In 1.3 and 1.4 the test client just uses smart_str + # In 1.4 the test client just uses smart_str from django.utils.encoding import smart_str as force_bytes_or_smart_bytes - class RequestFactory(DjangoRequestFactory): def generic(self, method, path, data='', content_type='application/octet-stream', **extra): @@ -478,6 +133,7 @@ class RequestFactory(DjangoRequestFactory): r.update(extra) return self.request(**r) + # Markdown is optional try: import markdown @@ -492,7 +148,6 @@ try: safe_mode = False md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode) return md.convert(text) - except ImportError: apply_markdown = None @@ -510,14 +165,16 @@ try: except ImportError: etree = None -# OAuth is optional + +# OAuth2 is optional try: # Note: The `oauth2` package actually provides oauth1.0a support. Urg. import oauth2 as oauth except ImportError: oauth = None -# OAuth is optional + +# OAuthProvider is optional try: import oauth_provider from oauth_provider.store import store as oauth_provider_store @@ -525,6 +182,7 @@ except (ImportError, ImproperlyConfigured): oauth_provider = None oauth_provider_store = None + # OAuth 2 support is optional try: import provider.oauth2 as oauth2_provider @@ -542,8 +200,6 @@ try: # Any other supported version does use timezone aware datetimes from django.utils.timezone import now as provider_now except ImportError: - import traceback - traceback.print_exc() oauth2_provider = None oauth2_provider_models = None oauth2_provider_forms = None @@ -551,7 +207,8 @@ except ImportError: oauth2_constants = None provider_now = None -# Handle lazy strings + +# Handle lazy strings across Py2/Py3 from django.utils.functional import Promise if six.PY3: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b3a9b0dfe..f340510d3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,12 +18,14 @@ from django.conf import settings from django.db.models.fields import BLANK_CHOICE_DASH from django.http import QueryDict from django.forms import widgets +from django.utils import timezone from django.utils.encoding import is_protected_type from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict +from django.utils.dateparse import parse_date, parse_datetime, parse_time from rest_framework import ISO_8601 from rest_framework.compat import ( - timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text, + BytesIO, six, smart_text, force_text, is_non_str_iterable ) from rest_framework.settings import api_settings diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 1c7a81585..790299cc3 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -17,9 +17,9 @@ from __future__ import unicode_literals import itertools from collections import namedtuple +from django.conf.urls import patterns, url from django.core.exceptions import ImproperlyConfigured from rest_framework import views -from rest_framework.compat import patterns, url from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.urlpatterns import format_suffix_patterns diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index be7216580..12aa73e7b 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -93,10 +93,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', + 'django.contrib.staticfiles', 'rest_framework', 'rest_framework.authtoken', 'rest_framework.tests', diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py index ed5baeae6..dff710115 100644 --- a/rest_framework/runtests/urls.py +++ b/rest_framework/runtests/urls.py @@ -1,7 +1,7 @@ """ Blank URLConf just to keep runtests.py happy. """ -from rest_framework.compat import patterns +from django.conf.urls import patterns urlpatterns = patterns('', ) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f17757625..9e3881a2c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -18,7 +18,7 @@ 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, six +from rest_framework.compat import six # Note: We do the following so that users of the framework can use this style: # @@ -575,7 +575,7 @@ class ModelSerializer(Serializer): cls = self.opts.model assert cls is not None, \ "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__ - opts = get_concrete_model(cls)._meta + opts = cls._meta.concrete_model._meta ret = SortedDict() nested = bool(self.opts.depth) @@ -784,7 +784,7 @@ class ModelSerializer(Serializer): Return a list of field names to exclude from model validation. """ cls = self.opts.model - opts = get_concrete_model(cls)._meta + opts = cls._meta.concrete_model._meta exclusions = [field.name for field in opts.fields + opts.many_to_many] for field_name, field in self.fields.items(): diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 2776d5500..47377d51f 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,4 +1,5 @@ {% load url from future %} +{% load staticfiles %} {% load rest_framework %} diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index be9a0072a..be83c2f53 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -1,4 +1,5 @@ {% load url from future %} +{% load staticfiles %} {% load rest_framework %} diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index e9c1cdd54..55f361490 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -2,97 +2,14 @@ from __future__ import unicode_literals, absolute_import from django import template from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict -from django.utils.html import escape +from django.utils.html import escape, smart_urlquote from django.utils.safestring import SafeData, mark_safe -from rest_framework.compat import urlparse, force_text, six, smart_urlquote +from rest_framework.compat import urlparse, force_text, six import re, string register = template.Library() -# Note we don't use 'load staticfiles', because we need a 1.3 compatible -# version, so instead we include the `static` template tag ourselves. - -# When 1.3 becomes unsupported by REST framework, we can instead start to -# use the {% load staticfiles %} tag, remove the following code, -# and add a dependency that `django.contrib.staticfiles` must be installed. - -# Note: We can't put this into the `compat` module because the compat import -# from rest_framework.compat import ... -# conflicts with this rest_framework template tag module. - -try: # Django 1.5+ - from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode - - @register.tag('static') - def do_static(parser, token): - return StaticFilesNode.handle_token(parser, token) - -except ImportError: - try: # Django 1.4 - from django.contrib.staticfiles.storage import staticfiles_storage - - @register.simple_tag - def static(path): - """ - A template tag that returns the URL to a file - using staticfiles' storage backend - """ - return staticfiles_storage.url(path) - - except ImportError: # Django 1.3 - from urlparse import urljoin - from django import template - from django.templatetags.static import PrefixNode - - class StaticNode(template.Node): - def __init__(self, varname=None, path=None): - if path is None: - raise template.TemplateSyntaxError( - "Static template nodes must be given a path to return.") - self.path = path - self.varname = varname - - def url(self, context): - path = self.path.resolve(context) - return self.handle_simple(path) - - def render(self, context): - url = self.url(context) - if self.varname is None: - return url - context[self.varname] = url - return '' - - @classmethod - def handle_simple(cls, path): - return urljoin(PrefixNode.handle_simple("STATIC_URL"), path) - - @classmethod - def handle_token(cls, parser, token): - """ - Class method to parse prefix node and return a Node. - """ - bits = token.split_contents() - - if len(bits) < 2: - raise template.TemplateSyntaxError( - "'%s' takes at least one argument (path to file)" % bits[0]) - - path = parser.compile_filter(bits[1]) - - if len(bits) >= 2 and bits[-2] == 'as': - varname = bits[3] - else: - varname = None - - return cls(varname, path) - - @register.tag('static') - def do_static_13(parser, token): - return StaticNode.handle_token(parser, token) - - def replace_query_param(url, key, val): """ Given a URL and a key/val pair, set or replace an item in the query diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index a44813b69..e9a817c0f 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url, include from django.contrib.auth.models import User from django.http import HttpResponse from django.test import TestCase @@ -18,7 +19,6 @@ from rest_framework.authentication import ( OAuth2Authentication ) from rest_framework.authtoken.models import Token -from rest_framework.compat import patterns, url, include from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient diff --git a/rest_framework/tests/test_breadcrumbs.py b/rest_framework/tests/test_breadcrumbs.py index 41ddf2cea..33740cbb2 100644 --- a/rest_framework/tests/test_breadcrumbs.py +++ b/rest_framework/tests/test_breadcrumbs.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url from django.test import TestCase -from rest_framework.compat import patterns, url from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.views import APIView diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 379db29d8..9697c5eef 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals import datetime from decimal import Decimal +from django.conf.urls import patterns, url from django.db import models from django.core.urlresolvers import reverse from django.test import TestCase from django.utils import unittest from rest_framework import generics, serializers, status, filters -from rest_framework.compat import django_filters, patterns, url +from rest_framework.compat import django_filters from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel diff --git a/rest_framework/tests/test_htmlrenderer.py b/rest_framework/tests/test_htmlrenderer.py index 8957a43c7..6c570dfdc 100644 --- a/rest_framework/tests/test_htmlrenderer.py +++ b/rest_framework/tests/test_htmlrenderer.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals from django.core.exceptions import PermissionDenied +from django.conf.urls import patterns, url from django.http import Http404 from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework import status -from rest_framework.compat import patterns, url from rest_framework.decorators import api_view, renderer_classes from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py index 61e613d75..ea7f70f2f 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals import json +from django.conf.urls import patterns, url from django.test import TestCase from rest_framework import generics, status, serializers -from rest_framework.compat import patterns, url from rest_framework.test import APIRequestFactory from rest_framework.tests.models import ( Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, @@ -24,7 +24,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer): class PhotoSerializer(serializers.Serializer): description = serializers.CharField() - album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title') + album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title') def restore_object(self, attrs, instance=None): return Photo(**attrs) diff --git a/rest_framework/tests/test_relations_hyperlink.py b/rest_framework/tests/test_relations_hyperlink.py index 3c4d39af6..fa6b01aca 100644 --- a/rest_framework/tests/test_relations_hyperlink.py +++ b/rest_framework/tests/test_relations_hyperlink.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url from django.test import TestCase from rest_framework import serializers -from rest_framework.compat import patterns, url from rest_framework.test import APIRequestFactory from rest_framework.tests.models import ( BlogPost, diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index df6f4aa63..9d1dd77e5 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals from decimal import Decimal +from django.conf.urls import patterns, url, include from django.core.cache import cache from django.test import TestCase from django.utils import unittest from django.utils.translation import ugettext_lazy as _ from rest_framework import status, permissions -from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO +from rest_framework.compat import yaml, etree, six, StringIO from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 969d8024a..d63634258 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -2,13 +2,13 @@ Tests for content parsing, and form-overloaded content parsing. """ from __future__ import unicode_literals +from django.conf.urls import patterns from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication -from rest_framework.compat import patterns from rest_framework.parsers import ( BaseParser, FormParser, diff --git a/rest_framework/tests/test_response.py b/rest_framework/tests/test_response.py index eea3c6418..1c4c551cf 100644 --- a/rest_framework/tests/test_response.py +++ b/rest_framework/tests/test_response.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url, include from django.test import TestCase from rest_framework.tests.models import BasicModel, BasicModelSerializer -from rest_framework.compat import patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import generics diff --git a/rest_framework/tests/test_reverse.py b/rest_framework/tests/test_reverse.py index 690a30b11..320b125d2 100644 --- a/rest_framework/tests/test_reverse.py +++ b/rest_framework/tests/test_reverse.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url from django.test import TestCase -from rest_framework.compat import patterns, url from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 3f456fefd..1c34648f4 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals +from django.conf.urls import patterns, url, include from django.db import models from django.test import TestCase from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers, viewsets, permissions -from rest_framework.compat import include, patterns, url from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 48b8956b5..c08dd4932 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -1,9 +1,9 @@ # -- coding: utf-8 -- from __future__ import unicode_literals +from django.conf.urls import patterns, url from django.contrib.auth.models import User from django.test import TestCase -from rest_framework.compat import patterns, url from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory, force_authenticate diff --git a/rest_framework/tests/test_urlpatterns.py b/rest_framework/tests/test_urlpatterns.py index 8132ec4c8..e0060e690 100644 --- a/rest_framework/tests/test_urlpatterns.py +++ b/rest_framework/tests/test_urlpatterns.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals from collections import namedtuple +from django.conf.urls import patterns, url, include from django.core import urlresolvers from django.test import TestCase from rest_framework.test import APIRequestFactory -from rest_framework.compat import patterns, url, include from rest_framework.urlpatterns import format_suffix_patterns diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index d9143bb4c..a62530c74 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals +from django.conf.urls import url, include from django.core.urlresolvers import RegexURLResolver -from rest_framework.compat import url, include from rest_framework.settings import api_settings diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 9c4719f1d..87ec0f0ae 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -13,7 +13,7 @@ your authentication settings include `SessionAuthentication`. ) """ from __future__ import unicode_literals -from rest_framework.compat import patterns, url +from django.conf.urls import patterns, url template_name = {'template_name': 'rest_framework/login.html'} diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 7efd5417b..13a855509 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -2,9 +2,10 @@ Helper classes for parsers. """ from __future__ import unicode_literals +from django.utils import timezone from django.utils.datastructures import SortedDict from django.utils.functional import Promise -from rest_framework.compat import timezone, force_text +from rest_framework.compat import force_text from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata import datetime import decimal diff --git a/tox.ini b/tox.ini index 6e3b8e0a8..7bd140e1c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 +envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4 [testenv] commands = {envpython} rest_framework/runtests/runtests.py @@ -88,23 +88,3 @@ deps = django==1.4.3 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 - -[testenv:py2.7-django1.3] -basepython = python2.7 -deps = django==1.3.5 - django-filter==0.5.4 - defusedxml==0.3 - django-oauth-plus==2.0 - oauth2==1.5.211 - django-oauth2-provider==0.2.3 - django-guardian==1.1.1 - -[testenv:py2.6-django1.3] -basepython = python2.6 -deps = django==1.3.5 - django-filter==0.5.4 - defusedxml==0.3 - django-oauth-plus==2.0 - oauth2==1.5.211 - django-oauth2-provider==0.2.3 - django-guardian==1.1.1 From 1bd8fe415296739521fd2e75c0b604cbf3dd3a83 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 25 Sep 2013 10:36:08 +0100 Subject: [PATCH 0178/1652] Whitespace fix --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index f048b10ad..efd2581fe 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -17,7 +17,7 @@ except ImportError: import six # Handle django.utils.encoding rename in 1.5 onwards. -# smart_unicode -> smart_text +# smart_unicode -> smart_text # force_unicode -> force_text try: from django.utils.encoding import smart_text From 75d6446c8799763dccde0f5f03fbcae39c18dc7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Sep 2013 16:09:08 +0100 Subject: [PATCH 0179/1652] Allow .template_name attribute specified on view. Closes #1000 --- rest_framework/renderers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2ce51e97b..a27160d46 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -272,7 +272,9 @@ class TemplateHTMLRenderer(BaseRenderer): return [self.template_name] elif hasattr(view, 'get_template_names'): return view.get_template_names() - raise ImproperlyConfigured('Returned a template response with no template_name') + elif hasattr(view, 'template_name'): + return [view.template_name] + raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response') def get_exception_template(self, response): template_names = [name % {'status_code': response.status_code} @@ -388,7 +390,7 @@ class HTMLFormRenderer(BaseRenderer): # likely change at some point. self.renderer_context = renderer_context or {} - request = renderer_context['request'] + request = self.renderer_context['request'] # Creating an on the fly form see: # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python @@ -419,8 +421,13 @@ class BrowsableAPIRenderer(BaseRenderer): """ renderers = [renderer for renderer in view.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer)] + non_template_renderers = [renderer for renderer in renderers + if not hasattr(renderer, 'get_template_names')] + if not renderers: return None + elif non_template_renderers: + return non_template_renderers[0]() return renderers[0]() def get_content(self, renderer, data, From 49847584a8524883181e4118a9df9bb62f597271 Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Sun, 29 Sep 2013 12:49:38 +0200 Subject: [PATCH 0180/1652] Minor documentation fix. --- docs/topics/ajax-csrf-cors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index 0555b84dd..97dd4710b 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -6,7 +6,7 @@ ## Javascript clients -If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. +If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. From 8a1d3275795a6eea931cb0b67465c88d745bd2b6 Mon Sep 17 00:00:00 2001 From: Doron Pearl Date: Mon, 30 Sep 2013 14:08:46 -0400 Subject: [PATCH 0181/1652] corrected doc for throttle_classes decorator the decorator actually expects an array and otherwise raise an exception. --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index cc4692171..fc1525df6 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -59,7 +59,7 @@ using the `APIView` class based views. Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @throttle_classes(UserRateThrottle) + @throttle_classes([UserRateThrottle]) def example_view(request, format=None): content = { 'status': 'request was permitted' From 61dd9ce914382ad7b26e2654a590215885308828 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 1 Oct 2013 15:47:47 -0600 Subject: [PATCH 0182/1652] Update release-notes.md Simple typo fix. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e4294ae33..3df8869a2 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -58,7 +58,7 @@ You can determine your currently installed version using `pip freeze`: * 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. -* Bugfix: Client sending emptry string instead of file now clears `FileField`. +* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. ### 2.3.7 From a14f1e886402b8d0f742fdbb912b03a4004e1735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 13:45:35 +0100 Subject: [PATCH 0183/1652] Serializers can now be rendered directly to HTML --- rest_framework/fields.py | 21 +++++++ rest_framework/renderers.py | 62 +------------------ rest_framework/serializers.py | 19 +++++- .../templates/rest_framework/form.html | 12 ++-- 4 files changed, 49 insertions(+), 65 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 210c2537d..16344d01b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -123,6 +123,7 @@ class Field(object): use_files = False form_field_class = forms.CharField type_label = 'field' + widget = None def __init__(self, source=None, label=None, help_text=None): self.parent = None @@ -134,9 +135,29 @@ class Field(object): if label is not None: self.label = smart_text(label) + else: + self.label = None if help_text is not None: self.help_text = strip_multiple_choice_msg(smart_text(help_text)) + else: + self.help_text = None + + self._errors = [] + self._value = None + self._name = None + + @property + def errors(self): + return self._errors + + def widget_html(self): + if not self.widget: + return '' + return self.widget.render(self._name, self._value) + + def label_tag(self): + return '' % (self._name, self.label) def initialize(self, parent, field_name): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a27160d46..0e17edafb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -336,71 +336,15 @@ class HTMLFormRenderer(BaseRenderer): template = 'rest_framework/form.html' charset = 'utf-8' - def data_to_form_fields(self, data): - fields = {} - for key, val in data.fields.items(): - if getattr(val, 'read_only', True): - # Don't include read-only fields. - continue - - if getattr(val, 'fields', None): - # Nested data not supported by HTML forms. - continue - - kwargs = {} - kwargs['required'] = val.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(val, 'choices', None) is not None: - kwargs['choices'] = val.choices - - if getattr(val, 'regex', None) is not None: - kwargs['regex'] = val.regex - - if getattr(val, 'widget', None): - widget = copy.deepcopy(val.widget) - kwargs['widget'] = widget - - if getattr(val, 'default', None) is not None: - kwargs['initial'] = val.default - - if getattr(val, 'label', None) is not None: - kwargs['label'] = val.label - - if getattr(val, 'help_text', None) is not None: - kwargs['help_text'] = val.help_text - - fields[key] = val.form_field_class(**kwargs) - - return fields - def render(self, data, accepted_media_type=None, renderer_context=None): """ Render serializer data and return an HTML form, as a string. """ - # The HTMLFormRenderer currently uses something of a hack to render - # the content, by translating each of the serializer fields into - # an html form field, creating a dynamic form using those fields, - # and then rendering that form. - - # This isn't strictly neccessary, as we could render the serilizer - # fields to HTML directly. The implementation is historical and will - # likely change at some point. - - self.renderer_context = renderer_context or {} - request = self.renderer_context['request'] - - # Creating an on the fly form see: - # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - fields = self.data_to_form_fields(data) - DynamicForm = type(str('DynamicForm'), (forms.Form,), fields) - data = None if data.empty else data + renderer_context = renderer_context or {} + request = renderer_context['request'] template = loader.get_template(self.template) - context = RequestContext(request, {'form': DynamicForm(data)}) - + context = RequestContext(request, {'form': data}) return template.render(context) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8d2e0feb8..206a8123b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -32,6 +32,13 @@ from rest_framework.relations import * from rest_framework.fields import * +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return '' + return name.replace('_', ' ').capitalize() + + class RelationsList(list): _deleted = [] @@ -306,7 +313,17 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) + if self._errors: + value = self.init_data.get(field_name) + else: + value = field.field_to_native(obj, field_name) + + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = value + if not field.label: + field.label = pretty_name(key) + ret[key] = value ret.fields[key] = field return ret diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html index b27f652e9..b1e148df7 100644 --- a/rest_framework/templates/rest_framework/form.html +++ b/rest_framework/templates/rest_framework/form.html @@ -1,13 +1,15 @@ {% load rest_framework %} {% csrf_token %} {{ form.non_field_errors }} -{% for field in form %} -
    +{% for field in form.fields.values %} + {% if not field.read_only %} +
    {{ field.label_tag|add_class:"control-label" }}
    - {{ field }} - {{ field.help_text }} - + {{ field.widget_html }} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {% for error in field.errors %}{{ error }}{% endfor %}
    + {% endif %} {% endfor %} From 8d4ba478cc5725b4de6ab86b4825b1ea94cb4c7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 16:13:34 +0100 Subject: [PATCH 0184/1652] Fix rendering of forms and add error rendering on HTML form --- rest_framework/renderers.py | 16 +++++++++--- rest_framework/serializers.py | 26 +++++++++---------- .../templates/rest_framework/base.html | 4 +-- .../rest_framework/raw_data_form.html | 12 +++++++++ 4 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 rest_framework/templates/rest_framework/raw_data_form.html diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0e17edafb..fe4f43d48 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -419,6 +419,13 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ + if request.method == method: + data = request.DATA + files = request.FILES + else: + data = None + files = None + with override_method(view, request, method) as request: obj = getattr(view, 'object', None) if not self.show_form_for_method(view, method, request, obj): @@ -431,9 +438,10 @@ class BrowsableAPIRenderer(BaseRenderer): or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): return - serializer = view.get_serializer(instance=obj) - + serializer = view.get_serializer(instance=obj, data=data, files=files) + serializer.is_valid() data = serializer.data + form_renderer = self.form_renderer_class() return form_renderer.render(data, self.accepted_media_type, self.renderer_context) @@ -525,6 +533,7 @@ class BrowsableAPIRenderer(BaseRenderer): renderer = self.get_default_renderer(view) + raw_data_post_form = self.get_raw_data_form(view, 'POST', request) raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form @@ -543,12 +552,11 @@ class BrowsableAPIRenderer(BaseRenderer): 'put_form': self.get_rendered_html_form(view, 'PUT', request), 'post_form': self.get_rendered_html_form(view, 'POST', request), - 'patch_form': self.get_rendered_html_form(view, 'PATCH', request), 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, - 'raw_data_post_form': self.get_raw_data_form(view, 'POST', request), + 'raw_data_post_form': raw_data_post_form, 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 206a8123b..bc9f73d11 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -308,24 +308,14 @@ class BaseSerializer(WritableField): """ ret = self._dict_class() ret.fields = self._dict_class() - ret.empty = obj is None for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - if self._errors: - value = self.init_data.get(field_name) - else: - value = field.field_to_native(obj, field_name) - - field._errors = self._errors.get(key) if self._errors else None - field._name = field_name - field._value = value - if not field.label: - field.label = pretty_name(key) - + value = field.field_to_native(obj, field_name) ret[key] = value - ret.fields[key] = field + ret.fields[key] = self.augment_field(field, field_name, key, value) + return ret def from_native(self, data, files): @@ -333,6 +323,7 @@ class BaseSerializer(WritableField): Deserialize primitives -> objects. """ self._errors = {} + if data is not None or files is not None: attrs = self.restore_fields(data, files) if attrs is not None: @@ -343,6 +334,15 @@ class BaseSerializer(WritableField): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + def field_to_native(self, obj, field_name): """ Override default so that the serializer can be used as a nested field diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 2776d5500..33be36db8 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -151,7 +151,7 @@ {% with form=raw_data_post_form %}
    - {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %}
    @@ -188,7 +188,7 @@ {% with form=raw_data_put_or_patch_form %}
    - {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %}
    {% if raw_data_put_form %} diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html new file mode 100644 index 000000000..075279f7e --- /dev/null +++ b/rest_framework/templates/rest_framework/raw_data_form.html @@ -0,0 +1,12 @@ +{% load rest_framework %} +{% csrf_token %} +{{ form.non_field_errors }} +{% for field in form %} +
    + {{ field.label_tag|add_class:"control-label" }} +
    + {{ field }} + {{ field.help_text }} +
    +
    +{% endfor %} From dc650f77b5f377bbfab3da66455feb57b195a1da Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 11:34:42 +1300 Subject: [PATCH 0185/1652] add tests for transform_fieldname methods --- rest_framework/tests/test_serializer.py | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..ca876eae4 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1643,3 +1643,38 @@ class SerializerSupportsManyRelationships(TestCase): serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]}) self.assertTrue(serializer.is_valid()) self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]}) + + +class TransformMethodsSerializer(serializers.Serializer): + a = serializers.CharField() + b_renamed = serializers.CharField(source='b') + + def transform_a(self, obj, value): + return value.lower() + + def transform_b_renamed(self, obj, value): + if value is not None: + return 'and ' + value + + +class TestSerializerTransformMethods(TestCase): + def setUp(self): + self.s = TransformMethodsSerializer() + + def test_transform_methods(self): + self.assertEqual( + self.s.to_native({'a': 'GREEN EGGS', 'b': 'HAM'}), + { + 'a': 'green eggs', + 'b_renamed': 'and HAM', + } + ) + + def test_missing_fields(self): + self.assertEqual( + self.s.to_native({'a': 'GREEN EGGS'}), + { + 'a': 'green eggs', + 'b_renamed': None, + } + ) From 42bbf6907e041d6abe773854b9aaa53eded82f4e Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 12:38:42 +1300 Subject: [PATCH 0186/1652] docs: add paragraph on transform_fieldname methods --- docs/api-guide/serializers.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index bbc8d019d..2d3e999f9 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -63,6 +63,21 @@ At this point we've translated the model instance into Python native datatypes. json # '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}' +### Customizing field representation + +Sometimes when serializing objects, you may not want to represent everything exactly the way it is in your model. + +If you need to customize the serialized value of a particular field, you can do this by creating a `transform_` method. For example if you needed to render some markdown from a text field: + + description = serializers.TextField() + description_html = serializers.TextField(source='description', read_only=True) + + def transform_description_html(self, obj, value): + from django.contrib.markup.templatetags.markup import markdown + return markdown(value) + +These methods are essentially the reverse of `validate_` (see *Validation* below.) + ## Deserializing objects Deserialization is similar. First we parse a stream into Python native datatypes... From f6301636fb52dc6e02fd55e1c07c0be0a3b4ebfd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 3 Oct 2013 15:18:38 +0100 Subject: [PATCH 0187/1652] Drop erronous left-over bit of docs. Closes #1147 --- docs/api-guide/serializers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a3cd1d6ab..6b91aa764 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -84,7 +84,6 @@ Deserialization is similar. First we parse a stream into Python native datatype # True serializer.object # - >>> serializer.deserialize('json', stream) When deserializing data, we can either create a new instance, or update an existing instance. From ab4be47379ba49092063f843fd446919534db776 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 3 Oct 2013 17:34:34 +0200 Subject: [PATCH 0188/1652] Fixed code example. --- docs/api-guide/routers.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 730fa876a..f20a695b6 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -42,12 +42,15 @@ The example above would generate the following URL patterns: Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed. For example, given a method like this on the `UserViewSet` class: - from myapp.permissions import IsAdminOrIsSelf + from myapp.permissions import IsAdminOrIsSelf from rest_framework.decorators import detail_route - - @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) - def set_password(self, request, pk=None): + + class UserViewSet(ModelViewSet): ... + + @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf]) + def set_password(self, request, pk=None): + ... The following URL pattern would additionally be generated: From 3e94f4dc709143d577433c164873654e7c0579f8 Mon Sep 17 00:00:00 2001 From: Henry Clifford Date: Fri, 4 Oct 2013 10:49:56 -0400 Subject: [PATCH 0189/1652] support args on get_object_or_404 --- rest_framework/generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7d1bf7945..4f134bce6 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -25,13 +25,13 @@ def strict_positive_int(integer_string, cutoff=None): ret = min(ret, cutoff) return ret -def get_object_or_404(queryset, **filter_kwargs): +def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to raise 404 if the filter_kwargs don't match the required types. """ try: - return _get_object_or_404(queryset, **filter_kwargs) + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) except (TypeError, ValueError): raise Http404 From 3833f1d55f97db33596242d89dbed997221f3b82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 4 Oct 2013 15:55:57 +0100 Subject: [PATCH 0190/1652] Added @hcliff for #1153. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b5dce5040..586bb0f00 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -170,6 +170,7 @@ The following people have helped make REST framework great. * Ben Reilly - [bwreilly] * Tai Lee - [mrmachine] * Markus Kaiserswerth - [mkai] +* Henry Clifford - [hcliff] Many thanks to everyone who's contributed to the project. @@ -376,3 +377,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [bwreilly]: https://github.com/bwreilly [mrmachine]: https://github.com/mrmachine [mkai]: https://github.com/mkai +[hcliff]: https://github.com/hcliff From 48a38386afb8a8619d2f089ebce364c7a0a845b4 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 19:29:25 -0400 Subject: [PATCH 0191/1652] **very minor** typo fix --- 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 22d29285b..e1c0009c3 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -225,7 +225,7 @@ For the moment we won't use any of REST framework's other features, we'll just w We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`. -Edit the `snippet/views.py` file, and add the following. +Edit the `snippets/views.py` file, and add the following. from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt From 864497eebbd39ab3e811c589a44a43176caef1bf Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 23:16:58 -0400 Subject: [PATCH 0192/1652] Be sure to import UserSerializer --- docs/tutorial/4-authentication-and-permissions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 393d879a4..510aa2439 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -75,6 +75,10 @@ We'll also add a couple of views. We'd like to just use read-only views for the class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer + +Make sure to also import the `UserSerializer` class + + from snippets.serializers import UserSerializer Finally we need to add those views into the API, by referencing them from the URL conf. From 9e29c6389529210978d58cee78e437b901f9daa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Oct 2013 17:33:22 +0100 Subject: [PATCH 0193/1652] Ensure read-only fields don't break with current HTML renderer behavior --- rest_framework/fields.py | 3 +++ rest_framework/serializers.py | 2 ++ rest_framework/tests/test_serializer.py | 3 +-- rest_framework/tests/test_serializer_empty.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_serializer_empty.py diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 16344d01b..6b039f6c2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -777,6 +777,7 @@ class IntegerField(WritableField): type_name = 'IntegerField' type_label = 'integer' form_field_class = forms.IntegerField + empty = 0 default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -808,6 +809,7 @@ class FloatField(WritableField): type_name = 'FloatField' type_label = 'float' form_field_class = forms.FloatField + empty = 0 default_error_messages = { 'invalid': _("'%s' value must be a float."), @@ -828,6 +830,7 @@ class DecimalField(WritableField): type_name = 'DecimalField' type_label = 'decimal' form_field_class = forms.DecimalField + empty = Decimal('0') default_error_messages = { 'invalid': _('Enter a number.'), diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bc9f73d11..8e9456885 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -310,6 +310,8 @@ class BaseSerializer(WritableField): ret.fields = self._dict_class() for field_name, field in self.fields.items(): + if field.read_only and obj is None: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8d246b010..d4e5a93f6 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -159,8 +159,7 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None, - 'sub_comment': '' + 'created': None } self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_serializer_empty.py b/rest_framework/tests/test_serializer_empty.py new file mode 100644 index 000000000..30cff3615 --- /dev/null +++ b/rest_framework/tests/test_serializer_empty.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from rest_framework import serializers + + +class EmptySerializerTestCase(TestCase): + def test_empty_serializer(self): + class FooBarSerializer(serializers.Serializer): + foo = serializers.IntegerField() + bar = serializers.SerializerMethodField('get_bar') + + def get_bar(self, obj): + return 'bar' + + serializer = FooBarSerializer() + self.assertEquals(serializer.data, {'foo': 0}) From 7c3769f04b5ec2cd14dcbd7e3601d59092255906 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:31:55 +1300 Subject: [PATCH 0194/1652] fix writing into foreign key with non-null source --- rest_framework/serializers.py | 2 +- .../tests/test_serializer_nested.py | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee1..fa5ac1431 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -403,7 +403,7 @@ class BaseSerializer(WritableField): return # Set the serializer object if it exists - obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj if self.source == '*': diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 71d0e24b5..e454235a1 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -244,3 +244,70 @@ class WritableNestedSerializerObjectTests(TestCase): serializer = self.AlbumSerializer(data=data, many=True) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected_object) + + +class ForeignKeyNestedSerializerUpdateTests(TestCase): + def setUp(self): + class Artist(object): + def __init__(self, name): + self.name = name + + def __eq__(self, other): + return self.name == other.name + + class Album(object): + def __init__(self, name, artist): + self.name, self.artist = name, artist + + def __eq__(self, other): + return self.name == other.name and self.artist == other.artist + + class ArtistSerializer(serializers.Serializer): + name = serializers.CharField() + + def restore_object(self, attrs, instance=None): + if instance: + instance.name = attrs['name'] + else: + instance = Artist(attrs['name']) + return instance + + class AlbumSerializer(serializers.Serializer): + name = serializers.CharField() + by = ArtistSerializer(source='artist') + + def restore_object(self, attrs, instance=None): + if instance: + instance.name = attrs['name'] + instance.artist = attrs['artist'] + else: + instance = Album(attrs['name'], attrs['artist']) + return instance + + self.Artist = Artist + self.Album = Album + self.AlbumSerializer = AlbumSerializer + + def test_create_via_foreign_key_with_source(self): + """ + Check that we can both *create* and *update* into objects across + ForeignKeys that have a `source` specified. + Regression test for # + """ + data = { + 'name': 'Discovery', + 'by': {'name': 'Daft Punk'}, + } + + expected = self.Album(artist=self.Artist('Daft Punk'), name='Discovery') + + # create + serializer = self.AlbumSerializer(data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.object, expected) + + # update + original = self.Album(artist=self.Artist('The Bats'), name='Free All the Monsters') + serializer = self.AlbumSerializer(instance=original, data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.object, expected) From 86ea969e1154de20a53fc5b853e8340508648e98 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:50:07 +1300 Subject: [PATCH 0195/1652] fix ticket link in test docstring --- 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 e454235a1..029f8bffd 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -292,7 +292,7 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): """ Check that we can both *create* and *update* into objects across ForeignKeys that have a `source` specified. - Regression test for # + Regression test for #1170 """ data = { 'name': 'Discovery', From 89ac03af26a63a2126165e8995f7936798ce0450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20L=C3=A1zaro?= Date: Sat, 12 Oct 2013 20:31:33 +0200 Subject: [PATCH 0196/1652] Add missing commas in relations.md --- docs/api-guide/relations.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 5ec4b22fc..b9d96b5e3 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -54,7 +54,7 @@ Would serialize to the following representation. { 'album_name': 'Things We Lost In The Fire', - 'artist': 'Low' + 'artist': 'Low', 'tracks': [ '1: Sunflower', '2: Whitetail', @@ -86,7 +86,7 @@ Would serialize to a representation like this: { 'album_name': 'The Roots', - 'artist': 'Undun' + 'artist': 'Undun', 'tracks': [ 89, 90, @@ -121,7 +121,7 @@ Would serialize to a representation like this: { 'album_name': 'Graceland', - 'artist': 'Paul Simon' + 'artist': 'Paul Simon', 'tracks': [ 'http://www.example.com/api/tracks/45/', 'http://www.example.com/api/tracks/46/', @@ -159,7 +159,7 @@ Would serialize to a representation like this: { 'album_name': 'Dear John', - 'artist': 'Loney Dear' + 'artist': 'Loney Dear', 'tracks': [ 'Airport Surroundings', 'Everything Turns to You', @@ -194,7 +194,7 @@ Would serialize to a representation like this: { 'album_name': 'The Eraser', - 'artist': 'Thom Yorke' + 'artist': 'Thom Yorke', 'track_listing': 'http://www.example.com/api/track_list/12/', } @@ -234,7 +234,7 @@ Would serialize to a nested representation like this: { 'album_name': 'The Grey Album', - 'artist': 'Danger Mouse' + 'artist': 'Danger Mouse', 'tracks': [ {'order': 1, 'title': 'Public Service Announcement'}, {'order': 2, 'title': 'What More Can I Say'}, @@ -271,7 +271,7 @@ This custom field would then serialize to the following representation. { 'album_name': 'Sometimes I Wish We Were an Eagle', - 'artist': 'Bill Callahan' + 'artist': 'Bill Callahan', 'tracks': [ 'Track 1: Jim Cain (04:39)', 'Track 2: Eid Ma Clack Shaw (04:19)', From c6be12f02b5e07e412c8c91b368566a85364b907 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 18:03:52 -0700 Subject: [PATCH 0197/1652] [Fix]: Error with partial=True and validate_ The error occurs when serializer is set with partial=True and a field-level validation is defined on a field, for which there's no corresponding update value in .data --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..0b5ae042a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -255,10 +255,13 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field_name in self._errors: continue + + source = field.source or field_name + if self.partial and source not in attrs: + continue try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: - source = field.source or field_name attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) From e83bc003234418fc6b21b841de216319491bd38d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:03:51 +0100 Subject: [PATCH 0198/1652] Added name of file to edit So reader doesn't have to remember, or check through all the files to find where this code fragment was, mention the file name when it is relevant. --- docs/tutorial/2-requests-and-responses.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 6ff97f37d..ba9eb7237 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -35,7 +35,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r Okay, let's go ahead and start using these new components to write a few views. -We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. +We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. from rest_framework import status from rest_framework.decorators import api_view @@ -64,7 +64,7 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. -Here is the view for an individual snippet. +Here is the view for an individual snippet (still in `views.py`). @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): From cb123e896ed2dca230088296db9663af5a53252d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:08:43 +0100 Subject: [PATCH 0199/1652] Mention name of file to edit To reduce unnecessary cognitive load of the learner, name the file they are putting this code in. --- docs/tutorial/3-class-based-views.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 9fc424fee..67a75d9f4 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -4,7 +4,7 @@ We can also write our API views using class based views, rather than function ba ## Rewriting our API using class based views -We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring. +We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring of `views.py`. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -30,7 +30,7 @@ We'll start by rewriting the root view as a class based view. All this involves return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view. +So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. class SnippetDetail(APIView): """ @@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be That's looking good. Again, it's still pretty similar to the function based view right now. -We'll also need to refactor our URLconf slightly now we're using class based views. +We'll also need to refactor our `urls.py` slightly now we're using class based views. from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns @@ -83,7 +83,7 @@ One of the big wins of using class based views is that it allows us to easily co The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. -Let's take a look at how we can compose our views by using the mixin classes. +Let's take a look at how we can compose our `views.py` by using the mixin classes. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor ## Using generic class based views -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. +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 to trim down `views.py` even more. from snippets.models import Snippet from snippets.serializers import SnippetSerializer From bf6084895263f827a80191fd6ed4eb437b555f9a Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:21:43 +0100 Subject: [PATCH 0200/1652] Using the filenames where relevant Sometimes it's hard to tell which file the code is intended to go in. Now it spells it out. --- docs/tutorial/4-authentication-and-permissions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 510aa2439..ecf92a7b1 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -12,7 +12,7 @@ Currently our API doesn't have any restrictions on who can edit or delete code s We're going to make a couple of changes to our `Snippet` model class. First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code. -Add the following two fields to the model. +Add the following two fields to the `Snippet` model in `models.py`. owner = models.ForeignKey('auth.User', related_name='snippets') highlighted = models.TextField() @@ -52,7 +52,7 @@ You might also want to create a few different users, to use for testing the API. ## Adding endpoints for our User models -Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy: +Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add: from django.contrib.auth.models import User @@ -65,7 +65,7 @@ Now that we've got some users to work with, we'd better add representations of t Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it. -We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. +We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. class UserList(generics.ListAPIView): queryset = User.objects.all() @@ -80,7 +80,7 @@ Make sure to also import the `UserSerializer` class from snippets.serializers import UserSerializer -Finally we need to add those views into the API, by referencing them from the URL conf. +Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), @@ -98,7 +98,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin ## Updating our serializer -Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition: +Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`: owner = serializers.Field(source='owner.username') From d31fd33f4bbd52fa60949b15c2614528991e2c7a Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 8 Oct 2013 16:15:15 +0200 Subject: [PATCH 0201/1652] Allow to customize description so that markup can be accepted if needed. --- rest_framework/templates/rest_framework/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 33be36db8..7ab17dff4 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -110,7 +110,9 @@
    + {% block description %} {{ description }} + {% endblock %}
    {{ request.method }} {{ request.get_full_path }}
    From 8a5fea06f01ed4c5114ec0743516b6e6179c88b4 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:07:50 +0200 Subject: [PATCH 0202/1652] Fix typo YAMLRendererTests --- rest_framework/tests/test_renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index df6f4aa63..78a7dac89 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -328,7 +328,7 @@ if yaml: class YAMLRendererTests(TestCase): """ - Tests specific to the JSON Renderer + Tests specific to the YAML Renderer """ def test_render(self): From b730aec0f46e2b849b3c597bcf1a1bcdc158e415 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:08:24 +0200 Subject: [PATCH 0203/1652] Fix decimal support with YAMLRenderer --- rest_framework/tests/test_renderers.py | 11 +++++++++++ rest_framework/utils/encoders.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 78a7dac89..76299a890 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -354,6 +354,17 @@ if yaml: data = parser.parse(StringIO(content)) self.assertEqual(obj, data) + def test_render_decimal(self): + """ + Test YAML decimal rendering. + """ + renderer = YAMLRenderer() + content = renderer.render({'field': Decimal('111.2')}, 'application/yaml') + self.assertYAMLContains(content, "field: '111.2'") + + def assertYAMLContains(self, content, string): + self.assertTrue(string in content, '%r not in %r' % (string, content)) + class XMLRendererTestCase(TestCase): """ diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 7efd5417b..35ad206b9 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -89,6 +89,9 @@ else: node.flow_style = best_style return node + SafeDumper.add_representer(decimal.Decimal, + SafeDumper.represent_decimal) + SafeDumper.add_representer(SortedDict, yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(DictWithMetadata, From 545ee013e332db0a81e5bd82b76f30b7d2d08b12 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 17 Oct 2013 09:40:06 +0100 Subject: [PATCH 0204/1652] Added @badale, for work on #1179. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 586bb0f00..9a20028cd 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -171,6 +171,7 @@ The following people have helped make REST framework great. * Tai Lee - [mrmachine] * Markus Kaiserswerth - [mkai] * Henry Clifford - [hcliff] +* Thomas Badaud - [badale] Many thanks to everyone who's contributed to the project. @@ -378,3 +379,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mrmachine]: https://github.com/mrmachine [mkai]: https://github.com/mkai [hcliff]: https://github.com/hcliff +[badale]: https://github.com/badale From cc3c16eaa09c7dc63592ae8bf4ee30f1af263be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Reni=C3=A9?= Date: Mon, 14 Oct 2013 16:28:32 +0200 Subject: [PATCH 0205/1652] Fix a docstring to reflect what the method does --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee1..6801e24d4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -912,7 +912,7 @@ class ModelSerializer(Serializer): def save_object(self, obj, **kwargs): """ - Save the deserialized object and return it. + Save the deserialized object. """ if getattr(obj, '_nested_forward_relations', None): # Nested relationships need to be saved before we can save the From daf927ef68ce992055e8b7bc1a07cf03ee67b742 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 17 Oct 2013 12:28:58 -0700 Subject: [PATCH 0206/1652] add @tamakisquare for work on #1111 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9a20028cd..e9c45965d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -172,6 +172,7 @@ The following people have helped make REST framework great. * Markus Kaiserswerth - [mkai] * Henry Clifford - [hcliff] * Thomas Badaud - [badale] +* Colin Huang - [tamakisquare] Many thanks to everyone who's contributed to the project. @@ -380,3 +381,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mkai]: https://github.com/mkai [hcliff]: https://github.com/hcliff [badale]: https://github.com/badale +[tamakisquare]: https://github.com/tamakisquare From 78c8e6de40f89580b9a4cefb6595d52bc1a6afbc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:10:54 +0100 Subject: [PATCH 0207/1652] Update 2-requests-and-responses.md --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index ba9eb7237..7fa4f3e4a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -64,7 +64,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. -Here is the view for an individual snippet (still in `views.py`). +Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): From c3aeb16557f2cbb1c1218b5af7bab646e4958234 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:32:04 +0100 Subject: [PATCH 0208/1652] Update 3-class-based-views.md --- docs/tutorial/3-class-based-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 67a75d9f4..b37bc31bd 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -83,7 +83,7 @@ One of the big wins of using class based views is that it allows us to easily co The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. -Let's take a look at how we can compose our `views.py` by using the mixin classes. +Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor ## Using generic class based views -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 to trim down `views.py` even more. +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 to trim down our `views.py` module even more. from snippets.models import Snippet from snippets.serializers import SnippetSerializer From 735c75abb9e612fa0f1e6bdfa4283610752056d2 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Fri, 18 Oct 2013 21:10:49 -0400 Subject: [PATCH 0209/1652] add test case around ensuring proper field inference for boolean model field types --- rest_framework/tests/models.py | 1 + rest_framework/tests/test_serializer.py | 32 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 1598ecd94..32a726c0b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -70,6 +70,7 @@ class Comment(RESTFrameworkModel): class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) + started = models.NullBooleanField(default=False) done = models.BooleanField(default=False) info = CustomField(default='---', max_length=12) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a4749..a7ea3ac0b 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1720,3 +1720,35 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + +class BoolenFieldTypeTest(TestCase): + ''' + Ensure the various Boolean based model fields are rendered as the proper + field type + + ''' + + def setUp(self): + ''' + Setup an ActionItemSerializer for BooleanTesting + ''' + data = { + 'title': 'b' * 201, + } + self.serializer = ActionItemSerializer(data=data) + + def test_booleanfield_type(self): + ''' + Test that BooleanField is infered from models.BooleanField + ''' + bfield = self.serializer.get_fields()['done'] + self.assertEqual(type(bfield), fields.BooleanField) + + def test_nullbooleanfield_type(self): + ''' + Test that BooleanField is infered from models.NullBooleanField + + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 + ''' + bfield = self.serializer.get_fields()['started'] + self.assertEqual(type(bfield), fields.BooleanField) From 17a00be830a3b8861be5014e94850f7a8f4ecd25 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Fri, 18 Oct 2013 21:13:20 -0400 Subject: [PATCH 0210/1652] This fix results in models.NullBooleanField rendering as a checkbox in the browsable API --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058b..f30f1fd8b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -606,6 +606,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, models.FileField: FileField, models.ImageField: ImageField, } From 63e6a3b4925bf54e80ae63502a0353136e846b31 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Oct 2013 20:43:23 -0700 Subject: [PATCH 0211/1652] paginator should validate page and provide default - use the standard paginator.validate_number method rather strict_postive_int. - support optional paginator method, default_page_number, to get the default page number rather than hard-coding it to 1 - this allows supporting non-integer based pagination which can be an important performance tweak on extermely large datasets or high request loads - relatively thorough unit tests of the changes --- rest_framework/generics.py | 14 +++- rest_framework/tests/test_pagination.py | 88 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4f134bce6..6b42a1d5f 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -145,10 +145,18 @@ class GenericAPIView(views.APIView): allow_empty_first_page=self.allow_empty) page_kwarg = self.kwargs.get(self.page_kwarg) page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) - page = page_kwarg or page_query_param or 1 + page = page_kwarg or page_query_param + if not page: + # we didn't recieve a page + if hasattr(paginator, 'default_page_number'): + # our paginator has a method that will provide a default + page = paginator.default_page_number() + else: + # fall back on the base default value + page = 1 try: - page_number = strict_positive_int(page) - except ValueError: + page_number = paginator.validate_number(page) + except InvalidPage: if page == 'last': page_number = paginator.num_pages else: diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index d6bc7895c..a1118f1ec 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -430,3 +430,91 @@ class TestCustomPaginationSerializer(TestCase): 'objects': ['john', 'paul'] } self.assertEqual(serializer.data, expected) + + +class NonIntegerPage(object): + + def __init__(self, paginator, object_list, prev_token, token, next_token): + self.paginator = paginator + self.object_list = object_list + self.prev_token = prev_token + self.token = token + self.next_token = next_token + + def has_next(self): + return not not self.next_token + + def next_page_number(self): + return self.next_token + + def has_previous(self): + return not not self.prev_token + + def previous_page_number(self): + return self.prev_token + + +class NonIntegerPaginator(object): + + def __init__(self, object_list, per_page): + self.object_list = object_list + self.per_page = per_page + + def count(self): + # pretend like we don't know how many pages we have + return None + + def default_page_token(self): + return None + + def page(self, token=None): + if token: + try: + first = self.object_list.index(token) + except ValueError: + first = 0 + else: + first = 0 + n = len(self.object_list) + last = min(first + self.per_page, n) + prev_token = self.object_list[last - (2 * self.per_page)] if first else None + next_token = self.object_list[last] if last < n else None + return NonIntegerPage(self, self.object_list[first:last], prev_token, token, next_token) + + +class TestNonIntegerPagination(TestCase): + + + def test_custom_pagination_serializer(self): + objects = ['john', 'paul', 'george', 'ringo'] + paginator = NonIntegerPaginator(objects, 2) + + request = APIRequestFactory().get('/foobar') + serializer = CustomPaginationSerializer( + instance=paginator.page(), + context={'request': request} + ) + expected = { + 'links': { + 'next': 'http://testserver/foobar?page={0}'.format(objects[2]), + 'prev': None + }, + 'total_results': None, + 'objects': objects[:2] + } + self.assertEqual(serializer.data, expected) + + request = APIRequestFactory().get('/foobar') + serializer = CustomPaginationSerializer( + instance=paginator.page('george'), + context={'request': request} + ) + expected = { + 'links': { + 'next': None, + 'prev': 'http://testserver/foobar?page={0}'.format(objects[0]), + }, + 'total_results': None, + 'objects': objects[2:] + } + self.assertEqual(serializer.data, expected) From ed9c3258a6f9df6fabb569a65f3eb3363affa523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 21 Oct 2013 10:24:06 +0200 Subject: [PATCH 0212/1652] Remove the detail=None from APIException signature The documentation not match with the implementation. The APIException doesn't have detail parameter in the constructor class, actually doesn't have constructor method at all. --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 0c48783a3..c46d415e4 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -82,7 +82,7 @@ Note that the exception handler will only be called for responses generated by r ## APIException -**Signature:** `APIException(detail=None)` +**Signature:** `APIException()` The **base class** for all exceptions raised inside REST framework. From 70b0798118c8c02903421bca03e0406fe65d737f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:30:01 +0100 Subject: [PATCH 0213/1652] Add invite signup --- docs/template.html | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/template.html b/docs/template.html index a20c81110..749d0afe3 100644 --- a/docs/template.html +++ b/docs/template.html @@ -167,7 +167,32 @@
    + + +
    From 76672787cdba6a4ab8173b51fa099c910556889b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:47:07 +0100 Subject: [PATCH 0214/1652] Added . Closes #1188. --- docs/api-guide/generic-views.md | 3 ++- rest_framework/generics.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index dc0076dff..24fc0bc71 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -65,7 +65,8 @@ The following attributes control the basic view behavior. * `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. * `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method. -* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf. +* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value. +* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`. **Shortcuts**: diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4f134bce6..f46dea762 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -54,6 +54,7 @@ class GenericAPIView(views.APIView): # If you want to use object lookups other than pk, set this attribute. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' + lookup_url_kwarg = None # Pagination settings paginate_by = api_settings.PAGINATE_BY @@ -278,9 +279,11 @@ class GenericAPIView(views.APIView): pass # Deprecation warning # Perform the lookup filtering. + # Note that `pk` and `slug` are deprecated styles of lookup filtering. + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - lookup = self.kwargs.get(self.lookup_field, None) if lookup is not None: filter_kwargs = {self.lookup_field: lookup} From 216ac8a5c1ba39bf24e4e91b6fac7e0ac1dee7e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 17:19:28 +0100 Subject: [PATCH 0215/1652] Use lookup_url_kwarg in presave if required --- rest_framework/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 426865ff9..4606c78b6 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -158,7 +158,8 @@ class UpdateModelMixin(object): Set any attributes on the object that are implicit in the request. """ # pk and/or slug attributes are implicit in the URL. - lookup = self.kwargs.get(self.lookup_field, None) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) slug_field = slug and self.slug_field or None From f0a129dcda3d671b88b5049d9ddaec53a4b32faf Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:23:06 -0700 Subject: [PATCH 0216/1652] retract the default page stuff. better way comming in a seperate pr --- rest_framework/generics.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 6b42a1d5f..4015ab20a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -145,15 +145,7 @@ class GenericAPIView(views.APIView): allow_empty_first_page=self.allow_empty) page_kwarg = self.kwargs.get(self.page_kwarg) page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) - page = page_kwarg or page_query_param - if not page: - # we didn't recieve a page - if hasattr(paginator, 'default_page_number'): - # our paginator has a method that will provide a default - page = paginator.default_page_number() - else: - # fall back on the base default value - page = 1 + page = page_kwarg or page_query_param or 1 try: page_number = paginator.validate_number(page) except InvalidPage: From c36122a7ba2cdc69f94f5732f26428329be54200 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:26:21 -0700 Subject: [PATCH 0217/1652] remove stray func from test --- rest_framework/tests/test_pagination.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index a1118f1ec..cadb515fa 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -464,9 +464,6 @@ class NonIntegerPaginator(object): # pretend like we don't know how many pages we have return None - def default_page_token(self): - return None - def page(self, token=None): if token: try: From fa87fac61b87858e80788fc233591fa11dbc18e7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 10:21:06 +0100 Subject: [PATCH 0218/1652] Added @ross for work on #1187. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e9c45965d..cd3b37107 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -173,6 +173,7 @@ The following people have helped make REST framework great. * Henry Clifford - [hcliff] * Thomas Badaud - [badale] * Colin Huang - [tamakisquare] +* Ross McFarland - [ross] Many thanks to everyone who's contributed to the project. @@ -382,3 +383,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [hcliff]: https://github.com/hcliff [badale]: https://github.com/badale [tamakisquare]: https://github.com/tamakisquare +[ross]: https://github.com/ross From 25c9d552c05527f4b8b257d59cd7be39005f3668 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:11:14 +0200 Subject: [PATCH 0219/1652] Explained a bit more about django-filter implementation. Well, I spent some time trying to gues how djang-filter works, and if this changes would be introduced, I would have saved this time. --- docs/api-guide/filtering.md | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 784aa585e..bcb0bb413 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -165,8 +165,8 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha from rest_framework import generics class ProductFilter(django_filters.FilterSet): - min_price = django_filters.NumberFilter(lookup_type='gte') - max_price = django_filters.NumberFilter(lookup_type='lte') + min_price = django_filters.NumberFilter(name="price", lookup_type='gte') + max_price = django_filters.NumberFilter(name="price", lookup_type='lte') class Meta: model = Product fields = ['category', 'in_stock', 'min_price', 'max_price'] @@ -176,12 +176,51 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha serializer_class = ProductSerializer filter_class = ProductFilter + Which will allow you to make requests such as: http://example.com/api/products?category=clothing&max_price=10.00 For more details on using filter sets see the [django-filter documentation][django-filter-docs]. +You can also span relationships using `django-filter`, let's assume that each +product has foreign key to `Manufacturer` model, so we create filter that +filters using `Manufacturer` name. For example: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer__name`] + +This enables us to make queries like: + + http://example.com/api/products?manufacturer__name=foo + +This is nice, but it shows underlying model structure in REST API, which may +be undesired, but you can use: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + + manufacturer = django_filters.CharFilter(name="manufacturer__name") + + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer`] + +And now you can execute: + + http://example.com/api/products?manufacturer=foo + --- **Hints & Tips** From cc9c7cd8a479b7fa76a66b8669e4a62fd78be867 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:15:48 +0200 Subject: [PATCH 0220/1652] Small documentation fix --- docs/api-guide/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index bcb0bb413..a0132ffcb 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -181,8 +181,6 @@ Which will allow you to make requests such as: http://example.com/api/products?category=clothing&max_price=10.00 -For more details on using filter sets see the [django-filter documentation][django-filter-docs]. - You can also span relationships using `django-filter`, let's assume that each product has foreign key to `Manufacturer` model, so we create filter that filters using `Manufacturer` name. For example: @@ -220,6 +218,8 @@ be undesired, but you can use: And now you can execute: http://example.com/api/products?manufacturer=foo + +For more details on using filter sets see the [django-filter documentation][django-filter-docs]. --- From f92d8bd9721d788e3017c16fb285189c88112a46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 12:21:26 +0100 Subject: [PATCH 0221/1652] Added @jbzdak, for the nice docs improvements in #1191. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index cd3b37107..bcf77b032 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -174,6 +174,7 @@ The following people have helped make REST framework great. * Thomas Badaud - [badale] * Colin Huang - [tamakisquare] * Ross McFarland - [ross] +* Jacek Bzdak - [jbzdak] Many thanks to everyone who's contributed to the project. @@ -384,3 +385,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [badale]: https://github.com/badale [tamakisquare]: https://github.com/tamakisquare [ross]: https://github.com/ross +[jbzdak]: https://github.com/jbzdak From 6b3500b684a43fb67c42231859fa27cf5193298a Mon Sep 17 00:00:00 2001 From: alexanderlukanin13 Date: Thu, 24 Oct 2013 17:52:52 +0600 Subject: [PATCH 0222/1652] Fixed UnicodeEncodeError when POST JSON via web interface; added test --- rest_framework/request.py | 2 +- rest_framework/tests/test_request.py | 32 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 977d4d965..b883d0d4f 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -334,7 +334,7 @@ class Request(object): self._CONTENT_PARAM in self._data and self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING)) + self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._data, self._files = (Empty, Empty) def _parse(self): diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 969d8024a..a60e7615e 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware +from django.core.handlers.wsgi import WSGIRequest from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -15,12 +16,13 @@ from rest_framework.parsers import ( MultiPartParser, JSONParser ) -from rest_framework.request import Request +from rest_framework.request import Request, Empty from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework.compat import six +from io import BytesIO import json @@ -146,6 +148,34 @@ class TestContentParsing(TestCase): request.parsers = (JSONParser(), ) self.assertEqual(request.DATA, json_data) + def test_form_POST_unicode(self): + """ + JSON POST via default web interface with unicode data + """ + # Note: environ and other variables here have simplified content compared to real Request + CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D' + environ = { + 'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', + 'CONTENT_LENGTH': len(CONTENT), + 'wsgi.input': BytesIO(CONTENT), + } + wsgi_request = WSGIRequest(environ=environ) + wsgi_request._load_post_and_files() + parsers = (JSONParser(), FormParser(), MultiPartParser()) + parser_context = { + 'encoding': 'utf-8', + 'kwargs': {}, + 'args': (), + } + request = Request(wsgi_request, parsers=parsers, parser_context=parser_context) + method = request.method + self.assertEqual(method, 'POST') + self.assertEqual(request._content_type, 'application/json') + self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}') + self.assertEqual(request._data, Empty) + self.assertEqual(request._files, Empty) + # def test_accessing_post_after_data_form(self): # """ # Ensures request.POST can be accessed after request.DATA in From 63023078856e78fa043df96378137fd7acc2c1de Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:45:16 +0100 Subject: [PATCH 0223/1652] Update comment in `get_parser_context`. --- rest_framework/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 853e64614..e863af6dd 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -154,8 +154,8 @@ 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. + # Note: Additionally `request` and `encoding` will also be added + # to the context by the Request object. return { 'view': self, 'args': getattr(self, 'args', ()), From 4d894fd39ec5670e72756c26908468fd743354c6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:50:05 +0100 Subject: [PATCH 0224/1652] Added @alexanderlukanin13 for fix #1198. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bcf77b032..028dfffc8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -175,6 +175,7 @@ The following people have helped make REST framework great. * Colin Huang - [tamakisquare] * Ross McFarland - [ross] * Jacek Bzdak - [jbzdak] +* Alexander Lukanin - [alexanderlukanin13] Many thanks to everyone who's contributed to the project. @@ -386,3 +387,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [tamakisquare]: https://github.com/tamakisquare [ross]: https://github.com/ross [jbzdak]: https://github.com/jbzdak +[alexanderlukanin13]: https://github.com/alexanderlukanin13 From c92af2b1dd25acebe440f667ede3bad4906b9b28 Mon Sep 17 00:00:00 2001 From: Yamila Date: Thu, 24 Oct 2013 15:56:53 +0200 Subject: [PATCH 0225/1652] Typo on generic-views.md --- 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 24fc0bc71..9681c8c72 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -125,7 +125,7 @@ Note that if your API doesn't include any object level permissions, you may opti Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. -May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of uesr. +May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users. For example: From 82e9ddcf7a5cb5fda81e84326bb6f8181ccdffab Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 24 Oct 2013 15:39:02 +0200 Subject: [PATCH 0226/1652] Added get_filter_backends method --- docs/api-guide/generic-views.md | 20 ++++++++++++++++++-- rest_framework/generics.py | 12 +++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 24fc0bc71..8fedcdaa0 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -121,6 +121,22 @@ For example: Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. +#### `get_filter_backends(self)` + +Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute. + +May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria. + +For example: + + def get_filter_backends(self): + if "geo_route" in self.request.QUERY_PARAMS: + return (GeoRouteFilter, CategoryFilter) + elif "geo_point" in self.request.QUERY_PARAMS: + return (GeoPointFilter, CategoryFilter) + + return (CategoryFilter,) + #### `get_serializer_class(self)` Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. @@ -328,7 +344,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap serializer_class = UserSerializer lookup_fields = ('account', 'username') -Using custom mixins is a good option if you have custom behavior that needs to be used +Using custom mixins is a good option if you have custom behavior that needs to be used ## Creating custom base classes @@ -337,7 +353,7 @@ If you are using a mixin across multiple views, you can take this a step further class BaseRetrieveView(MultipleFieldLookupMixin, generics.RetrieveAPIView): pass - + class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView): pass diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 6d204cf5f..7cb80a84c 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -175,6 +175,14 @@ class GenericAPIView(views.APIView): method if you want to apply the configured filtering backend to the default queryset. """ + for backend in self.get_filter_backends(): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def get_filter_backends(self): + """ + Returns the list of filter backends that this view requires. + """ filter_backends = self.filter_backends or [] if not filter_backends and self.filter_backend: warnings.warn( @@ -185,10 +193,8 @@ class GenericAPIView(views.APIView): PendingDeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] + return filter_backends - for backend in filter_backends: - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset ######################## ### The following methods provide default implementations From 2ddf7869e3ce57d056c5b0546154c7bbe524cc09 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 15:29:40 +0100 Subject: [PATCH 0227/1652] Added @yamila-moreno for work on #1199. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 028dfffc8..e6de1dbe3 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -176,6 +176,7 @@ The following people have helped make REST framework great. * Ross McFarland - [ross] * Jacek Bzdak - [jbzdak] * Alexander Lukanin - [alexanderlukanin13] +* Yamila Moreno - [yamila-moreno] Many thanks to everyone who's contributed to the project. @@ -388,3 +389,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ross]: https://github.com/ross [jbzdak]: https://github.com/jbzdak [alexanderlukanin13]: https://github.com/alexanderlukanin13 +[yamila-moreno]: https://github.com/yamila-moreno From 7a87893b962d155f5e9d06bd0001a7f994419a2d Mon Sep 17 00:00:00 2001 From: Chris Guethle Date: Thu, 24 Oct 2013 09:40:43 -0500 Subject: [PATCH 0228/1652] reworked APIException, pushing some of the status_code and detail management up. Also, makes the APIException useful in isolation (defaults to status code 500) --- rest_framework/exceptions.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 425a72149..2bd21de3b 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -13,47 +13,40 @@ class APIException(Exception): Base class for REST framework exceptions. Subclasses should provide `.status_code` and `.detail` properties. """ - pass + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + default_detail = "" + + def __init__(self, detail=None, status_code=None): + self.status_code = status_code or self.status_code + self.detail = detail or self.default_detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = 'Malformed request.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Incorrect authentication credentials.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = 'Authentication credentials were not provided.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = 'You do not have permission to perform this action.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail - class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED default_detail = "Method '%s' not allowed." def __init__(self, method, detail=None): - self.detail = (detail or self.default_detail) % method + super(MethodNotAllowed, self).__init__((detail or self.default_detail) % method) class NotAcceptable(APIException): @@ -61,7 +54,7 @@ class NotAcceptable(APIException): default_detail = "Could not satisfy the request's Accept header" def __init__(self, detail=None, available_renderers=None): - self.detail = detail or self.default_detail + super(NotAcceptable, self).__init__(detail) self.available_renderers = available_renderers @@ -70,19 +63,19 @@ class UnsupportedMediaType(APIException): default_detail = "Unsupported media type '%s' in request." def __init__(self, media_type, detail=None): - self.detail = (detail or self.default_detail) % media_type + super(UnsupportedMediaType, self).__init__((detail or self.default_detail) % media_type) class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS - default_detail = "Request was throttled." + default_detail = 'Request was throttled.' extra_detail = "Expected available in %d second%s." def __init__(self, wait=None, detail=None): + super(Throttled, self).__init__(detail) + import math self.wait = wait and math.ceil(wait) or None if wait is not None: - format = detail or self.default_detail + self.extra_detail - self.detail = format % (self.wait, self.wait != 1 and 's' or '') - else: - self.detail = detail or self.default_detail + format = self.detail + self.extra_detail + self.detail = format % (self.wait, self.wait != 1 and 's' or '') \ No newline at end of file From be55a3c5c7f0c573129903e29b7c9dfc02dd5958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Rozto=C4=8Dil?= Date: Thu, 24 Oct 2013 17:53:02 +0200 Subject: [PATCH 0229/1652] Removed commented-out credits from footer to make django-debug-toolbar work. The comment, although valid, caused that the Django debug toolbar's injected HTML was partially commented-out and thus the toolbar didn't work as expected. --- rest_framework/templates/rest_framework/base.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 7ab17dff4..495163b64 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -221,9 +221,6 @@
    {% block footer %} - {% endblock %} {% block script %} From 7d5499bcac379a506f78fc0065ebe31c8d01240f Mon Sep 17 00:00:00 2001 From: Kit Randel Date: Fri, 25 Oct 2013 11:45:33 +1300 Subject: [PATCH 0230/1652] In the API test client example 'data' was not defined. There's also no need to define 'expected' as we can just test against the dict. --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 35c1f7660..4a8a91682 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -205,10 +205,10 @@ You can use any of REST framework's test case classes as you would for the regul Ensure we can create a new account object. """ url = reverse('account-list') - expected = {'name': 'DabApps'} + data = {'name': 'DabApps'} response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, expected) + self.assertEqual(response.data, data) --- From 458af921f36cec48ff6c27f4824d69f1aafcd18e Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Tue, 29 Oct 2013 15:10:06 -0500 Subject: [PATCH 0231/1652] minor typo --- rest_framework/viewsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index d91323f22..7eb29f99b 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views. user_detail = UserViewSet.as_view({'get': 'retrieve'}) Typically, rather than instantiate views from viewsets directly, you'll -regsiter the viewset with a router and let the URL conf be determined +register the viewset with a router and let the URL conf be determined automatically. router = DefaultRouter() From f72488d60915f2f77234bc75ccfd604cc6a4143f Mon Sep 17 00:00:00 2001 From: erkarl Date: Thu, 31 Oct 2013 03:47:23 +0200 Subject: [PATCH 0232/1652] Updated OAuth2 authentication docs. --- docs/api-guide/authentication.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 7caeac1e2..1a1c68b84 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -265,6 +265,12 @@ This authentication class depends on the optional [django-oauth2-provider][djang 'provider.oauth2', ) +Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting: + + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.OAuth2Authentication', + ), + You must also include the following in your root `urls.py` module: url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), From e33435d0da0dba13fae39070b3d87ad8af47862f Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Thu, 31 Oct 2013 15:03:50 -0700 Subject: [PATCH 0233/1652] Fixed exception handling with YAML and XML parsers. --- rest_framework/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 98fc03417..f1b3e38d4 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -83,7 +83,7 @@ class YAMLParser(BaseParser): data = stream.read().decode(encoding) return yaml.safe_load(data) except (ValueError, yaml.parser.ParserError) as exc: - raise ParseError('YAML parse error - %s' % six.u(exc)) + raise ParseError('YAML parse error - %s' % six.text_type(exc)) class FormParser(BaseParser): @@ -153,7 +153,7 @@ class XMLParser(BaseParser): try: tree = etree.parse(stream, parser=parser, forbid_dtd=True) except (etree.ParseError, ValueError) as exc: - raise ParseError('XML parse error - %s' % six.u(exc)) + raise ParseError('XML parse error - %s' % six.text_type(exc)) data = self._xml_convert(tree.getroot()) return data From 5e0538fcda75eed82ed183c7c4dcfcda5ad35d5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 2 Nov 2013 09:15:35 +0000 Subject: [PATCH 0234/1652] Added @robhudson for work on #1211. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6de1dbe3..9751850ba 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -177,6 +177,7 @@ The following people have helped make REST framework great. * Jacek Bzdak - [jbzdak] * Alexander Lukanin - [alexanderlukanin13] * Yamila Moreno - [yamila-moreno] +* Rob Hudson - [robhudson] Many thanks to everyone who's contributed to the project. @@ -390,3 +391,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [jbzdak]: https://github.com/jbzdak [alexanderlukanin13]: https://github.com/alexanderlukanin13 [yamila-moreno]: https://github.com/yamila-moreno +[robhudson]: https://github.com/robhudson From 53258908210b1eabd0ee204653a589d6579ac772 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 5 Nov 2013 17:21:18 +0100 Subject: [PATCH 0235/1652] Improve handling of 'empty' values for ChoiceField The empty value defaults back to '' (for backwards-compatibility) but is changed automatically to None for ModelSerializers if the `null` property is set on the db field. --- rest_framework/fields.py | 8 ++-- rest_framework/serializers.py | 2 + rest_framework/tests/test_fields.py | 74 +++++++++++++++++++++++------ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e23fc0010..6c07dbb3b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -497,6 +497,7 @@ class ChoiceField(WritableField): } def __init__(self, choices=(), *args, **kwargs): + self.empty = kwargs.pop('empty', '') super(ChoiceField, self).__init__(*args, **kwargs) self.choices = choices if not self.required: @@ -537,9 +538,10 @@ class ChoiceField(WritableField): return False def from_native(self, value): - if value in validators.EMPTY_VALUES: - return None - return super(ChoiceField, self).from_native(value) + value = super(ChoiceField, self).from_native(value) + if value == self.empty or value in validators.EMPTY_VALUES: + return self.empty + return value class EmailField(CharField): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058b..5240dbf6b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -794,6 +794,8 @@ class ModelSerializer(Serializer): # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices kwargs['choices'] = model_field.flatchoices + if model_field.null: + kwargs['empty'] = None return ChoiceField(**kwargs) # put this below the ChoiceField because min_value isn't a valid initializer diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 34fbab9c9..333476bac 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer): model = TimeFieldModel +SAMPLE_CHOICES = [ + ('red', 'Red'), + ('green', 'Green'), + ('blue', 'Blue'), +] + + +class ChoiceFieldModel(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255) + + +class ChoiceFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModel + + +class ChoiceFieldModelWithNull(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255) + + +class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModelWithNull + + class BasicFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ @@ -667,34 +692,53 @@ class ChoiceFieldTests(TestCase): """ Tests for the ChoiceField options generator """ - - SAMPLE_CHOICES = [ - ('red', 'Red'), - ('green', 'Green'), - ('blue', 'Blue'), - ] - def test_choices_required(self): """ Make sure proper choices are rendered if field is required """ - f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, SAMPLE_CHOICES) def test_choices_not_required(self): """ Make sure proper choices (plus blank) are rendered if the field isn't required """ - f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) + + def test_invalid_choice_model(self): + s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors, {'choice': [u'Select a valid choice. wrong_value is not one of the available choices.']}) + self.assertEqual(s.data['choice'], '') + + def test_empty_choice_model(self): + """ + Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + """ + s = ChoiceFieldModelSerializer(data={'choice' : ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], '') + + s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], None) def test_from_native_empty(self): """ - Make sure from_native() returns None on empty param. + Make sure from_native() returns an empty string on empty param by default. """ - f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES) - result = f.from_native('') - self.assertEqual(result, None) + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.from_native(''), '') + self.assertEqual(f.from_native(None), '') + + def test_from_native_empty_override(self): + """ + Make sure you can override from_native() behavior regarding empty values. + """ + f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None) + self.assertEqual(f.from_native(''), None) + self.assertEqual(f.from_native(None), None) class EmailFieldTests(TestCase): From 5829eb7a5b0d45fe668d7ce1ad394a7b5966c70d Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Wed, 6 Nov 2013 12:51:40 +0100 Subject: [PATCH 0236/1652] Drop u'' prefix for python 3.x compatibility --- rest_framework/tests/test_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 333476bac..ab2cceacd 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -709,7 +709,7 @@ class ChoiceFieldTests(TestCase): def test_invalid_choice_model(self): s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) self.assertFalse(s.is_valid()) - self.assertEqual(s.errors, {'choice': [u'Select a valid choice. wrong_value is not one of the available choices.']}) + self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): From 1296753b5c55de19e057eb6fde73da2b41f7032b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Reni=C3=A9?= Date: Fri, 8 Nov 2013 11:58:14 +0100 Subject: [PATCH 0237/1652] Updated versions in tox and travis config --- .travis.yml | 28 ++++++++++++++-------------- tox.ini | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index d12479e9e..386c1d644 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,20 +7,20 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/" - - DJANGO="django==1.5.1 --use-mirrors" - - DJANGO="django==1.4.5 --use-mirrors" - - DJANGO="django==1.3.7 --use-mirrors" + - DJANGO="django==1.6" + - DJANGO="django==1.5.5" + - DJANGO="django==1.4.10" + - DJANGO="django==1.3.7" install: - pip install $DJANGO - pip install defusedxml==0.3 - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" - - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" + - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" + - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6; fi" - export PYTHONPATH=. script: @@ -29,11 +29,11 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO="django==1.4.5 --use-mirrors" + env: DJANGO="django==1.4.5" - python: "3.2" - env: DJANGO="django==1.3.7 --use-mirrors" + env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.5 --use-mirrors" + env: DJANGO="django==1.4.5" - python: "3.3" - env: DJANGO="django==1.3.7 --use-mirrors" + env: DJANGO="django==1.3.7" diff --git a/tox.ini b/tox.ini index 6e3b8e0a8..1fa0a958f 100644 --- a/tox.ini +++ b/tox.ini @@ -7,19 +7,19 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.6] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 [testenv:py3.2-django1.6] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 [testenv:py2.7-django1.6] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -29,7 +29,7 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ [testenv:py2.6-django1.6] basepython = python2.6 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -39,19 +39,19 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ [testenv:py3.3-django1.5] basepython = python3.3 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 [testenv:py3.2-django1.5] basepython = python3.2 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 [testenv:py2.7-django1.5] basepython = python2.7 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -61,7 +61,7 @@ deps = django==1.5 [testenv:py2.6-django1.5] basepython = python2.6 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -71,7 +71,7 @@ deps = django==1.5 [testenv:py2.7-django1.4] basepython = python2.7 -deps = django==1.4.3 +deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -81,7 +81,7 @@ deps = django==1.4.3 [testenv:py2.6-django1.4] basepython = python2.6 -deps = django==1.4.3 +deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 From d4a50429b098656e7a0855c6acf12f0aa4bc434f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 8 Nov 2013 13:12:40 +0100 Subject: [PATCH 0238/1652] Fixed a regression with ValidationError under Django 1.6 --- rest_framework/serializers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5240dbf6b..7cdb55c8c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -42,6 +42,7 @@ def pretty_name(name): class RelationsList(list): _deleted = [] + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -56,9 +57,13 @@ class NestedValidationError(ValidationError): def __init__(self, message): if isinstance(message, dict): - self.messages = [message] + self._messages = [message] else: - self.messages = message + self._messages = message + + @property + def messages(self): + return self._messages class DictWithMetadata(dict): From b7b57adee2cc5a785a5df492424969c8ba311aa8 Mon Sep 17 00:00:00 2001 From: Ben Pietravalle Date: Fri, 8 Nov 2013 13:19:40 +0000 Subject: [PATCH 0239/1652] Fix object creation with reverse M2M when related_name unspecified It seems that field.related_query_name() does not return the related_name for reverse M2M relations when related_name is not explicitly set in the M2M field definition. So, change to use obj.get_accessor_name(), where obj is an instance of RelatedObject, as are returned by a model's _meta.get_all_related_many_to_many_objects(), or as in the tuples returned by _meta.get_all_m2m_objects_with_model(). --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5240dbf6b..244fbe634 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -873,7 +873,7 @@ class ModelSerializer(Serializer): # Reverse m2m relations for (obj, model) in meta.get_all_related_m2m_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: m2m_data[field_name] = attrs.pop(field_name) From f2ea5780d2d6e783019bceb73c79bb9445e99e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Reni=C3=A9?= Date: Fri, 8 Nov 2013 14:58:36 +0100 Subject: [PATCH 0240/1652] Exclude 1.4 on python 3 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 386c1d644..bcf1bae0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,11 +29,11 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO="django==1.4.5" + env: DJANGO="django==1.4.10" - python: "3.2" env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.5" + env: DJANGO="django==1.4.10" - python: "3.3" env: DJANGO="django==1.3.7" From fd2c291c4d9243937a31e0e6f523016067824b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dog=CC=86an=20C=CC=A7ec=CC=A7en?= Date: Mon, 11 Nov 2013 11:54:30 +0200 Subject: [PATCH 0241/1652] Typo on api-guide/fields.md and serializers.py --- docs/api-guide/fields.md | 4 ++-- rest_framework/serializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 962c49e2a..4272c9a7a 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -299,9 +299,9 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. # Custom fields -If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects. +If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. -The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation. +The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation. ## Examples diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 160954527..163abf4f0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -6,8 +6,8 @@ form encoded input. Serialization in REST framework is a two-phase process: 1. Serializers marshal between complex types like model instances, and -python primatives. -2. The process of marshalling between python primatives and request and +python primitives. +2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ from __future__ import unicode_literals From 52ac2199a8b332f7a485d5c22b1a53633b4be9dd Mon Sep 17 00:00:00 2001 From: Jacob Haslehurst Date: Mon, 11 Nov 2013 22:24:37 +1100 Subject: [PATCH 0242/1652] Added drf-ujson-renderer to renderers docs drf-ujson-renderer is a third party renderer that implements JSON renderering using UltraJSON --- docs/api-guide/renderers.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 657377d92..1f286ef10 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -409,6 +409,10 @@ The following third party packages are also available. Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework. +## UltraJSON + +[UltraJSON][ultrajson] is a blazing-fast C JSON encoder which can give 2-10x performance increases on typical workloads. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. + [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -426,3 +430,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily [mjumbewu]: https://github.com/mjumbewu [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv +[ultrajson]: https://github.com/esnme/ultrajson +[hzy]: https://github.com/hzy +[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer From d1dc68d7550e90ba56a3122f8de1f38bb5aa1e3a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2013 23:40:07 +0000 Subject: [PATCH 0243/1652] Add queryset aggregates to allowed fields in OrderingFilter --- rest_framework/filters.py | 1 + rest_framework/tests/test_filters.py | 39 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index e287a1683..5c6a187c4 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -124,6 +124,7 @@ class OrderingFilter(BaseFilterBackend): def remove_invalid_fields(self, queryset, ordering): field_names = [field.name for field in queryset.model._meta.fields] + field_names += queryset.query.aggregates.keys() return [term for term in ordering if term.lstrip('-') in field_names] def filter_queryset(self, request, queryset, view): diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 379db29d8..614f45cc7 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -363,6 +363,12 @@ class OrdringFilterModel(models.Model): text = models.CharField(max_length=100) +class OrderingFilterRelatedModel(models.Model): + related_object = models.ForeignKey(OrdringFilterModel, + related_name="relateds") + + + class OrderingFilterTests(TestCase): def setUp(self): # Sequence of title/text is: @@ -472,3 +478,36 @@ class OrderingFilterTests(TestCase): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] ) + + def test_ordering_by_aggregate_field(self): + # create some related models to aggregate order by + num_objs = [2, 5, 3] + for obj, num_relateds in zip(OrdringFilterModel.objects.all(), + num_objs): + for _ in range(num_relateds): + new_related = OrderingFilterRelatedModel( + related_object=obj + ) + new_related.save() + + class OrderingListView(generics.ListAPIView): + model = OrdringFilterModel + filter_backends = (filters.OrderingFilter,) + ordering = 'title' + queryset = OrdringFilterModel.objects.all().annotate( + models.Count("relateds")) + + view = OrderingListView.as_view() + request = factory.get('?ordering=relateds__count') + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + ] + ) + + + From f4e610248b94f9a7708ec564fc545ce41561e6c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2013 23:46:01 +0000 Subject: [PATCH 0244/1652] Bump version --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2bd2991ba..de82fef51 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.8' +__version__ = '2.3.9' VERSION = __version__ # synonym From e29942948f320088fbbf9a08cfd8579e0ef2e26f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2013 12:06:59 +0000 Subject: [PATCH 0245/1652] Undo version bump --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index de82fef51..2bd2991ba 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.9' +__version__ = '2.3.8' VERSION = __version__ # synonym From 72d72e4d3e497000bab8019151972b0e71f6957a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2013 14:32:37 +0000 Subject: [PATCH 0246/1652] Add Alex Good to credits (Whoop!) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9751850ba..e6c9c034f 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -178,6 +178,7 @@ The following people have helped make REST framework great. * Alexander Lukanin - [alexanderlukanin13] * Yamila Moreno - [yamila-moreno] * Rob Hudson - [robhudson] +* Alex Good - [alexjg] Many thanks to everyone who's contributed to the project. @@ -392,3 +393,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexanderlukanin13]: https://github.com/alexanderlukanin13 [yamila-moreno]: https://github.com/yamila-moreno [robhudson]: https://github.com/robhudson +[alexjg]: https://github.com/alexjg From 6be62bc1d77f77978847584c358c7c00c34c0992 Mon Sep 17 00:00:00 2001 From: jgomezb Date: Thu, 14 Nov 2013 09:22:07 +0100 Subject: [PATCH 0247/1652] Update urlpatterns.py Allow numbers in format extension. --- rest_framework/urlpatterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index d9143bb4c..0ff137b00 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): allowed_pattern = '(%s)' % '|'.join(allowed) suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern) else: - suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg + suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) From 5239362951956f130a1ef91d12d6b7d680220104 Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Thu, 14 Nov 2013 18:02:07 -0500 Subject: [PATCH 0248/1652] pass oauth_timestamp to oauth_provider --- rest_framework/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cf001a24d..bca542ebb 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -281,7 +281,8 @@ class OAuthAuthentication(BaseAuthentication): """ Checks nonce of request, and return True if valid. """ - return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) + return oauth_provider_store.check_nonce(request, oauth_request, + oauth_request['oauth_nonce'], oauth_request['oauth_timestamp']) class OAuth2Authentication(BaseAuthentication): From 1d1b2f765cbf3a56c677c44eee99260c8979a0a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 13:55:02 +0000 Subject: [PATCH 0249/1652] Version 2.3.9 --- docs/topics/release-notes.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3df8869a2..6c1447d53 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,10 +40,21 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.9 +**Date**: 15th November 2013 + +* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`. +* Include errors in HTML forms in browsable API. * Added JSON renderer support for numpy scalars. +* Added `transform_` hooks on serializers for easily modifying field output. * Added `get_context` hook in `BrowsableAPIRenderer`. +* Allow serializers to be passed `files` but no `data`. +* `HTMLFormRenderer` now renders serializers directly to HTML without needing to create an intermediate form object. +* Added `get_filter_backends` hook. +* Added queryset aggregates to allowed fields in `OrderingFilter`. +* Bugfix: Fix decimal suppoprt with `YAMLRenderer`. +* Bugfix: Fix submission of unicode in browsable API through raw data form. ### 2.3.8 From 7a0e2ed6f6ce2f3b33af2c228f1895273eb9bc73 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 13:55:36 +0000 Subject: [PATCH 0250/1652] Version 2.3.9 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2bd2991ba..de82fef51 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.8' +__version__ = '2.3.9' VERSION = __version__ # synonym From 128bda5712ef041514c5e2feadef0ad248f33f54 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 15:24:32 +0000 Subject: [PATCH 0251/1652] Use less specfic language in UltaJSON notes --- docs/api-guide/renderers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 1f286ef10..858e2f072 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -411,7 +411,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## UltraJSON -[UltraJSON][ultrajson] is a blazing-fast C JSON encoder which can give 2-10x performance increases on typical workloads. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. +[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md From 4807e00bffc0116630af42b86049f197f8d4fc99 Mon Sep 17 00:00:00 2001 From: George Hickman Date: Fri, 15 Nov 2013 15:49:53 +0000 Subject: [PATCH 0252/1652] Set up wheel distribution support --- setup.cfg | 2 ++ setup.py | 1 + 2 files changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..5e4090017 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = 1 diff --git a/setup.py b/setup.py index adf083cbf..96fbc4f41 100755 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ version = get_version('rest_framework') if sys.argv[-1] == 'publish': os.system("python setup.py sdist upload") + os.system("python setup.py bdist_wheel upload") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") From 4b947a6a2af83592d1542d8984b9e5013efd0b1e Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Fri, 15 Nov 2013 12:18:22 -0500 Subject: [PATCH 0253/1652] update requirements for django-oauth-plus to 2.2.1 --- .travis.yml | 2 +- tox.ini | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index bcf1bae0e..18fe66abd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - pip install $DJANGO - pip install defusedxml==0.3 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.1; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" diff --git a/tox.ini b/tox.ini index 1fa0a958f..77766d20b 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ basepython = python2.7 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 @@ -32,7 +32,7 @@ basepython = python2.6 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 @@ -54,7 +54,7 @@ basepython = python2.7 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -64,7 +64,7 @@ basepython = python2.6 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -74,7 +74,7 @@ basepython = python2.7 deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -84,7 +84,7 @@ basepython = python2.6 deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -94,7 +94,7 @@ basepython = python2.7 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 @@ -104,7 +104,7 @@ basepython = python2.6 deps = django==1.3.5 django-filter==0.5.4 defusedxml==0.3 - django-oauth-plus==2.0 + django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 From b86765d9c02388f6bb82dbb5824a005e8fe73dec Mon Sep 17 00:00:00 2001 From: Philip Forget Date: Fri, 15 Nov 2013 12:25:32 -0500 Subject: [PATCH 0254/1652] add auth param to request client calls --- rest_framework/tests/test_authentication.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index a44813b69..fe11423dc 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -362,7 +362,8 @@ class OAuthTests(TestCase): def test_post_form_with_urlencoded_parameters(self): """Ensure POSTing with x-www-form-urlencoded auth parameters passes""" params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @@ -424,7 +425,8 @@ class OAuthTests(TestCase): read_write_access_token.resource.is_readonly = False read_write_access_token.resource.save() params = self._create_authorization_url_parameters() - response = self.csrf_client.post('/oauth-with-scope/', params) + auth = self._create_authorization_header() + response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') From ad7aa8fe485580e1bdff53d39fe3ea282d908a04 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 17 Nov 2013 01:27:16 +0100 Subject: [PATCH 0255/1652] =?UTF-8?q?Fixed=20the=20nested=20model=20serial?= =?UTF-8?q?izers=20in=20case=20of=20the=20related=5Fname=20isn=E2=80=99t?= =?UTF-8?q?=20set.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 2 +- .../tests/test_serializer_nested.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f0..e20e582ec 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -872,7 +872,7 @@ class ModelSerializer(Serializer): # Reverse fk or one-to-one relations for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: related_data[field_name] = attrs.pop(field_name) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 029f8bffd..7114a0603 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -6,6 +6,7 @@ Doesn't cover model serializers. from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers +from . import models class WritableNestedSerializerBasicTests(TestCase): @@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): serializer = self.AlbumSerializer(instance=original, data=data) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected) + + +class NestedModelSerializerUpdateTests(TestCase): + def test_second_nested_level(self): + john = models.Person.objects.create(name="john") + + post = john.blogpost_set.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") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.BlogPostComment + + class BlogPostSerializer(serializers.ModelSerializer): + comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') + class Meta: + model = models.BlogPost + fields = ('id', 'title', 'comments') + + class PersonSerializer(serializers.ModelSerializer): + posts = BlogPostSerializer(many=True, source='blogpost_set') + class Meta: + model = models.Person + fields = ('id', 'name', 'age', 'posts') + + serialize = PersonSerializer(instance=john) + deserialize = PersonSerializer(data=serialize.data, instance=john) + self.assertTrue(deserialize.is_valid()) + + result = deserialize.object + result.save() + self.assertEqual(result.id, john.id) + From f322e894aebd042a9edb0e333662d0b0ed1ba750 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 18 Nov 2013 09:21:09 +0200 Subject: [PATCH 0256/1652] Enabled syntax highlighting in the README file. --- README.md | 63 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 62883e32e..684868006 100644 --- a/README.md +++ b/README.md @@ -48,48 +48,51 @@ Let's take a look at a quick example of using REST framework to build a simple m Here's our project's root `urls.py` module: - from django.conf.urls.defaults import url, patterns, include - from django.contrib.auth.models import User, Group - from rest_framework import viewsets, routers +```python +from django.conf.urls.defaults import url, patterns, include +from django.contrib.auth.models import User, Group +from rest_framework import viewsets, routers - # ViewSets define the view behavior. - class UserViewSet(viewsets.ModelViewSet): - model = User +# ViewSets define the view behavior. +class UserViewSet(viewsets.ModelViewSet): + model = User - class GroupViewSet(viewsets.ModelViewSet): - model = Group +class GroupViewSet(viewsets.ModelViewSet): + model = Group - # Routers provide an easy way of automatically determining the URL conf - router = routers.DefaultRouter() - router.register(r'users', UserViewSet) - router.register(r'groups', GroupViewSet) +# Routers provide an easy way of automatically determining the URL conf +router = routers.DefaultRouter() +router.register(r'users', UserViewSet) +router.register(r'groups', GroupViewSet) - # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. - urlpatterns = patterns('', - url(r'^', include(router.urls)), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) - ) +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browseable API. +urlpatterns = patterns('', + url(r'^', include(router.urls)), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) +) +``` We'd also like to configure a couple of settings for our API. Add the following to your `settings.py` module: - REST_FRAMEWORK = { - # Use hyperlinked styles by default. - # Only used if the `serializer_class` attribute is not set on a view. - 'DEFAULT_MODEL_SERIALIZER_CLASS': - 'rest_framework.serializers.HyperlinkedModelSerializer', - - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ] - } +```python +REST_FRAMEWORK = { + # Use hyperlinked styles by default. + # Only used if the `serializer_class` attribute is not set on a view. + 'DEFAULT_MODEL_SERIALIZER_CLASS': + 'rest_framework.serializers.HyperlinkedModelSerializer', + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ] +} +``` Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting. That's it, we're done! From 075b8c1037588a590360ab5290b25648367a26c5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 09:29:09 +0000 Subject: [PATCH 0257/1652] Add User import. Refs #599 --- docs/tutorial/4-authentication-and-permissions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index ecf92a7b1..b472322a3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -67,6 +67,9 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. + from django.contrib.auth.models import User + + class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer From a8b15f4290f4bad17d0dd599b8d5c29c155b89e5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 15:06:39 +0100 Subject: [PATCH 0258/1652] =?UTF-8?q?Another=20fix=20for=20nested=20writab?= =?UTF-8?q?le=20serializers=20in=20case=20of=20the=20related=5Fname=20isn?= =?UTF-8?q?=E2=80=99t=20set=20on=20the=20ForeignKey.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e20e582ec..5059c71b3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -940,11 +940,12 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): + related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + fk_field = related_fields[accessor_name].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) From 20325cce1808cc3facd60acb15115d285ca10b0f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 16:10:14 +0000 Subject: [PATCH 0259/1652] Drop .html suffixes in docs --- docs/template.html | 1 + mkdocs.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/template.html b/docs/template.html index 749d0afe3..874ace540 100644 --- a/docs/template.html +++ b/docs/template.html @@ -4,6 +4,7 @@ {{ title }} + diff --git a/mkdocs.py b/mkdocs.py index 13228a0ce..380a91e72 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -19,7 +19,7 @@ if local: index = 'index.html' else: base_url = 'http://django-rest-framework.org' - suffix = '.html' + suffix = '' index = '' @@ -90,7 +90,10 @@ for idx in range(len(path_list)): path = path_list[idx] rel = '../' * path.count('/') - if idx > 0: + if idx == 1 and not local: + # Link back to '/', not '/index' + prev_url_map[path] = '/' + elif idx > 0: prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix if idx < len(path_list) - 1: @@ -143,6 +146,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): else: main_title = 'Django REST framework - ' + main_title + canonical_url = base_url + '/' + relative_path[:-3] + suffix prev_url = prev_url_map.get(relative_path) next_url = next_url_map.get(relative_path) @@ -152,6 +156,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = output.replace('{{ title }}', main_title) output = output.replace('{{ description }}', description) output = output.replace('{{ page_id }}', filename[:-3]) + output = output.replace('{{ canonical_url }}', canonical_url) if prev_url: output = output.replace('{{ prev_url }}', prev_url) From bc69a6d983072ecbcf0c7a79b0b954f0c92892a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 16:10:26 +0000 Subject: [PATCH 0260/1652] Add a 404 page to the docs --- docs/404.html | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/404.html diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 000000000..4938da6ea --- /dev/null +++ b/docs/404.html @@ -0,0 +1,201 @@ + + + + + Django REST framework - 404 - Page not found + + + + + + + + + + + + + + + + + + + +
    + + + +
    +
    + + + + +
    +
    +

    404

    +

    Page not found

    +

    Try the homepage, or search the documentation.

    +
    +
    +
    +
    + +
    +
    + + + + + + + + + + From 88f5921f2fb7f4480e5d3da97508c815bba17155 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 20:15:35 +0100 Subject: [PATCH 0261/1652] Removed the DynamicSerializerView duplication --- rest_framework/tests/test_generics.py | 41 +++++++++++++-------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 79cd99ac5..3fcef6060 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -508,6 +508,25 @@ class ExclusiveFilterBackend(object): return queryset.filter(text='other') +class TwoFieldModel(models.Model): + field_a = models.CharField(max_length=100) + field_b = models.CharField(max_length=100) + + +class DynamicSerializerView(generics.ListCreateAPIView): + model = TwoFieldModel + renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) + + def get_serializer_class(self): + if self.request.method == 'POST': + class DynamicSerializer(serializers.ModelSerializer): + class Meta: + model = TwoFieldModel + fields = ('field_b',) + return DynamicSerializer + return super(DynamicSerializerView, self).get_serializer_class() + + class TestFilterBackendAppliedToViews(TestCase): def setUp(self): @@ -564,28 +583,6 @@ class TestFilterBackendAppliedToViews(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) - -class TwoFieldModel(models.Model): - field_a = models.CharField(max_length=100) - field_b = models.CharField(max_length=100) - - -class DynamicSerializerView(generics.ListCreateAPIView): - model = TwoFieldModel - renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) - - def get_serializer_class(self): - if self.request.method == 'POST': - class DynamicSerializer(serializers.ModelSerializer): - class Meta: - model = TwoFieldModel - fields = ('field_b',) - return DynamicSerializer - return super(DynamicSerializerView, self).get_serializer_class() - - -class TestFilterBackendAppliedToViews(TestCase): - def test_dynamic_serializer_form_in_browsable_api(self): """ GET requests to ListCreateAPIView should return filtered list. From 6330212453bcddab9ecc8dcf0c368c3e8af8876d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 10:27:04 +0000 Subject: [PATCH 0262/1652] Canonical page for index should be '/', not '/index' --- mkdocs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mkdocs.py b/mkdocs.py index 380a91e72..d1790168a 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -146,7 +146,10 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): else: main_title = 'Django REST framework - ' + main_title - canonical_url = base_url + '/' + relative_path[:-3] + suffix + if relative_path == 'index.md': + canonical_url = base_url + else: + canonical_url = base_url + '/' + relative_path[:-3] + suffix prev_url = prev_url_map.get(relative_path) next_url = next_url_map.get(relative_path) From ca2bd616d989f78d00641231e645198a6c95caa0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 11:01:27 +0000 Subject: [PATCH 0263/1652] Remove a couple of .html suffixes in docs --- docs/index.md | 10 +++++----- docs/topics/2.2-announcement.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index bb2129f6f..775fae620 100644 --- a/docs/index.md +++ b/docs/index.md @@ -255,11 +255,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [image]: img/quickstart.png [index]: . -[oauth1-section]: api-guide/authentication.html#oauthauthentication -[oauth2-section]: api-guide/authentication.html#oauth2authentication -[serializer-section]: api-guide/serializers.html#serializers -[modelserializer-section]: api-guide/serializers.html#modelserializer -[functionview-section]: api-guide/views.html#function-based-views +[oauth1-section]: api-guide/authentication#oauthauthentication +[oauth2-section]: api-guide/authentication#oauth2authentication +[serializer-section]: api-guide/serializers#serializers +[modelserializer-section]: api-guide/serializers#modelserializer +[functionview-section]: api-guide/views#function-based-views [sandbox]: http://restframework.herokuapp.com/ [quickstart]: tutorial/quickstart.md diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 7d2760499..0f980e1cb 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ [python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy -[credits]: http://django-rest-framework.org/topics/credits.html +[credits]: http://django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [marcgibbons]: https://github.com/marcgibbons/ From 9cea6880f7103c4e9407f975753c830f109c8c7c Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Tue, 19 Nov 2013 15:49:31 +0100 Subject: [PATCH 0264/1652] Added handling of validation errors in PUT-as-create. Fixes #1035. --- rest_framework/mixins.py | 8 +++++++- rest_framework/tests/test_generics.py | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 4606c78b6..79f79c30c 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways. """ from __future__ import unicode_literals +from django.core.exceptions import ValidationError from django.http import Http404 from rest_framework import status from rest_framework.response import Response @@ -127,7 +128,12 @@ class UpdateModelMixin(object): files=request.FILES, partial=partial) if serializer.is_valid(): - self.pre_save(serializer.object) + try: + self.pre_save(serializer.object) + except ValidationError as err: + # full_clean on model instance may be called in pre_save, so we + # have to handle eventual errors. + return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) self.object = serializer.save(**save_kwargs) self.post_save(self.object, created=created) return Response(serializer.data, status=success_status_code) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 3fcef6060..996bd5b0e 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ model = BasicModel + def get_queryset(self): + queryset = super(InstanceView, self).get_queryset() + return queryset.exclude(text='filtered out') + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -160,10 +164,10 @@ class TestInstanceView(TestCase): """ Create 3 BasicModel intances. """ - items = ['foo', 'bar', 'baz'] + items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: BasicModel(text=item).save() - self.objects = BasicModel.objects + self.objects = BasicModel.objects.exclude(text='filtered out') self.data = [ {'id': obj.id, 'text': obj.text} for obj in self.objects.all() @@ -352,6 +356,17 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEqual(updated.text, 'foobar') + def test_put_to_filtered_out_instance(self): + """ + PUT requests to an URL of instance which is filtered out should not be + able to create new objects. + """ + data = {'text': 'foo'} + filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk + request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') + response = self.view(request, pk=filtered_out_pk).render() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object From 304b193efe076868ceca624543183791f5931726 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 15:58:34 +0000 Subject: [PATCH 0265/1652] Update release-notes.md --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 6c1447d53..0759bd9d1 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. + ### 2.3.9 **Date**: 15th November 2013 From 3765865b4bf69d76d5bcb8e9c8071f4380b54177 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Nov 2013 17:40:56 +0000 Subject: [PATCH 0266/1652] Update 'default' explanation. Closes #1239 --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4272c9a7a..03c5af32a 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -41,7 +41,7 @@ Defaults to `True`. ### `default` -If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all. +If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. May be set to a function or other callable, in which case the value will be evaluated each time it is used. From 263281d71d0425d7bb9b4ebbdf1811ef637ee60a Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Thu, 21 Nov 2013 20:09:48 +0000 Subject: [PATCH 0267/1652] Fix issue #1231: JSONEncoder doesn't handle dict-like objects Check for __getitem__ and then attempt to convert to a dict. The check for __getitem__ is there as there's no universal way to check if an object is a mapping type, but this is a likely proxy --- rest_framework/tests/test_renderers.py | 57 ++++++++++++++++++++++++++ rest_framework/utils/encoders.py | 6 +++ 2 files changed, 63 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..18da6ef85 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -18,6 +18,9 @@ from rest_framework.test import APIRequestFactory import datetime import pickle import re +import UserDict +import collections +import json DUMMYSTATUS = status.HTTP_200_OK @@ -244,6 +247,60 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') + def test_render_userdict_obj(self): + class DictLike(UserDict.DictMixin): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def keys(self): + return self._dict.keys() + x = DictLike() + x['a'] = 1 + x['b'] = "string value" + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + + def test_render_dict_abc_obj(self): + class Dict(collections.MutableMapping): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def __iter__(self): + return self._dict.__iter__() + def __len__(self): + return self._dict.__len__() + + x = Dict() + x['key'] = 'string value' + x[2] = 3 + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + + + def test_render_obj_with_getitem(self): + class DictLike(object): + def __init__(self): + self._dict = {} + def set(self, value): + self._dict = dict(value) + def __getitem__(self, key): + return self._dict[key] + + x = DictLike() + x.set({'a': 1, 'b': 'string'}) + with self.assertRaises(TypeError): + JSONRenderer().render(x) + def test_without_content_type_args(self): """ Test basic JSON rendering. diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35ad206b9..22b1ab3de 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -44,6 +44,12 @@ class JSONEncoder(json.JSONEncoder): return str(o) elif hasattr(o, 'tolist'): return o.tolist() + elif hasattr(o, '__getitem__'): + try: + return dict(o) + except KeyError: + # Couldn't convert to a dict, fall through + pass elif hasattr(o, '__iter__'): return [i for i in o] return super(JSONEncoder, self).default(o) From 6af31ed3945fd051a6e8c08851d7a656637d1f00 Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Fri, 22 Nov 2013 10:59:48 +0000 Subject: [PATCH 0268/1652] Remove u from literals --- rest_framework/tests/test_renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 18da6ef85..9c9c7452e 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -263,7 +263,7 @@ class JSONRendererTests(TestCase): x['a'] = 1 x['b'] = "string value" ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) def test_render_dict_abc_obj(self): class Dict(collections.MutableMapping): @@ -284,7 +284,7 @@ class JSONRendererTests(TestCase): x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): From a38d9d5b24501ae0e279c9afbea08e423112ba34 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 09:33:47 +0000 Subject: [PATCH 0269/1652] Add choices to options metadata for ChoiceField. --- rest_framework/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3b..80eff66c4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -514,6 +514,11 @@ class ChoiceField(WritableField): choices = property(_get_choices, _set_choices) + def metadata(self): + data = super(ChoiceField, self).metadata() + data['choices'] = self.choices + return data + def validate(self, value): """ Validates that the input is in self.choices. From 2484fc914159571a3867c2dae2d9b51314f4581d Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 17:10:16 +0000 Subject: [PATCH 0270/1652] Add more context to the ChoiceField metadata. --- 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 80eff66c4..1657e57f3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = self.choices + data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] return data def validate(self, value): From 8d09f56061a3ee82e31fb646cfa84484ae525f88 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Wed, 27 Nov 2013 11:00:15 +0000 Subject: [PATCH 0271/1652] Add unittests for ChoiceField metadata. Rename 'name' to 'display_name'. --- rest_framework/fields.py | 2 +- rest_framework/tests/test_fields.py | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1657e57f3..0fca718e7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] + data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices] return data def validate(self, value): diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ab2cceacd..5c96bce92 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) def test_invalid_choice_model(self): - s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): """ - Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + Test that the 'empty' value is correctly passed and used depending on + the 'null' property on the model field. """ - s = ChoiceFieldModelSerializer(data={'choice' : ''}) + s = ChoiceFieldModelSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], '') - s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + s = ChoiceFieldModelWithNullSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], None) @@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.from_native(''), None) self.assertEqual(f.from_native(None), None) + def test_metadata_choices(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES] + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + + def test_metadata_choices_not_required(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} + for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES] + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + class EmailFieldTests(TestCase): """ From 2dce8d7a8a0e64f84994b6ac437e2d96920f094e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:23:49 +0200 Subject: [PATCH 0272/1652] Recommend using Pillow instead of PIL. --- docs/api-guide/fields.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 03c5af32a..b0dedd394 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires the `PIL` package. +Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. Signature and validation is the same as with `FileField`. @@ -345,3 +345,4 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime +[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html From b8f8fb7779dc01b5117e468345aaf99304f807ac Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:26:49 +0200 Subject: [PATCH 0273/1652] Updated the assertion message of the ImageField. --- 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 6c07dbb3b..463d296f6 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -966,7 +966,7 @@ class ImageField(FileField): return None from rest_framework.compat import Image - assert Image is not None, 'PIL must be installed for ImageField support' + assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.' # We need to get a file object for PIL. We might have a path or we might # have to read the data into memory. From c46106c96158a99eb2ff29c464a2fa60aff23122 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 14:47:37 +0200 Subject: [PATCH 0274/1652] Rephrased documentation changes according to feedback on IRC. --- docs/api-guide/fields.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b0dedd394..e05c03061 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. +Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained. Signature and validation is the same as with `FileField`. @@ -345,4 +345,3 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime -[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html From 850cd83ba709e863598f8eec3d6551ef3bc3801c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 2 Dec 2013 11:44:04 +0100 Subject: [PATCH 0275/1652] Fix TemplateHTMLRenderer example --- docs/api-guide/renderers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 858e2f072..f30fa26a4 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -167,14 +167,14 @@ The template name is determined by (in order of preference): An example of a view that uses `TemplateHTMLRenderer`: - class UserDetail(generics.RetrieveUserAPIView): + class UserDetail(generics.RetrieveAPIView): """ A view that returns a templated HTML representations of a given user. """ queryset = User.objects.all() renderer_classes = (TemplateHTMLRenderer,) - def get(self, request, *args, **kwargs) + def get(self, request, *args, **kwargs): self.object = self.get_object() return Response({'user': self.object}, template_name='user_detail.html') From 699ec7236b326c97a98c6058280b822c701393fe Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Tue, 3 Dec 2013 00:07:41 +0000 Subject: [PATCH 0276/1652] Adds pre_delete and post_delete hooks on --- docs/api-guide/generic-views.md | 4 +++- rest_framework/generics.py | 12 ++++++++++++ rest_framework/mixins.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index b92427244..83c3e45f6 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -163,12 +163,14 @@ For example: return 20 return 100 -**Save hooks**: +**Save / deletion hooks**: The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes. * `pre_save(self, obj)` - A hook that is called before saving an object. * `post_save(self, obj, created=False)` - A hook that is called after saving an object. +* `pre_delete(self, obj)` - A hook that is called before deleting an object. +* `post_delete(self, obj)` - A hook that is called after deleting an object. The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7cb80a84c..fd411ad38 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -344,6 +344,18 @@ class GenericAPIView(views.APIView): """ pass + def pre_delete(self, obj): + """ + Placeholder method for calling before deleting an object. + """ + pass + + def post_delete(self, obj): + """ + Placeholder method for calling after saving an object. + """ + pass + def metadata(self, request): """ Return a dictionary of metadata about the view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 79f79c30c..43950c4bf 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -192,5 +192,7 @@ class DestroyModelMixin(object): """ def destroy(self, request, *args, **kwargs): obj = self.get_object() + self.pre_delete(obj) obj.delete() + self.post_delete(obj) return Response(status=status.HTTP_204_NO_CONTENT) From fe4c7d4000675a1720e7d3e02c3014a693d835aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:26:58 +0000 Subject: [PATCH 0277/1652] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0759bd9d1..c7e24a5ee 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 From c1d9a96df03ea81454d7d0e3583c68e270ed5043 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:58:05 +0000 Subject: [PATCH 0278/1652] Catch errors during parsing and set empty .DATA/.FILES before re-raising. --- rest_framework/request.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index b883d0d4f..9b551aa80 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -356,7 +356,16 @@ class Request(object): if not parser: raise exceptions.UnsupportedMediaType(media_type) - parsed = parser.parse(stream, media_type, self.parser_context) + try: + parsed = parser.parse(stream, media_type, self.parser_context) + except: + # If we get an exception during parsing, fill in empty data and + # re-raise. Ensures we don't simply repeat the error when + # attempting to render the browsable renderer response, or when + # logging the request or similar. + self._data = QueryDict('', self._request._encoding) + self._files = MultiValueDict() + raise # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. From b92c911cf66805f7826713c68f4e6704b2ad4589 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:05:19 +0000 Subject: [PATCH 0279/1652] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c7e24a5ee..f9ad55f76 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. From 9f1918e41e1b8dcfa621b00788bab865f2fc31aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:06:57 +0000 Subject: [PATCH 0280/1652] Added @ian-foote, for work on #1250. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6c9c034f..3395cd9e2 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -179,6 +179,7 @@ The following people have helped make REST framework great. * Yamila Moreno - [yamila-moreno] * Rob Hudson - [robhudson] * Alex Good - [alexjg] +* Ian Foote - [ian-foote] Many thanks to everyone who's contributed to the project. @@ -394,3 +395,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [yamila-moreno]: https://github.com/yamila-moreno [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg +[ian-foote]: https://github.com/ian-foote From 774298f145d18292b76f2bd90469e25c1950b1af Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:18:35 +0000 Subject: [PATCH 0281/1652] First pass at a test for ParseErrors breaking the browsable API --- rest_framework/tests/test_renderers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..549e763b1 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -88,6 +88,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -219,6 +220,12 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_parse_error_renderers_browsable_api(self): + """Invalid data should still render the browsable API correctly.""" + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') + self.assertContains(resp.content, 'Mock GET View') + self.assertEqual(resp.status_code, status.HTTP_400_) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 38d78b21c0a7c68c205ebe6e79433ca51fe609ce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:55:11 +0000 Subject: [PATCH 0282/1652] Remove Content-Type header from empty responses. Fixes #1196 --- docs/topics/release-notes.md | 1 + rest_framework/response.py | 4 ++++ rest_framework/tests/test_renderers.py | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f9ad55f76..e6085f592 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3e..1dc6abcf6 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse): assert charset, 'renderer returned unicode, and did not specify ' \ 'a charset value.' return bytes(ret.encode(charset)) + + if not ret: + del self['Content-Type'] + return ret @property diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a890..f7de8fd72 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -64,11 +64,16 @@ class MockView(APIView): class MockGETView(APIView): - def get(self, request, **kwargs): return Response({'foo': ['bar', 'baz']}) +class EmptyGETView(APIView): + renderer_classes = (JSONRenderer,) + + def get(self, request, **kwargs): + return Response(status=status.HTTP_204_NO_CONTENT) + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -90,6 +95,7 @@ urlpatterns = patterns('', url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^empty$', EmptyGETView.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) ) @@ -219,6 +225,16 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_204_no_content_responses_have_no_content_type_set(self): + """ + Regression test for #1196 + + https://github.com/tomchristie/django-rest-framework/issues/1196 + """ + resp = self.client.get('/empty') + self.assertEqual(resp.get('Content-Type', None), None) + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 3c3906e278d5e707ab1fd72bdbcb79649777df33 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 08:51:34 +0000 Subject: [PATCH 0283/1652] Clarify wording, fixes #1133. --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 1062cb32c..4fdd9364d 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, #### Example -Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: class AccountViewSet(viewsets.ModelViewSet): """ From de5b9e39dd4c21f4dcceb7cf13c7366b0fb74ec9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 14:59:09 +0000 Subject: [PATCH 0284/1652] First pass on contribution guide --- docs/img/travis-status.png | Bin 0 -> 10023 bytes docs/index.md | 1 + docs/template.html | 1 + docs/topics/contributing.md | 91 ++++++++++++++++++++++++------------ 4 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 docs/img/travis-status.png diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png new file mode 100644 index 0000000000000000000000000000000000000000..fec98cf9b2ba728a532df7b50b89f505053e9012 GIT binary patch literal 10023 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 zm{-Browser enhancements
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • +
  • Contributing to REST framework
  • 2.0 Announcement
  • 2.2 Announcement
  • 2.3 Announcement
  • diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 123e4a8a1..2b18c4f68 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -6,19 +6,27 @@ There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. -# Community +## Community -If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users. +The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter. +If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +## Code of conduct + +Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. + +Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. + +The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. + # Issues -It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. +It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. Some tips on good issue reporting: @@ -26,30 +34,61 @@ Some tips on good issue reporting: * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +## Triaging issues +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to -* TODO: Triage +* Read through the ticket - does it make sense, is it missing any context that would help explain it better? +* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? +* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? +* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? # Development +To start developing on Django REST framework, clone the repo: -* git clone & PYTHONPATH -* Pep8 -* Recommend editor that runs pep8 + git clone git@github.com:tomchristie/django-rest-framework.git -### Pull requests +Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. -* Make pull requests early -* Describe branching +## Testing -### Managing compatibility issues +To run the tests, clone the repository, and then: -* Describe compat module + # Setup the virtual environment + virtualenv env + env/bin/activate + pip install -r requirements.txt + pip install -r optionals.txt -# Testing + # Run the tests + rest_framework/runtests/runtests.py -* Running the tests -* tox +You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: + + tox + +## Pull requests + +It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. + +It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. + +It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. + +GitHub's documentation for working on pull requests is [available here][pull-requests]. + +Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. + +Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. + +![Travis status][travis-status] + +*Above: Travis build notifications* + +## Managing compatibility issues + +Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. # Documentation @@ -77,7 +116,7 @@ Some other tips: * Keep paragraphs reasonably short. * Use double spacing after the end of sentences. -* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'. +* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. ## Markdown style @@ -118,25 +157,19 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- - **Note:** Make sure you do this thing. + **Note:** A useful documentation note. --- -# Third party packages - -* Django reusable app - -# Core committers - -* Still use pull reqs -* Credits - [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html -[github]: https://github.com/tomchristie/django-rest-framework -[django-packages]: https://www.djangopackages.com/grids/g/api/ +[code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [so-filter]: http://stackexchange.com/filters/66475/rest-framework [issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open +[pep-8]: http://www.python.org/dev/peps/pep-0008/ +[travis-status]: ../img/travis-status.png +[pull-requests]: https://help.github.com/articles/using-pull-requests +[tox]: http://tox.readthedocs.org/en/latest/ [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ From f2682537e0fa91bb415be1a64e6bc85275129141 Mon Sep 17 00:00:00 2001 From: Drew Kowalik Date: Wed, 4 Dec 2013 16:10:05 -0800 Subject: [PATCH 0285/1652] fix broken documentation links --- docs/api-guide/views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 15581e098..194a7a6b3 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html -[settings]: api-guide/settings.md -[throttling]: api-guide/throttling.md +[settings]: settings.md +[throttling]: throttling.md From f8088bedef04c5bc487bdc764ac54d1f18f42c26 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 09:01:00 +0000 Subject: [PATCH 0286/1652] Upgrade JSONP security warning. --- docs/api-guide/renderers.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index f30fa26a4..cf2005691 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`. -**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. +--- + +**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. + +The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions. + +--- **.media_type**: `application/javascript` @@ -419,6 +425,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [cors]: http://www.w3.org/TR/cors/ [cors-docs]: ../topics/ajax-csrf-cors.md +[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use [testing]: testing.md [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven From 1f8069c0a9740297e7b5d5fa0c81830c876d7240 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 11:05:25 +0000 Subject: [PATCH 0287/1652] Boilerplate cuteness --- rest_framework/__init__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index de82fef51..b6a4d3a04 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,6 +1,20 @@ -__version__ = '2.3.9' +""" +______ _____ _____ _____ __ _ +| ___ \ ___/ ___|_ _| / _| | | +| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ +| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / +| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < +\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| +""" -VERSION = __version__ # synonym +__title__ = 'Django REST framework' +__version__ = '2.3.9' +__author__ = 'Tom Christie' +__license__ = 'BSD 2-Clause' +__copyright__ = 'Copyright 2011-2013 Tom Christie' + +# Version synonym +VERSION = __version__ # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' From 79596dc613bbf24aac7b5c56179cbc5c46eacdf3 Mon Sep 17 00:00:00 2001 From: Justin Davis Date: Thu, 5 Dec 2013 13:17:23 -0800 Subject: [PATCH 0288/1652] fix setup.py with new __init__.py boilerplate --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 26d072837..1a487f178 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def get_version(package): Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() - return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) + return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_packages(package): From cf6c11bd4b7e7fdaa1de659d69792030e565412a Mon Sep 17 00:00:00 2001 From: Chuck Harmston Date: Fri, 6 Dec 2013 14:00:23 -0600 Subject: [PATCH 0289/1652] Raise appropriate error in serializer when making a partial update to set a required RelatedField to null (issue #1158) --- rest_framework/serializers.py | 5 ++++- rest_framework/tests/test_serializer.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f0..44e4b04b1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -896,7 +896,10 @@ class ModelSerializer(Serializer): # Update an existing instance... if instance is not None: for key, val in attrs.items(): - setattr(instance, key, val) + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # ...or create a new instance else: diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a4749..eca467ee2 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -558,6 +558,29 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + def test_foreign_key_is_null_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test that a null foreign key does not pass validation + """ + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) + self.assertFalse(photo_serializer.is_valid()) + self.assertTrue('album' in photo_serializer.errors) + self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + def test_foreign_key_with_partial(self): """ Test ModelSerializer validation with partial=True From 51359e461299905c6cd359000941f9da3d561f7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:42:52 +0000 Subject: [PATCH 0290/1652] Added @chuckharmston for kickass bug squashing in #1272 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 3395cd9e2..1a838421d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -180,6 +180,7 @@ The following people have helped make REST framework great. * Rob Hudson - [robhudson] * Alex Good - [alexjg] * Ian Foote - [ian-foote] +* Chuck Harmston - [chuckharmston] Many thanks to everyone who's contributed to the project. @@ -396,3 +397,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg [ian-foote]: https://github.com/ian-foote +[chuckharmston]: https://github.com/chuckharmston From 85d9eb0f7ed3ef66a25a443b34ead914a506462c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:47:26 +0000 Subject: [PATCH 0291/1652] Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e6085f592..2df2cf931 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. From 910de38a9c8cd03243e738c8f4adcbade8a4d7d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 22:13:50 +0000 Subject: [PATCH 0292/1652] Version 2.3.10 --- docs/api-guide/status-codes.md | 21 ++++++++++++++++++ docs/topics/release-notes.md | 5 ++++- rest_framework/__init__.py | 2 +- rest_framework/status.py | 17 +++++++++++++++ rest_framework/tests/test_status.py | 33 +++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_status.py diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index 409f659b2..64c464349 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc The full set of HTTP status codes included in the `status` module is listed below. +The module also includes a set of helper functions for testing if a status code is in a given range. + + from rest_framework import status + from rest_framework.test import APITestCase + + class ExampleTestCase(APITestCase): + def test_url_root(self): + url = reverse('index') + response = self.client.get(url) + self.assertTrue(status.is_success(response.status_code)) + + For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616] and [RFC 6585][rfc6585]. @@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s HTTP_505_HTTP_VERSION_NOT_SUPPORTED HTTP_511_NETWORK_AUTHENTICATION_REQUIRED +## Helper functions + +The following helper functions are available for identifying the category of the response code. + + is_informational() # 1xx + is_success() # 2xx + is_redirect() # 3xx + is_client_error() # 4xx + is_server_error() # 5xx [rfc2324]: http://www.ietf.org/rfc/rfc2324.txt [rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 2df2cf931..b080ad436 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,10 +40,13 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.10 + +**Date**: 6th December 2013 * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Added status code category helper functions. * Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b6a4d3a04..f5483b9d6 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.9' +__version__ = '2.3.10' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2013 Tom Christie' diff --git a/rest_framework/status.py b/rest_framework/status.py index b9f249f9f..764353711 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585 """ from __future__ import unicode_literals + +def is_informational(code): + return code >= 100 and code <= 199 + +def is_success(code): + return code >= 200 and code <= 299 + +def is_redirect(code): + return code >= 300 and code <= 399 + +def is_client_error(code): + return code >= 400 and code <= 499 + +def is_server_error(code): + return code >= 500 and code <= 599 + + HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py new file mode 100644 index 000000000..7b1bdae31 --- /dev/null +++ b/rest_framework/tests/test_status.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.status import ( + is_informational, is_success, is_redirect, is_client_error, is_server_error +) + + +class TestStatus(TestCase): + def test_status_categories(self): + self.assertFalse(is_informational(99)) + self.assertTrue(is_informational(100)) + self.assertTrue(is_informational(199)) + self.assertFalse(is_informational(200)) + + self.assertFalse(is_success(199)) + self.assertTrue(is_success(200)) + self.assertTrue(is_success(299)) + self.assertFalse(is_success(300)) + + self.assertFalse(is_redirect(299)) + self.assertTrue(is_redirect(300)) + self.assertTrue(is_redirect(399)) + self.assertFalse(is_redirect(400)) + + self.assertFalse(is_client_error(399)) + self.assertTrue(is_client_error(400)) + self.assertTrue(is_client_error(499)) + self.assertFalse(is_client_error(500)) + + self.assertFalse(is_server_error(499)) + self.assertTrue(is_server_error(500)) + self.assertTrue(is_server_error(599)) + self.assertFalse(is_server_error(600)) \ No newline at end of file From 9ab0759e38492d9950d66299ed5c58155d39e696 Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:21:33 -0800 Subject: [PATCH 0293/1652] Add tests to pass for get_ident method in BaseThrottle class. --- rest_framework/tests/test_throttling.py | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py index 41bff6926..031276968 100644 --- a/rest_framework/tests/test_throttling.py +++ b/rest_framework/tests/test_throttling.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.contrib.auth.models import User from django.core.cache import cache +from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.views import APIView from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle @@ -275,3 +276,67 @@ class ScopedRateThrottleTests(TestCase): self.increment_timer() response = self.unscoped_view(request) self.assertEqual(200, response.status_code) + +class XffTestingBase(TestCase): + def setUp(self): + + class Throttle(ScopedRateThrottle): + THROTTLE_RATES = {'test_limit': '1/day'} + TIMER_SECONDS = 0 + timer = lambda self: self.TIMER_SECONDS + + class View(APIView): + throttle_classes = (Throttle,) + throttle_scope = 'test_limit' + + def get(self, request): + return Response('test_limit') + + cache.clear() + self.throttle = Throttle() + self.view = View.as_view() + self.request = APIRequestFactory().get('/some_uri') + self.request.META['REMOTE_ADDR'] = '3.3.3.3' + self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 2.2.2.2' + + def config_proxy(self, num_proxies): + setattr(api_settings, 'NUM_PROXIES', num_proxies) + + +class IdWithXffBasicTests(XffTestingBase): + def test_accepts_request_under_limit(self): + self.config_proxy(0) + self.assertEqual(200, self.view(self.request).status_code) + + def test_denies_request_over_limit(self): + self.config_proxy(0) + self.view(self.request) + self.assertEqual(429, self.view(self.request).status_code) + + +class XffSpoofingTests(XffTestingBase): + def test_xff_spoofing_doesnt_change_machine_id_with_one_app_proxy(self): + self.config_proxy(1) + self.view(self.request) + self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 5.5.5.5, 2.2.2.2' + self.assertEqual(429, self.view(self.request).status_code) + + def test_xff_spoofing_doesnt_change_machine_id_with_two_app_proxies(self): + self.config_proxy(2) + self.view(self.request) + self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 1.1.1.1, 2.2.2.2' + self.assertEqual(429, self.view(self.request).status_code) + + +class XffUniqueMachinesTest(XffTestingBase): + def test_unique_clients_are_counted_independently_with_one_proxy(self): + self.config_proxy(1) + self.view(self.request) + self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 7.7.7.7' + self.assertEqual(200, self.view(self.request).status_code) + + def test_unique_clients_are_counted_independently_with_two_proxies(self): + self.config_proxy(2) + self.view(self.request) + self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2' + self.assertEqual(200, self.view(self.request).status_code) From 89f26c5e040febd27bc9142b0096ca119bb3fa32 Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:21:52 -0800 Subject: [PATCH 0294/1652] Add get_ident method to pass new tests. --- rest_framework/settings.py | 1 + rest_framework/throttling.py | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8abaf1409..383de72ea 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -63,6 +63,7 @@ DEFAULTS = { 'user': None, 'anon': None, }, + 'NUM_PROXIES': None, # Pagination 'PAGINATE_BY': None, diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index a946d837f..60e46d479 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -18,6 +18,21 @@ class BaseThrottle(object): """ raise NotImplementedError('.allow_request() must be overridden') + def get_ident(self, request): + """ + Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR + if present and number of proxies is > 0. If not use all of + HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR. + """ + xff = request.META.get('HTTP_X_FORWARDED_FOR') + remote_addr = request.META.get('REMOTE_ADDR') + num_proxies = api_settings.NUM_PROXIES + + if xff and num_proxies: + return xff.split(',')[-min(num_proxies, len(xff))].strip() + + return xff if xff else remote_addr + def wait(self): """ Optionally, return a recommended number of seconds to wait before @@ -152,13 +167,9 @@ class AnonRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): return None # Only throttle unauthenticated requests. - ident = request.META.get('HTTP_X_FORWARDED_FOR') - if ident is None: - ident = request.META.get('REMOTE_ADDR') - return self.cache_format % { 'scope': self.scope, - 'ident': ident + 'ident': self.get_ident(request) } @@ -176,7 +187,7 @@ class UserRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): ident = request.user.id else: - ident = request.META.get('REMOTE_ADDR', None) + ident = self.get_ident(request) return self.cache_format % { 'scope': self.scope, @@ -224,7 +235,7 @@ class ScopedRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): ident = request.user.id else: - ident = request.META.get('REMOTE_ADDR', None) + ident = self.get_ident(request) return self.cache_format % { 'scope': self.scope, From 100a933279e3119e2627d744cd7eb472b542f6fe Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:22:08 -0800 Subject: [PATCH 0295/1652] Add documentation to explain what effect these changes have. --- docs/api-guide/throttling.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index cc4692171..ee57383cd 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -35,11 +35,16 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day' - } + }, + 'NUM_PROXIES': 2, } The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. +By default Django REST Framework will try to use the `HTTP_X_FORWARDED_FOR` header to uniquely identify client machines for throttling. If HTTP_X_FORWARDED_FOR is not present `REMOTE_ADDR` header value will be used. + +To help Django REST Framework identify unique clients the number of application proxies can be set using `NUM_PROXIES`. This setting will allow the throttle to correctly identify unique requests whenthere are multiple application side proxies in front of the server. `NUM_PROXIES` should be set to an integer. It is important to understand that if you configure `NUM_PROXIES > 0` all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. + You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. From 196c5952e4f610054e832aef36cb2383b8c129c0 Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:24:16 -0800 Subject: [PATCH 0296/1652] Fix typo --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index ee57383cd..69b15a829 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -43,7 +43,7 @@ The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `mi By default Django REST Framework will try to use the `HTTP_X_FORWARDED_FOR` header to uniquely identify client machines for throttling. If HTTP_X_FORWARDED_FOR is not present `REMOTE_ADDR` header value will be used. -To help Django REST Framework identify unique clients the number of application proxies can be set using `NUM_PROXIES`. This setting will allow the throttle to correctly identify unique requests whenthere are multiple application side proxies in front of the server. `NUM_PROXIES` should be set to an integer. It is important to understand that if you configure `NUM_PROXIES > 0` all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. +To help Django REST Framework identify unique clients the number of application proxies can be set using `NUM_PROXIES`. This setting will allow the throttle to correctly identify unique requests when there are multiple application side proxies in front of the server. `NUM_PROXIES` should be set to an integer. It is important to understand that if you configure `NUM_PROXIES > 0` all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. From 887da7f6c5a9e7b5007f5e4af32a6b93b18c70ea Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:30:33 -0800 Subject: [PATCH 0297/1652] Add missing tick marks --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 69b15a829..34418e84d 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -41,7 +41,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. -By default Django REST Framework will try to use the `HTTP_X_FORWARDED_FOR` header to uniquely identify client machines for throttling. If HTTP_X_FORWARDED_FOR is not present `REMOTE_ADDR` header value will be used. +By default Django REST Framework will try to use the `HTTP_X_FORWARDED_FOR` header to uniquely identify client machines for throttling. If `HTTP_X_FORWARDED_FOR` is not present `REMOTE_ADDR` header value will be used. To help Django REST Framework identify unique clients the number of application proxies can be set using `NUM_PROXIES`. This setting will allow the throttle to correctly identify unique requests when there are multiple application side proxies in front of the server. `NUM_PROXIES` should be set to an integer. It is important to understand that if you configure `NUM_PROXIES > 0` all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. From 23db6c98495d7b3c18a3069c6cb770d5cbc18ee1 Mon Sep 17 00:00:00 2001 From: kahnjw Date: Fri, 6 Dec 2013 14:52:39 -0800 Subject: [PATCH 0298/1652] PEP8 Compliance --- rest_framework/tests/test_throttling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py index 031276968..8c5eefe9c 100644 --- a/rest_framework/tests/test_throttling.py +++ b/rest_framework/tests/test_throttling.py @@ -277,6 +277,7 @@ class ScopedRateThrottleTests(TestCase): response = self.unscoped_view(request) self.assertEqual(200, response.status_code) + class XffTestingBase(TestCase): def setUp(self): From db19fba50d65c1093efa25bd5ed1230b6404c8ca Mon Sep 17 00:00:00 2001 From: Andy Wilson Date: Fri, 6 Dec 2013 22:31:07 -0600 Subject: [PATCH 0299/1652] update installation example to work with django 1.6 looks like django.conf.urls.defaults was deprecated as of django 1.6 --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3e5adbc4a..badd6f60a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -100,7 +100,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_ We're ready to create our API now. Here's our project's root `urls.py` module: - from django.conf.urls.defaults import url, patterns, include + from django.conf.urls import url, patterns, include from django.contrib.auth.models import User, Group from rest_framework import viewsets, routers From 3399158d62416af56201eac63cc20d8934f08de2 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 8 Dec 2013 11:40:40 -0500 Subject: [PATCH 0300/1652] RelatedField is function of serializer class --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index b9d96b5e3..556429bb9 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o For example, the following serializer. class AlbumSerializer(serializers.ModelSerializer): - tracks = RelatedField(many=True) + tracks = serializers.RelatedField(many=True) class Meta: model = Album From b8732d21652cf6b6e3c3e9807594b508be6583f8 Mon Sep 17 00:00:00 2001 From: Rustam Lalkaka Date: Sun, 8 Dec 2013 19:34:24 -0500 Subject: [PATCH 0301/1652] Minor grammar fix -- 'team' is singular --- docs/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/template.html b/docs/template.html index 5a0bdbfd4..c065237a5 100644 --- a/docs/template.html +++ b/docs/template.html @@ -172,7 +172,7 @@

    -

    The team behind REST framework are launching a new API service.

    +

    The team behind REST framework is launching a new API service.

    If you want to be first in line when we start issuing invitations, please sign up here:

    From 06d8a31e132c99a9645e26b5def3a1d9b9585c24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:34:08 +0000 Subject: [PATCH 0302/1652] Catch and mask ParseErrors that occur during rendering of the BrowsableAPI. --- rest_framework/renderers.py | 9 +++++++-- rest_framework/request.py | 2 +- rest_framework/tests/test_renderers.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fe4f43d48..2fdd33376 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,6 +20,7 @@ from rest_framework.compat import StringIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml +from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders @@ -420,8 +421,12 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ if request.method == method: - data = request.DATA - files = request.FILES + try: + data = request.DATA + files = request.FILES + except ParseError: + data = None + files = None else: data = None files = None diff --git a/rest_framework/request.py b/rest_framework/request.py index 9b551aa80..fcea25083 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -362,7 +362,7 @@ class Request(object): # If we get an exception during parsing, fill in empty data and # re-raise. Ensures we don't simply repeat the error when # attempting to render the browsable renderer response, or when - # logging the request or similar. + # logging the request or similar. self._data = QueryDict('', self._request._encoding) self._files = MultiValueDict() raise diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 549e763b1..10aa4248b 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -69,6 +69,12 @@ class MockGETView(APIView): return Response({'foo': ['bar', 'baz']}) +class MockPOSTView(APIView): + + def post(self, request, **kwargs): + return Response({'foo': request.DATA}) + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -88,7 +94,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), - url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), + url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -224,8 +230,8 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertContains(resp.content, 'Mock GET View') - self.assertEqual(resp.status_code, status.HTTP_400_) + self.assertIn('Mock Post', resp.content) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 4e9385e709bcee87456a99839841ecf6b56f337a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:37:13 +0000 Subject: [PATCH 0303/1652] Drop unneeded assert --- rest_framework/tests/test_renderers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 10aa4248b..f4818eef2 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -230,7 +230,6 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertIn('Mock Post', resp.content) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' From e80b353085c460c5ab7e9b4d22249f01176c5eb1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:10:51 +0000 Subject: [PATCH 0304/1652] Add notes to contributing docs --- docs/topics/contributing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 2b18c4f68..4dd2718f2 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -33,6 +33,8 @@ Some tips on good issue reporting: * When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. +* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. ## Triaging issues @@ -42,6 +44,7 @@ Getting involved in triaging incoming issues is a good way to start contributing * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? * If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? * If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. # Development From 23369650e3502305ebf5d682e141c7d47db89111 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:14:21 +0000 Subject: [PATCH 0305/1652] Add notes to contributing docs --- docs/topics/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 4dd2718f2..b3ab52bbf 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -10,7 +10,7 @@ There are many ways you can contribute to Django REST framework. We'd like it t The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. From e6f6bb5c7e3e882b0215c981e2f2b6a576820100 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:42:09 +0000 Subject: [PATCH 0306/1652] Add notes to contributing docs --- docs/topics/contributing.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index b3ab52bbf..77c60fb47 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,6 +164,16 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- +## Third party packages + +New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. + +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. + +We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework @@ -176,3 +186,4 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ +[django-reusable-app]: https://github.com/dabapps/django-reusable-app From c1be503308e755d72aae2c9695739bd33631e18b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:46:18 +0000 Subject: [PATCH 0307/1652] Add notes to contributing docs --- docs/topics/contributing.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 77c60fb47..906950bb5 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,16 +164,20 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- -## Third party packages +# Third party packages New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. +## Getting started + If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. -Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. - We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. +## Linking to your package + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework From ddd17c69e7abdd70448fa0f2f2a807d600b3391d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:24:10 +0000 Subject: [PATCH 0308/1652] Fix compat issues for #1231 --- rest_framework/compat.py | 7 ++++++ rest_framework/tests/test_renderers.py | 31 ++++++-------------------- rest_framework/utils/encoders.py | 3 +-- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 581e29fc7..05bd99e0c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -69,6 +69,13 @@ try: except ImportError: import urlparse +# UserDict moves in Python 3 +try: + from UserDict import UserDict + from UserDict import DictMixin +except ImportError: + from collections import UserDict + from collections import MutableMapping as DictMixin # Try to import PIL in either of the two ways it can end up installed. try: diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index d720bc51a..2ae8ae18c 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -15,12 +15,11 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory +from collections import MutableMapping import datetime +import json import pickle import re -import UserDict -import collections -import json DUMMYSTATUS = status.HTTP_200_OK @@ -277,26 +276,8 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') - def test_render_userdict_obj(self): - class DictLike(UserDict.DictMixin): - def __init__(self): - self._dict = dict() - def __getitem__(self, key): - return self._dict.__getitem__(key) - def __setitem__(self, key, value): - return self._dict.__setitem__(key, value) - def __delitem__(self, key): - return self._dict.__delitem__(key) - def keys(self): - return self._dict.keys() - x = DictLike() - x['a'] = 1 - x['b'] = "string value" - ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) - def test_render_dict_abc_obj(self): - class Dict(collections.MutableMapping): + class Dict(MutableMapping): def __init__(self): self._dict = dict() def __getitem__(self, key): @@ -309,13 +290,15 @@ class JSONRendererTests(TestCase): return self._dict.__iter__() def __len__(self): return self._dict.__len__() + def keys(self): + return self._dict.keys() x = Dict() x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) - + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): class DictLike(object): diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 22b1ab3de..3ac920c6f 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -47,8 +47,7 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(o, '__getitem__'): try: return dict(o) - except KeyError: - # Couldn't convert to a dict, fall through + except: pass elif hasattr(o, '__iter__'): return [i for i in o] From 2c898bd9018e40a8d0e4718fb2a0e3672e64782c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:27:10 +0000 Subject: [PATCH 0309/1652] Update release notes --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b080ad436..d3f85e6ee 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* JSON renderer now deals with objects that implement a dict-like interface. + ### 2.3.10 **Date**: 6th December 2013 From de319f3e28d27d71fffce7c8f12c23363d5c25eb Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 9 Dec 2013 09:53:16 +0000 Subject: [PATCH 0310/1652] Fix typo "Not" -> "Note" --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4c3fb9d33..6fc25f57a 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -425,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_ fields = ('url', 'account_name', 'users', 'created') lookup_field = 'slug' -Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. +Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example: From 9ba7be959c2a2fea989527c590ce833df5925e63 Mon Sep 17 00:00:00 2001 From: Maxim Kamenkov Date: Mon, 9 Dec 2013 20:33:06 +0200 Subject: [PATCH 0311/1652] Added REST Condition to 3rd party permissions packages list. --- docs/api-guide/permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 871de84ec..60624b630 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components. +## REST Condition + +The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md @@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t [filtering]: filtering.md [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions [composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions +[rest-condition]: https://github.com/caxap/rest_condition From 785a42cd5aee9e96f9b780ff144fa13c16189748 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:38:43 +0000 Subject: [PATCH 0312/1652] Tweak REST condition text. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 60624b630..6a0f48f44 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -232,7 +232,7 @@ The [Composed Permissions][composed-permissions] package provides a simple way t ## REST Condition -The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. +The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md From 3a1c40f81488c241cb64860d6cc510f8e71c0c40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:46:44 +0000 Subject: [PATCH 0313/1652] Refine model manager behavior so as not to use the behavior in incorrect cases. Closes #1205 --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 44e4b04b1..0d35fb327 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -412,7 +412,13 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() if self.source == '*': if value: From 40164fcc6241489033d7015d429cbe0afc674d37 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:49:54 +0000 Subject: [PATCH 0314/1652] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d3f85e6ee..e186893e1 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. +* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 From c09ad1bedc8559b2e6eadf0e5b8f3732af2d9d29 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:53:38 +0000 Subject: [PATCH 0315/1652] Remove incorrect apostrophe --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e186893e1..1ddd83519 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,7 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. -* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. +* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 From d3a118c728729f63f7e7c7c39b62a7932ca06391 Mon Sep 17 00:00:00 2001 From: Alan Justino Date: Tue, 10 Dec 2013 17:14:17 -0200 Subject: [PATCH 0316/1652] SimpleRouter.get_lookup_regex got lookup_prefix This allows @alanjds/drf-nested-routers to not duplicate/monkeypatch work made here --- rest_framework/routers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 3fee1e494..7915991d9 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -184,18 +184,18 @@ class SimpleRouter(BaseRouter): bound_methods[method] = action return bound_methods - def get_lookup_regex(self, viewset): + def get_lookup_regex(self, viewset, lookup_prefix=''): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. """ if self.trailing_slash: - base_regex = '(?P<{lookup_field}>[^/]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' else: # Don't consume `.json` style suffixes - base_regex = '(?P<{lookup_field}>[^/.]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' lookup_field = getattr(viewset, 'lookup_field', 'pk') - return base_regex.format(lookup_field=lookup_field) + return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) def get_urls(self): """ From 7382f8c6adc17c9feb02d028f7791af632d6dd3b Mon Sep 17 00:00:00 2001 From: David Ray Date: Tue, 10 Dec 2013 14:56:07 -0500 Subject: [PATCH 0317/1652] Update routers.md Reference to ```DefaultRouter``` should be ```SimpleRouter``` --- docs/api-guide/routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index fb48197e9..8151e60f2 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo ## Usage -Here's an example of a simple URL conf, that uses `DefaultRouter`. +Here's an example of a simple URL conf, that uses `SimpleRouter`. from rest_framework import routers From 5acefd3b17e498af756fa48e27d7f8ce19322c7a Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:55:54 +0000 Subject: [PATCH 0318/1652] Add full required imports to Generating Tokens example Previously we were missing User and post_save. --- docs/api-guide/authentication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1a1c68b84..ef77e02c5 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,6 +162,8 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. + from django.contrib.auth.models import User + from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token From 4f473f0b9e918f2e071da0c84bd9b584c00ac919 Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:56:56 +0000 Subject: [PATCH 0319/1652] Use get_user_model instead of User in Generating Tokens example Because that's a better way of doing it. --- docs/api-guide/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ef77e02c5..53efc49a0 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,12 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. - from django.contrib.auth.models import User + from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token - @receiver(post_save, sender=User) + @receiver(post_save, sender=get_user_model()) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) From df2d9034c2a5a07dc3aa5455db892ee94cbed467 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Dec 2013 23:10:31 +0000 Subject: [PATCH 0320/1652] Add third party packages --- docs/api-guide/filtering.md | 9 +++++++++ docs/api-guide/routers.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index a0132ffcb..0e02a2a7d 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -360,6 +360,14 @@ For example, you might need to restrict users to only being able to see objects We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API. +# Third party packages + +The following third party packages provide additional filter implementations. + +## Django REST framework chain + +The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. + [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html @@ -368,3 +376,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views, [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields +[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f2..9001b8597 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,13 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third Party Packages + +The following third party packages provide router implementations that extend the default functionality provided by REST framework. + +## DRF Nested Routers + +The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. + [cite]: http://guides.rubyonrails.org/routing.html +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers From 83da4949c099fcf7e7636c98b9052b502e1bf74b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 00:02:18 +0000 Subject: [PATCH 0321/1652] Allow NUM_PROXIES=0 and include more docs --- docs/api-guide/settings.md | 6 ++++++ docs/api-guide/throttling.md | 18 ++++++++++++------ rest_framework/throttling.py | 8 ++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 13f96f9a3..d8c878ff8 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -359,5 +359,11 @@ The name of a parameter in the URL conf that may be used to provide a format suf Default: `'format'` +#### NUM_PROXIES + +An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes. + +Default: `None` + [cite]: http://www.python.org/dev/peps/pep-0020/ [strftime]: http://docs.python.org/2/library/time.html#time.strftime diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 34418e84d..b2a5bb194 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -35,16 +35,11 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day' - }, - 'NUM_PROXIES': 2, + } } The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period. -By default Django REST Framework will try to use the `HTTP_X_FORWARDED_FOR` header to uniquely identify client machines for throttling. If `HTTP_X_FORWARDED_FOR` is not present `REMOTE_ADDR` header value will be used. - -To help Django REST Framework identify unique clients the number of application proxies can be set using `NUM_PROXIES`. This setting will allow the throttle to correctly identify unique requests when there are multiple application side proxies in front of the server. `NUM_PROXIES` should be set to an integer. It is important to understand that if you configure `NUM_PROXIES > 0` all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. - You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. @@ -71,6 +66,16 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +## How clients are identified + +By default the `X-Forwarded-For` HTTP header is used to uniquely identify client machines for throttling. If the `X-Forwarded-For` header is not present, then the value of the `Remote-Addr` header will be used. + +If you need to more strictly identify unique clients, you'll need to configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of 0 or more, and will allow the throttle to identify the client IP as being the last IP address in the `X-Forwarded-For` header, once any application proxy IP addresses have first been excluded. + +It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. + +Further context on how the `X-Forwarded-For` header works, and identifier a remote client IP can be [found here][identifing-clients]. + ## Setting up the cache The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. @@ -183,5 +188,6 @@ The following is an example of a rate throttle, that will randomly throttle 1 in [cite]: https://dev.twitter.com/docs/error-codes-responses [permissions]: permissions.md +[identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster [cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches [cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 60e46d479..c40f3065b 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -28,8 +28,12 @@ class BaseThrottle(object): remote_addr = request.META.get('REMOTE_ADDR') num_proxies = api_settings.NUM_PROXIES - if xff and num_proxies: - return xff.split(',')[-min(num_proxies, len(xff))].strip() + if num_proxies is not None: + if num_proxies == 0 or xff is None: + return remote_addr + addrs = xff.split(',') + client_addr = addrs[-min(num_proxies, len(xff))] + return client_addr.strip() return xff if xff else remote_addr From ed931b90ae9e72f963673e6e188b1802a5a65360 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 00:11:59 +0000 Subject: [PATCH 0322/1652] Further docs tweaks --- docs/api-guide/throttling.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index b2a5bb194..536f0ab78 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -68,13 +68,13 @@ Or, if you're using the `@api_view` decorator with function based views. ## How clients are identified -By default the `X-Forwarded-For` HTTP header is used to uniquely identify client machines for throttling. If the `X-Forwarded-For` header is not present, then the value of the `Remote-Addr` header will be used. +The `X-Forwarded-For` and `Remote-Addr` HTTP headers are used to uniquely identify client IP addresses for throttling. If the `X-Forwarded-For` header is present then it will be used, otherwise the value of the `Remote-Addr` header will be used. -If you need to more strictly identify unique clients, you'll need to configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of 0 or more, and will allow the throttle to identify the client IP as being the last IP address in the `X-Forwarded-For` header, once any application proxy IP addresses have first been excluded. +If you need to strictly identify unique client IP addresses, you'll need to first configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of zero or more. If set to non-zero then the client IP will be identified as being the last IP address in the `X-Forwarded-For` header, once any application proxy IP addresses have first been excluded. If set to zero, then the `Remote-Addr` header will always be used as the identifying IP address. It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client. -Further context on how the `X-Forwarded-For` header works, and identifier a remote client IP can be [found here][identifing-clients]. +Further context on how the `X-Forwarded-For` header works, and identifing a remote client IP can be [found here][identifing-clients]. ## Setting up the cache From 73e8536e0d38f6677ac30aa2b3ba80563961191f Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Thu, 12 Dec 2013 21:45:44 -0600 Subject: [PATCH 0323/1652] third-party package: wq.db --- docs/api-guide/routers.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f2..ed9031144 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,18 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third party packages + +The following third party packages are also available. + +## wq.db + +[wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration. + + from wq.db.rest import app + from .models import MyModel + app.router.register_model(MyModel) + [cite]: http://guides.rubyonrails.org/routing.html +[wq.db]: http://wq.io/wq.db +[wq.db-router]: http://wq.io/docs/app.py From 0453cbd56bf5c553412b61a2a5e5522a2d44a419 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 11:09:54 +0000 Subject: [PATCH 0324/1652] Clean up implementation --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34f315318..40caa1f38 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -949,7 +949,11 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): - related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship From ca244ad614e2f6fb4fef1dc9987be996d2624303 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 15:30:59 +0000 Subject: [PATCH 0325/1652] Expanded notes in quickstart. Closes #1127. Closes #1128. --- docs/index.md | 2 +- docs/tutorial/quickstart.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index badd6f60a..04804fa79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,7 +112,7 @@ Here's our project's root `urls.py` module: model = Group - # Routers provide an easy way of automatically determining the URL conf + # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() router.register(r'users', UserViewSet) router.register(r'groups', GroupViewSet) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 80bb9abb4..8bf8c7f5c 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. +Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute. + +For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications. + ## URLs Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... @@ -169,6 +173,7 @@ Great, that was easy! If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide]. +[readme-example-api]: ../#example [image]: ../img/quickstart.png [tutorial]: 1-serialization.md [guide]: ../#api-guide From 90edcbf938ed8d6f3b783372c17e60bbf0761b61 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 13 Dec 2013 12:41:44 -0500 Subject: [PATCH 0326/1652] Fix default values always being False for browsable API This fixes a bug that was introduced in 28ff6fb [1] for the browsable API, specifically with how it handled default values for boolean fields. Previously, it had a global default for boolean fields set to `False`, which was different than the standard None that was used elsewhere. Because this only needed to be done for the browsable API, a fix was put into place that only set the default to `False` when form data was passed into the serializer. This had the unintended side effect of overriding any default set on the boolean field. This fixes #1101 [2] by only overriding the default if the default is `None`, which is the default for all fields. [1]: https://github.com/tomchristie/django-rest-framework/commit/28ff6fb1ec02b7a04c4a0db54885f3735b6dd43f [2]: https://github.com/tomchristie/django-rest-framework/issues/1101 --- rest_framework/fields.py | 2 +- rest_framework/tests/test_serializer.py | 39 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5a4f04a5b..f1de447c7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -428,7 +428,7 @@ class BooleanField(WritableField): def field_from_native(self, data, files, field_name, into): # HTML checkboxes do not explicitly represent unchecked as `False` # we deal with that here... - if isinstance(data, QueryDict): + if isinstance(data, QueryDict) and self.default is None: self.default = False return super(BooleanField, self).field_from_native( diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index eca467ee2..14d1c664e 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1743,3 +1743,42 @@ class TestSerializerTransformMethods(TestCase): 'b_renamed': None, } ) + + +class DefaultTrueBooleanModel(models.Model): + cat = models.BooleanField(default=True) + dog = models.BooleanField(default=False) + + +class SerializerDefaultTrueBoolean(TestCase): + + def setUp(self): + super(SerializerDefaultTrueBoolean, self).setUp() + + class DefaultTrueBooleanSerializer(serializers.ModelSerializer): + class Meta: + model = DefaultTrueBooleanModel + fields = ('cat', 'dog') + + self.default_true_boolean_serializer = DefaultTrueBooleanSerializer + + def test_enabled_as_false(self): + serializer = self.default_true_boolean_serializer(data={'cat': False, + 'dog': False}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) + + def test_enabled_as_true(self): + serializer = self.default_true_boolean_serializer(data={'cat': True, + 'dog': True}) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], True) + self.assertEqual(serializer.data['dog'], True) + + def test_enabled_partial(self): + serializer = self.default_true_boolean_serializer(data={'cat': False}, + partial=True) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data['cat'], False) + self.assertEqual(serializer.data['dog'], False) From 87b99d1ac8ce212dd0751f597e3279d80d4fcad1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:17:26 +0000 Subject: [PATCH 0327/1652] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 1ddd83519..54865053e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * JSON renderer now deals with objects that implement a dict-like interface. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. +* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. ### 2.3.10 From 193af483efca2aaaebc186301b88f5735c87e638 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:22:56 +0000 Subject: [PATCH 0328/1652] Add notes on lookup_prefix argument and why it's there even though unused by the default implementations. --- rest_framework/routers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 7915991d9..97b35c10a 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -188,6 +188,12 @@ class SimpleRouter(BaseRouter): """ Given a viewset, return the portion of URL regex that is used to match against a single instance. + + Note that lookup_prefix is not used directly inside REST rest_framework + itself, but is required in order to nicely support nested router + implementations, such as drf-nested-routers. + + https://github.com/alanjds/drf-nested-routers """ if self.trailing_slash: base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' From 39dbea4da43f829863d395d5f2ee158837f2afe2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 20:27:17 +0000 Subject: [PATCH 0329/1652] Links to drf-nested-routers --- docs/api-guide/relations.md | 11 +++++++++++ docs/api-guide/routers.md | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 556429bb9..1b089c541 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -442,7 +442,18 @@ In the 2.4 release, these parts of the API will be removed entirely. For more details see the [2.2 release announcement][2.2-announcement]. +--- + +# Third Party Packages + +The following third party packages are also available. + +## DRF Nested Routers + +The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. + [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 54fae65f5..895589db9 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -158,9 +158,6 @@ The following third party packages are also available. The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. -[cite]: http://guides.rubyonrails.org/routing.html -[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers - ## wq.db The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration. @@ -171,5 +168,6 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an app.router.register_model(MyModel) [cite]: http://guides.rubyonrails.org/routing.html +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db]: http://wq.io/wq.db [wq.db-router]: http://wq.io/docs/app.py From a87c55a93a7ca380bc49425cc6df00f4eba99aa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 21:57:07 +0000 Subject: [PATCH 0330/1652] Compat fixes for django-oauth-plus versions 2.0-2.2.1 --- rest_framework/authentication.py | 7 ++++--- rest_framework/compat.py | 15 +++++++++++++++ rest_framework/tests/test_authentication.py | 20 ++++++++++---------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index bca542ebb..e491ce5f9 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -9,7 +9,7 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import oauth, oauth_provider, oauth_provider_store -from rest_framework.compat import oauth2_provider, provider_now +from rest_framework.compat import oauth2_provider, provider_now, check_nonce from rest_framework.authtoken.models import Token @@ -281,8 +281,9 @@ class OAuthAuthentication(BaseAuthentication): """ Checks nonce of request, and return True if valid. """ - return oauth_provider_store.check_nonce(request, oauth_request, - oauth_request['oauth_nonce'], oauth_request['oauth_timestamp']) + oauth_nonce = oauth_request['oauth_nonce'] + oauth_timestamp = oauth_request['oauth_timestamp'] + return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp) class OAuth2Authentication(BaseAuthentication): diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 05bd99e0c..88211becb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -7,6 +7,7 @@ versions of django/python, and compatibility wrappers around optional packages. from __future__ import unicode_literals import django +import inspect from django.core.exceptions import ImproperlyConfigured from django.conf import settings @@ -536,9 +537,23 @@ except ImportError: try: import oauth_provider from oauth_provider.store import store as oauth_provider_store + + # check_nonce's calling signature in django-oauth-plus changes sometime + # between versions 2.0 and 2.2.1 + def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp): + check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args + if 'timestamp' in check_nonce_args: + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce, oauth_timestamp + ) + return oauth_provider_store.check_nonce( + request, oauth_request, oauth_nonce + ) + except (ImportError, ImproperlyConfigured): oauth_provider = None oauth_provider_store = None + check_nonce = None # OAuth 2 support is optional try: diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index fe11423dc..f072b81b7 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -249,7 +249,7 @@ class OAuthTests(TestCase): def setUp(self): # these imports are here because oauth is optional and hiding them in try..except block or compat # could obscure problems if something breaks - from oauth_provider.models import Consumer, Resource + from oauth_provider.models import Consumer, Scope from oauth_provider.models import Token as OAuthToken from oauth_provider import consts @@ -269,8 +269,8 @@ class OAuthTests(TestCase): self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET, name='example', user=self.user, status=self.consts.ACCEPTED) - self.resource = Resource.objects.create(name="resource name", url="api/") - self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource, + self.scope = Scope.objects.create(name="resource name", url="api/") + self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope, token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True ) @@ -398,10 +398,10 @@ class OAuthTests(TestCase): @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed') @unittest.skipUnless(oauth, 'oauth2 not installed') def test_get_form_with_readonly_resource_passing_auth(self): - """Ensure POSTing with a readonly resource instead of a write scope fails""" + """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.get('/oauth-with-scope/', params) self.assertEqual(response.status_code, 200) @@ -411,8 +411,8 @@ class OAuthTests(TestCase): def test_post_form_with_readonly_resource_failing_auth(self): """Ensure POSTing with a readonly resource instead of a write scope fails""" read_only_access_token = self.token - read_only_access_token.resource.is_readonly = True - read_only_access_token.resource.save() + read_only_access_token.scope.is_readonly = True + read_only_access_token.scope.save() params = self._create_authorization_url_parameters() response = self.csrf_client.post('/oauth-with-scope/', params) self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) @@ -422,8 +422,8 @@ class OAuthTests(TestCase): def test_post_form_with_write_resource_passing_auth(self): """Ensure POSTing with a write resource succeed""" read_write_access_token = self.token - read_write_access_token.resource.is_readonly = False - read_write_access_token.resource.save() + read_write_access_token.scope.is_readonly = False + read_write_access_token.scope.save() params = self._create_authorization_url_parameters() auth = self._create_authorization_header() response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth) From 54d3c6a725358ee5bb3e07450fb5efbaf957e1b7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 21:59:47 +0000 Subject: [PATCH 0331/1652] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 54865053e..f171b1f50 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. +* Fix compatiblity with newer versions of `django-oauth-plus`. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. * Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. From f78b3187df11925caf07f9d86eb868de906c60c8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 22:01:19 +0000 Subject: [PATCH 0332/1652] Added @philipforget for work on #1232. Thanks :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 1a838421d..d4c00bc4e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -181,6 +181,7 @@ The following people have helped make REST framework great. * Alex Good - [alexjg] * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] +* Philip Forget - [philipforget] Many thanks to everyone who's contributed to the project. @@ -398,3 +399,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexjg]: https://github.com/alexjg [ian-foote]: https://github.com/ian-foote [chuckharmston]: https://github.com/chuckharmston +[philipforget]: https://github.com/philipforget From 6b6b255684e8cfc25bf91168b46e9ab6512b800a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 14 Dec 2013 20:42:58 +0000 Subject: [PATCH 0333/1652] Add note on pagination bugfix. Closes #1293. --- docs/topics/release-notes.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f171b1f50..d1ace1164 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -89,6 +89,19 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. +* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*] + +--- + +[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. + + class DisablePaginationMixin(object): + def get_paginate_by(self, queryset=None): + if self.request.QUERY_PARAMS['self.paginate_by_param'] == '0': + return None + return super(DisablePaginationMixin, self).get_paginate_by(queryset) + +--- ### 2.3.7 From 69fef838cce33b9079640f83cc03edc30f56f5f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 14 Dec 2013 21:05:11 +0000 Subject: [PATCH 0334/1652] Update django-oauth-plus version --- optionals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optionals.txt b/optionals.txt index 4ebfceab4..96f4b2f44 100644 --- a/optionals.txt +++ b/optionals.txt @@ -2,6 +2,6 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-filter>=0.5.4 -django-oauth-plus>=2.0 +django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 From 4a134eefa2c3b71aa1dc6a4ec94716fe41dca8f5 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Mon, 16 Dec 2013 15:51:43 +1300 Subject: [PATCH 0335/1652] Fix expansion of writable nested serializers where the inner fields have source set. --- rest_framework/serializers.py | 4 +++- rest_framework/tests/test_serializer.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 40caa1f38..d9313342c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -422,7 +422,9 @@ class BaseSerializer(WritableField): if self.source == '*': if value: - into.update(value) + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 14d1c664e..7808ba1ae 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -105,6 +105,17 @@ class ModelSerializerWithNestedSerializer(serializers.ModelSerializer): model = Person +class NestedSerializerWithRenamedField(serializers.Serializer): + renamed_info = serializers.Field(source='info') + + +class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer): + nested = NestedSerializerWithRenamedField(source='*') + + class Meta: + model = Person + + class PersonSerializerInvalidReadOnly(serializers.ModelSerializer): """ Testing for #652. @@ -456,6 +467,20 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_with_inner_source_fields(self): + """ + Tests that a serializer with source="*" correctly expands the + it's fields into the outer serializer even if they have their + own 'source' parameters. + """ + + serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={ + 'name': 'marko', + 'nested': {'renamed_info': 'hi'}}, + ) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.errors, {}) + class CustomValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): From fc2dee844ab0ca77928f296f13777bf01d94e6fd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 08:59:10 +0000 Subject: [PATCH 0336/1652] Don't import compat.py from authtoken.models. Closes #1297 --- rest_framework/authtoken/models.py | 8 +++++++- rest_framework/compat.py | 7 ------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 7601f5b79..024f62bfe 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,11 +1,17 @@ import uuid import hmac from hashlib import sha1 -from rest_framework.compat import AUTH_USER_MODEL from django.conf import settings from django.db import models +# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist. +# Note that we don't perform this code in the compat module due to +# bug report #1297 +# See: https://github.com/tomchristie/django-rest-framework/issues/1297 +AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') + + class Token(models.Model): """ The default authorization token model. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 88211becb..b69749feb 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -104,13 +104,6 @@ def get_concrete_model(model_cls): return model_cls -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): - AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: - AUTH_USER_MODEL = 'auth.User' - - if django.VERSION >= (1, 5): from django.views.generic import View else: From 31dd160256a86c261099602dadb1163811a41e23 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 11:59:14 +0000 Subject: [PATCH 0337/1652] Typo --- docs/topics/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 906950bb5..30d292f80 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -10,7 +10,7 @@ There are many ways you can contribute to Django REST framework. We'd like it t The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. From f0f7a91b3157790aac8c568e3349dde8dca1fac5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 16 Dec 2013 11:59:37 +0000 Subject: [PATCH 0338/1652] Add CONTRIBUTING.md --- CONTRIBUTING.md | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..e0544a479 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,193 @@ +# Contributing to REST framework + +> The world can only really be changed one piece at a time. The art is picking that piece. +> +> — [Tim Berners-Lee][cite] + +There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. + +## Community + +The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. + +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. + +Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. + +When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. + +## Code of conduct + +Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. + +Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. + +The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. + +# Issues + +It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. + +Some tips on good issue reporting: + +* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. +* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. +* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. +* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. + +## Triaging issues + +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to + +* Read through the ticket - does it make sense, is it missing any context that would help explain it better? +* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? +* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? +* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. + +# Development + +To start developing on Django REST framework, clone the repo: + + git clone git@github.com:tomchristie/django-rest-framework.git + +Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. + +## Testing + +To run the tests, clone the repository, and then: + + # Setup the virtual environment + virtualenv env + env/bin/activate + pip install -r requirements.txt + pip install -r optionals.txt + + # Run the tests + rest_framework/runtests/runtests.py + +You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: + + tox + +## Pull requests + +It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. + +It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. + +It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. + +GitHub's documentation for working on pull requests is [available here][pull-requests]. + +Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. + +Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. + +![Travis status][travis-status] + +*Above: Travis build notifications* + +## Managing compatibility issues + +Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. + +# Documentation + +The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs]. + +There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended. + +## Building the documentation + +To build the documentation, simply run the `mkdocs.py` script. + + ./mkdocs.py + +This will build the html output into the `html` directory. + +You can build the documentation and open a preview in a browser window by using the `-p` flag. + + ./mkdocs.py -p + +## Language style + +Documentation should be in American English. The tone of the documentation is very important - try to stick to a simple, plain, objective and well-balanced style where possible. + +Some other tips: + +* Keep paragraphs reasonably short. +* Use double spacing after the end of sentences. +* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. + +## Markdown style + +There are a couple of conventions you should follow when working on the documentation. + +##### 1. Headers + +Headers should use the hash style. For example: + + ### Some important topic + +The underline style should not be used. **Don't do this:** + + Some important topic + ==================== + +##### 2. Links + +Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. + + Here is a link to [some other thing][other-thing]. + + More text... + + [other-thing]: http://example.com/other/thing + +This style helps keep the documentation source consistent and readable. + +If you are hyperlinking to another REST framework document, you should use a relative link, and link to the `.md` suffix. For example: + + [authentication]: ../api-guide/authentication.md + +Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages. + +##### 3. Notes + +If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: + + --- + + **Note:** A useful documentation note. + + --- + +# Third party packages + +New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. + +## Getting started + +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. + +We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. + +## Linking to your package + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. + +[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html +[code-of-conduct]: https://www.djangoproject.com/conduct/ +[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[so-filter]: http://stackexchange.com/filters/66475/rest-framework +[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open +[pep-8]: http://www.python.org/dev/peps/pep-0008/ +[travis-status]: https://raw.github.com/tomchristie/django-rest-framework/master/docs/img/travis-status.png +[pull-requests]: https://help.github.com/articles/using-pull-requests +[tox]: http://tox.readthedocs.org/en/latest/ +[markdown]: http://daringfireball.net/projects/markdown/basics +[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs +[mou]: http://mouapp.com/ +[django-reusable-app]: https://github.com/dabapps/django-reusable-app From 802648045476f91f1c4b6a9f507cc08625194c2c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 17 Dec 2013 10:30:23 +0100 Subject: [PATCH 0339/1652] Use the BytesIO for buffering bytes and import the one from the compat module. --- docs/tutorial/1-serialization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e1c0009c3..4d4e7258f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -183,9 +183,9 @@ At this point we've translated the model instance into Python native datatypes. Deserialization is similar. First we parse a stream into Python native datatypes... - import StringIO + from rest_framework.compat import BytesIO - stream = StringIO.StringIO(content) + stream = BytesIO(content) data = JSONParser().parse(stream) ...then we restore those native datatypes into to a fully populated object instance. From 02ae1682b5585581e88bbd996f7cb7fd22b146f7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Dec 2013 09:45:28 +0000 Subject: [PATCH 0340/1652] Add note on compat import in tutorial --- docs/tutorial/1-serialization.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 4d4e7258f..e015a545f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -183,6 +183,8 @@ At this point we've translated the model instance into Python native datatypes. Deserialization is similar. First we parse a stream into Python native datatypes... + # This import will use either `StringIO.StringIO` or `io.BytesIO` + # as appropriate, depending on if we're running Python 2 or Python 3. from rest_framework.compat import BytesIO stream = BytesIO(content) From 0e3822d6e0ea71d43ce7108b43184a6b3eba3f3a Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 16:53:06 +0100 Subject: [PATCH 0341/1652] Updated test class name to be unique --- rest_framework/tests/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9cd..ce7022cdf 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -52,7 +52,7 @@ class ShouldValidateModelSerializer(serializers.ModelSerializer): fields = ('renamed',) -class TestPreSaveValidationExclusions(TestCase): +class TestPreSaveValidationExclusionsSerializer(TestCase): def test_renamed_fields_are_model_validated(self): """ Ensure fields with 'source' applied do get still get model validation. From 71aa5f3c455db9bea5163dca70bfe6f3b3c924be Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 17:16:24 +0100 Subject: [PATCH 0342/1652] Added missing custom validation method test --- rest_framework/tests/test_validation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ebfdff9cd..371697f8f 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -47,6 +47,12 @@ class ShouldValidateModel(models.Model): class ShouldValidateModelSerializer(serializers.ModelSerializer): renamed = serializers.CharField(source='should_validate_field', required=False) + def validate_renamed(self, attrs, source): + value = attrs[source] + if len(value) < 3: + raise serializers.ValidationError('Minimum 3 characters.') + return attrs + class Meta: model = ShouldValidateModel fields = ('renamed',) @@ -63,6 +69,13 @@ class TestPreSaveValidationExclusions(TestCase): self.assertEqual(serializer.is_valid(), False) +class TestCustomValidationMethods(TestCase): + def test_custom_validation_method_is_executed(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) + self.assertFalse(serializer.is_valid()) + self.assertIn('renamed', serializer.errors) + + class ValidationSerializer(serializers.Serializer): foo = serializers.CharField() From 973f898a4b8042775a94d3e76188b43f13d493a8 Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 20 Dec 2013 17:45:56 +0100 Subject: [PATCH 0343/1652] Should it be that way? --- rest_framework/tests/test_validation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index ce7022cdf..ab75da252 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -61,6 +61,8 @@ class TestPreSaveValidationExclusionsSerializer(TestCase): # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) self.assertEqual(serializer.is_valid(), False) + self.assertIn('renamed', serializer.errors) + self.assertNotIn('should_validate_field', serializer.errors) class ValidationSerializer(serializers.Serializer): From 22343ee11764aac3686ad500da5c9aae30540e8e Mon Sep 17 00:00:00 2001 From: Vitaly Babiy Date: Sat, 21 Dec 2013 07:05:21 -0500 Subject: [PATCH 0344/1652] Added links to djangorestframework-camel-case in the third party sections of the docs for both parsers and renderers. --- docs/api-guide/parsers.md | 6 ++++++ docs/api-guide/renderers.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 1030fcb65..db0b666fb 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -186,9 +186,15 @@ The following third party packages are also available. [MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework. +## CamelCase JSON + +[djangorestframework-camel-case] provides a camelCase JSON parser for django REST framework, its maintained by [vbabiy] + [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers [messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack [juanriaza]: https://github.com/juanriaza +[vbabiy]: https://github.com/vbabiy [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack +[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index cf2005691..673b59025 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -419,6 +419,11 @@ Comma-separated values are a plain-text tabular data format, that can be easily [UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. +## CamelCase JSON + +[djangorestframework-camel-case] provides a camelCase JSON render for django REST framework, its maintained by [vbabiy] + + [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -435,8 +440,10 @@ Comma-separated values are a plain-text tabular data format, that can be easily [messagepack]: http://msgpack.org/ [juanriaza]: https://github.com/juanriaza [mjumbewu]: https://github.com/mjumbewu +[vbabiy]: https://github.com/vbabiy [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv [ultrajson]: https://github.com/esnme/ultrajson [hzy]: https://github.com/hzy [drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer +[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file From 1f3ded4559ad18d03ee49b3befd19ddaea7e70b2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 17:18:25 +0000 Subject: [PATCH 0345/1652] Docs tweaks --- docs/api-guide/parsers.md | 2 +- docs/api-guide/renderers.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index db0b666fb..72a4af643 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -188,7 +188,7 @@ The following third party packages are also available. ## CamelCase JSON -[djangorestframework-camel-case] provides a camelCase JSON parser for django REST framework, its maintained by [vbabiy] +[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy]. [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 673b59025..7798827bc 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -421,7 +421,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## CamelCase JSON -[djangorestframework-camel-case] provides a camelCase JSON render for django REST framework, its maintained by [vbabiy] +[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy]. [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process From bc0e994784f46330b4e849c3326fbd5c500298b3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:10:05 +0000 Subject: [PATCH 0346/1652] Added example of using APIException class. Closes #1300 --- docs/api-guide/exceptions.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index c46d415e4..221df679d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -88,6 +88,14 @@ The **base class** for all exceptions raised inside REST framework. To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class. +For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so: + + from rest_framework.exceptions import APIException + + class ServiceUnavailable(APIException): + status_code = 503 + detail = 'Service temporarily unavailable, try again later.' + ## ParseError **Signature:** `ParseError(detail=None)` From a439c80cd856f0661cb66003263c454c5108fe0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:21:53 +0000 Subject: [PATCH 0347/1652] Less brittle through relationship testing. Closes #1292. --- rest_framework/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 28a098805..8351b3df6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -713,7 +713,9 @@ class ModelSerializer(Serializer): is_m2m = isinstance(relation.field, models.fields.related.ManyToManyField) - if is_m2m and not relation.field.rel.through._meta.auto_created: + if (is_m2m and + hasattr(relation.field.rel, 'through') and + not relation.field.rel.through._meta.auto_created): has_through_model = True if nested: From 71ab7cda2a28b0df8df54c1446b4780413909acb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 21 Dec 2013 21:54:51 +0000 Subject: [PATCH 0348/1652] Additional test for 'source' behaviour. Refs #1302 --- rest_framework/tests/test_validation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 3fb6ba373..8a8187fa8 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -75,6 +75,10 @@ class TestCustomValidationMethods(TestCase): self.assertFalse(serializer.is_valid()) self.assertIn('renamed', serializer.errors) + def test_custom_validation_method_passing(self): + serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) + self.assertTrue(serializer.is_valid()) + class ValidationSerializer(serializers.Serializer): foo = serializers.CharField() From 2d6d725c2f7f2226f9287211e64037816f8f2cac Mon Sep 17 00:00:00 2001 From: amatellanes Date: Sun, 22 Dec 2013 12:39:47 +0100 Subject: [PATCH 0349/1652] Simplified some functions --- rest_framework/permissions.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index ab6655e7b..f24a51235 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -54,9 +54,7 @@ class IsAuthenticated(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_authenticated(): - return True - return False + return request.user and request.user.is_authenticated() class IsAdminUser(BasePermission): @@ -65,9 +63,7 @@ class IsAdminUser(BasePermission): """ def has_permission(self, request, view): - if request.user and request.user.is_staff: - return True - return False + return request.user and request.user.is_staff class IsAuthenticatedOrReadOnly(BasePermission): @@ -76,11 +72,9 @@ class IsAuthenticatedOrReadOnly(BasePermission): """ def has_permission(self, request, view): - if (request.method in SAFE_METHODS or - request.user and - request.user.is_authenticated()): - return True - return False + return (request.method in SAFE_METHODS or + request.user and + request.user.is_authenticated()) class DjangoModelPermissions(BasePermission): @@ -138,11 +132,9 @@ class DjangoModelPermissions(BasePermission): perms = self.get_required_permissions(request.method, model_cls) - if (request.user and + return (request.user and (request.user.is_authenticated() or not self.authenticated_users_only) and - request.user.has_perms(perms)): - return True - return False + request.user.has_perms(perms)) class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): From e5caf48a12e777df3e5801fa19d98f59b6453aa2 Mon Sep 17 00:00:00 2001 From: bennbollay Date: Sun, 22 Dec 2013 16:53:52 -0800 Subject: [PATCH 0350/1652] Change the page title to prioritize page content When many tab's are open (which is necessary for DRF's documentation), it becomes impossible to determine which tab contains which pieces of documentation. That they are all related is obvious by the use of a common icon, just not the specific page each tab is loaded to. This change inverts the order of the title to maintain as much useful context as possible on the tabbar. --- mkdocs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.py b/mkdocs.py index d1790168a..09c9dcc67 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -144,7 +144,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): if filename == 'index.md': main_title = 'Django REST framework - APIs made easy' else: - main_title = 'Django REST framework - ' + main_title + main_title = main_title + ' - Django REST framework' if relative_path == 'index.md': canonical_url = base_url From 80e9f0d64b0ace50d413eaccbf28a3b4ded75ed3 Mon Sep 17 00:00:00 2001 From: Yin Jifeng Date: Mon, 23 Dec 2013 11:02:07 +0800 Subject: [PATCH 0351/1652] fix url double quoted in Django 1.6 get_full_path returns unicode, so we use build_absolute_uri which returns iri_to_uri'ed one --- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/tests/test_templatetags.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/test_templatetags.py diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index e9c1cdd54..5c267ab36 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -144,7 +144,7 @@ def add_query_param(request, key, val): """ Add a query parameter to the current request url, and return the new url. """ - return replace_query_param(request.get_full_path(), key, val) + return replace_query_param(request.build_absolute_uri(), key, val) @register.filter diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py new file mode 100644 index 000000000..cbac768a3 --- /dev/null +++ b/rest_framework/tests/test_templatetags.py @@ -0,0 +1,18 @@ +# encoding: utf-8 +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.test import APIRequestFactory +from rest_framework.templatetags.rest_framework import add_query_param + +factory = APIRequestFactory() + + +class TemplateTagTests(TestCase): + + def test_add_query_param_with_non_latin_charactor(self): + request = factory.get("/?q=查询") + json_url = add_query_param(request, "format", "json") + self.assertIn(json_url, [ + "http://testserver/?format=json&q=%E6%9F%A5%E8%AF%A2", + "http://testserver/?q=%E6%9F%A5%E8%AF%A2&format=json", + ]) From d6806340e54408858da4b2dc991be99edd65df76 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 08:50:46 +0100 Subject: [PATCH 0352/1652] Simplified some examples in tutorial --- docs/tutorial/1-serialization.md | 6 ++---- docs/tutorial/2-requests-and-responses.md | 6 ++---- docs/tutorial/4-authentication-and-permissions.md | 7 ++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e015a545f..2298df59a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,8 +263,7 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -290,8 +289,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 7fa4f3e4a..603edd081 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index b472322a3..986f13ff3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,15 +163,12 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True - # Write permissions are only allowed to the owner of the snippet - return obj.owner == request.user + return request.method in permissions.SAFE_METHODS or obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 74f1cf635536ea99937954a11fa11531a832ebc2 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 08:56:34 +0100 Subject: [PATCH 0353/1652] Revert "Simplified some examples in tutorial" This reverts commit d6806340e54408858da4b2dc991be99edd65df76. --- docs/tutorial/1-serialization.md | 6 ++++-- docs/tutorial/2-requests-and-responses.md | 6 ++++-- docs/tutorial/4-authentication-and-permissions.md | 7 +++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 2298df59a..e015a545f 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,7 +263,8 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - return JSONResponse(serializer.errors, status=400) + else: + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -289,7 +290,8 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - return JSONResponse(serializer.errors, status=400) + else: + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 603edd081..7fa4f3e4a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,7 +59,8 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -84,7 +85,8 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 986f13ff3..b472322a3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,12 +163,15 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + # Write permissions are only allowed to the owner of the snippet - return request.method in permissions.SAFE_METHODS or obj.owner == request.user + return obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 2846ddb5d2ba84b3905d4dc0593afe3a0d4b2749 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Mon, 23 Dec 2013 09:06:03 +0100 Subject: [PATCH 0354/1652] Simplified some examples in tutorial --- docs/tutorial/1-serialization.md | 6 ++---- docs/tutorial/2-requests-and-responses.md | 6 ++---- docs/tutorial/4-authentication-and-permissions.md | 7 ++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index e015a545f..2298df59a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -263,8 +263,7 @@ The root of our API is going to be a view that supports listing all the existing if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data, status=201) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. @@ -290,8 +289,7 @@ We'll also need a view which corresponds to an individual snippet, and can be us if serializer.is_valid(): serializer.save() return JSONResponse(serializer.data) - else: - return JSONResponse(serializer.errors, status=400) + return JSONResponse(serializer.errors, status=400) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 7fa4f3e4a..603edd081 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -59,8 +59,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. @@ -85,8 +84,7 @@ Here is the view for an individual snippet, in the `views.py` module. if serializer.is_valid(): serializer.save() return Response(serializer.data) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': snippet.delete() diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index b472322a3..986f13ff3 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,15 +163,12 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: - return True - # Write permissions are only allowed to the owner of the snippet - return obj.owner == request.user + return request.method in permissions.SAFE_METHODS or obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From d8a95b4b6d4480089d38808b45a7b47f30e81cdd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 09:12:34 +0000 Subject: [PATCH 0355/1652] Back out permissions example change in favor of easier to follow example --- docs/tutorial/4-authentication-and-permissions.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 986f13ff3..bdc6b5791 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -163,12 +163,15 @@ In the snippets app, create a new file, `permissions.py` """ Custom permission to only allow owners of an object to edit it. """ - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. - # Write permissions are only allowed to the owner of the snippet - return request.method in permissions.SAFE_METHODS or obj.owner == request.user + if request.method in permissions.SAFE_METHODS: + return True + + # Write permissions are only allowed to the owner of the snippet. + return obj.owner == request.user Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: From 3f5e3c28f5a4f8b12f5f3ae6c7b571d08be2bf7e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 11:55:25 +0000 Subject: [PATCH 0356/1652] Updated tests to pass in python 3 --- rest_framework/templatetags/rest_framework.py | 5 ++++- rest_framework/tests/test_templatetags.py | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 5c267ab36..83c046f99 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import from django import template from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict +from django.utils.encoding import iri_to_uri from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe from rest_framework.compat import urlparse, force_text, six, smart_urlquote @@ -144,7 +145,9 @@ def add_query_param(request, key, val): """ Add a query parameter to the current request url, and return the new url. """ - return replace_query_param(request.build_absolute_uri(), key, val) + iri = request.get_full_path() + uri = iri_to_uri(iri) + return replace_query_param(uri, key, val) @register.filter diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index cbac768a3..7ac90ae60 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -10,9 +10,10 @@ factory = APIRequestFactory() class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_charactor(self): - request = factory.get("/?q=查询") + # Ensure we don't double-escape non-latin characters + # that are present in the querystring. + # https://github.com/tomchristie/django-rest-framework/pull/1314 + request = factory.get("/", {'q': '查询'}) json_url = add_query_param(request, "format", "json") - self.assertIn(json_url, [ - "http://testserver/?format=json&q=%E6%9F%A5%E8%AF%A2", - "http://testserver/?q=%E6%9F%A5%E8%AF%A2&format=json", - ]) + self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) + self.assertIn("format=json") From bed2f08c24a13831590ae5fc8cefbb1bca300a96 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 11:57:25 +0000 Subject: [PATCH 0357/1652] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d1ace1164..b09bd0bea 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -46,6 +46,7 @@ You can determine your currently installed version using `pip freeze`: * Fix compatiblity with newer versions of `django-oauth-plus`. * Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. * Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. +* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params. ### 2.3.10 From feddd16c54c99977bd5503fd09232828c280fc10 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 12:04:17 +0000 Subject: [PATCH 0358/1652] Tweak test style --- rest_framework/tests/test_templatetags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 7ac90ae60..609a9e089 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -12,8 +12,8 @@ class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_charactor(self): # Ensure we don't double-escape non-latin characters # that are present in the querystring. - # https://github.com/tomchristie/django-rest-framework/pull/1314 + # See #1314. request = factory.get("/", {'q': '查询'}) json_url = add_query_param(request, "format", "json") self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) - self.assertIn("format=json") + self.assertIn("format=json", json_url) From d24ea39a4e4131486d45492339dcbbfefb6a933b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 14:29:22 +0000 Subject: [PATCH 0359/1652] Added note on view_name in hyperlinked relationships. Closes #1221 --- docs/api-guide/relations.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 1b089c541..4bee75af7 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -134,7 +134,7 @@ By default this field is read-write, although you can change this behavior using **Arguments**: -* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `-detail`. **required**. * `many` - If applied to a to-many relationship, you should set this argument to `True`. * `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. @@ -202,7 +202,7 @@ This field is always read-only. **Arguments**: -* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this wil be a string with the format `-detail`. **required**. * `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. @@ -454,6 +454,7 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward +[routers]: http://django-rest-framework.org/api-guide/routers#defaultrouter [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers From 75e872473197f9b810c9daf348cb452faadac476 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 23 Dec 2013 14:38:51 +0000 Subject: [PATCH 0360/1652] Fuller notes on the 'base_name' argument. Closes #1160. --- docs/api-guide/routers.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 895589db9..7efc140a5 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -37,6 +37,18 @@ The example above would generate the following URL patterns: * URL pattern: `^accounts/$` Name: `'account-list'` * URL pattern: `^accounts/{pk}/$` Name: `'account-detail'` +--- + +**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part. + +Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have any `.model` or `.queryset` attribute set. If you try to register that viewset you'll see an error like this: + + 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.model' or '.queryset' attribute. + +This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name. + +--- + ### Extra link and actions Any methods on the viewset decorated with `@link` or `@action` will also be routed. From 25bd6d1d4b7a85279047ab8e35f6faee0bc10a1a Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Mon, 23 Dec 2013 22:27:40 -0600 Subject: [PATCH 0361/1652] can't save genericrelations via nested serializers in django 1.6 --- rest_framework/tests/test_genericrelations.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index c38bfb9f3..2d3413444 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -69,6 +69,35 @@ class TestGenericRelations(TestCase): } self.assertEqual(serializer.data, expected) + def test_generic_nested_relation(self): + """ + Test saving a GenericRelation field via a nested serializer. + """ + + class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + exclude = ('content_type', 'object_id') + + class BookmarkSerializer(serializers.ModelSerializer): + tags = TagSerializer() + + class Meta: + model = Bookmark + exclude = ('id',) + + data = { + 'url': 'https://docs.djangoproject.com/', + 'tags': [ + {'tag': 'contenttypes'}, + {'tag': 'genericrelations'}, + ] + } + serializer = BookmarkSerializer(data=data) + self.assertTrue(serializer.is_valid()) + serializer.save() + self.assertEqual(serializer.object.tags.count(), 2) + def test_generic_fk(self): """ Test a relationship that spans a GenericForeignKey field. From d30ce2575c6b3901f15eb96eeaf66cc65e1d298b Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Mon, 23 Dec 2013 22:31:31 -0600 Subject: [PATCH 0362/1652] fix for genericrelation saving --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8351b3df6..c0c810ab9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -894,7 +894,7 @@ class ModelSerializer(Serializer): m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many: + for field in meta.many_to_many + meta.virtual_fields: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) From 1f3f2741f519967aa236cd861a79c2c459063197 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 2 Jan 2014 09:28:34 +0000 Subject: [PATCH 0363/1652] Happy new year --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 684868006..8c6822312 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou # License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/index.md b/docs/index.md index 04804fa79..7688d4281 100644 --- a/docs/index.md +++ b/docs/index.md @@ -219,7 +219,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou ## License -Copyright (c) 2011-2013, Tom Christie +Copyright (c) 2011-2014, Tom Christie All rights reserved. Redistribution and use in source and binary forms, with or without From 0672d6de6e47ba0269a58ad0da3cc7ff4c82908e Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 2 Jan 2014 16:46:57 -0500 Subject: [PATCH 0364/1652] Fix bugfix note This fixes a bugfix note that was added because of #1293, which pointed out that a change in a bugfix [1] broke backwards compatibility. The bugfix did not work as expected because a variable was quoted when it should not have been. This removes the quotes around the variable, which brings back the expected functionality. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b09bd0bea..ca966d20e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -98,7 +98,7 @@ You can determine your currently installed version using `pip freeze`: class DisablePaginationMixin(object): def get_paginate_by(self, queryset=None): - if self.request.QUERY_PARAMS['self.paginate_by_param'] == '0': + if self.request.QUERY_PARAMS[self.paginate_by_param] == '0': return None return super(DisablePaginationMixin, self).get_paginate_by(queryset) From e032bad1a7c1f9ad2d9c591617ea92b2e802d7e5 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Thu, 2 Jan 2014 16:54:06 -0500 Subject: [PATCH 0365/1652] FIx link to tox --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0544a479..a7aa6fc40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,7 @@ To run the tests, clone the repository, and then: # Run the tests rest_framework/runtests/runtests.py -You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: +You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: tox From e020c51b44b9acecdb01cc90f2d6da977ba5ea0f Mon Sep 17 00:00:00 2001 From: Steven Cummings Date: Thu, 2 Jan 2014 17:18:08 -0600 Subject: [PATCH 0366/1652] FIX BaseSerializer.from_native has an altered signature * base classes define it with one parameter * BaseSerializer currently defines a second parameter, which we make optional here for method-dispatch passivity --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c0c810ab9..b22ca5783 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -331,7 +331,7 @@ class BaseSerializer(WritableField): return ret - def from_native(self, data, files): + def from_native(self, data, files=None): """ Deserialize primitives -> objects. """ From 3050f0e82a60a12dc35ef7947c2f47de12387919 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Jan 2014 13:06:41 +0000 Subject: [PATCH 0367/1652] Frontpage tweaks --- README.md | 6 +++--- docs/img/logo.png | Bin 0 -> 45955 bytes docs/index.md | 26 +++++++++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 docs/img/logo.png diff --git a/README.md b/README.md index 8c6822312..403f9c0af 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# Django REST framework - -**Awesome web-browseable Web APIs.** + [![build-status-image]][travis] +**Awesome web-browseable Web APIs.** + **Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs]. # Overview diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0f0cf800d316496efbd89f55365bacad00f0ef96 GIT binary patch literal 45955 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 + -# Django REST framework +--- -**Awesome web-browsable Web APIs.** +

    + +

    + + Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. @@ -20,13 +26,16 @@ Some reasons you might want to use REST framework: * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. * [Extensive documentation][index], and [great community support][group]. +* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite]. -There is a live example API for testing purposes, [available here][sandbox]. - -**Below**: *Screenshot from the browsable API* +--- ![Screenshot][image] +**Above**: *Screenshot from the browsable API* + +---- + ## Requirements REST framework requires the following: @@ -140,6 +149,8 @@ The tutorial will walk you through the building blocks that make up REST framewo * [5 - Relationships & hyperlinked APIs][tut-5] * [6 - Viewsets & routers][tut-6] +There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox]. + ## API Guide The API guide is your complete reference manual to all the functionality provided by REST framework. @@ -244,7 +255,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master -[urlobject]: https://github.com/zacharyvoase/urlobject +[mozilla]: http://www.mozilla.org/en-US/about/ +[eventbrite]: https://www.eventbrite.co.uk/about/ [markdown]: http://pypi.python.org/pypi/Markdown/ [yaml]: http://pypi.python.org/pypi/PyYAML [defusedxml]: https://pypi.python.org/pypi/defusedxml From 442916b9649baaf305ff094fe05f026ad04c7818 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Jan 2014 13:24:52 +0000 Subject: [PATCH 0368/1652] Link to BrightAPI, and remove ad except from frontpage --- docs/template.html | 23 ++--------------------- mkdocs.py | 6 ++++++ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/docs/template.html b/docs/template.html index c065237a5..a397d067b 100644 --- a/docs/template.html +++ b/docs/template.html @@ -170,31 +170,12 @@ + - -

    @@ -7,9 +7,15 @@

    diff --git a/mkdocs.py b/mkdocs.py index 09c9dcc67..92679a21e 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -161,6 +161,12 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = output.replace('{{ page_id }}', filename[:-3]) output = output.replace('{{ canonical_url }}', canonical_url) + if filename =='index.md': + output = output.replace('{{ ad_block }}', """

    The team behind REST framework is launching a new API service.

    +

    If you want to be first in line when we start issuing invitations, please sign up here.

    """) + else: + output = output.replace('{{ ad_block }}', '') + if prev_url: output = output.replace('{{ prev_url }}', prev_url) output = output.replace('{{ prev_url_disabled }}', '') From a1d7aa8f712b659f9d8302a2d2a098d2538e6c89 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Thu, 2 Jan 2014 17:44:47 -0500 Subject: [PATCH 0369/1652] Allow viewset to specify lookup value regex for routing This patch allows a viewset to define a pattern for its lookup field, which the router will honor. Without this patch, any characters are allowed in the lookup field, and overriding this behavior requires subclassing router and copying and pasting the implementation of get_lookup_regex. It's possible it would be better to remove this functionality from the routers and simply expose a parameter to get_lookup_regex which allows overriding the lookup_regex. That way the viewset config logic could be in the a subclass, which could invoke the super method directly. I'm using this now for PostgreSQL UUID fields using https://github.com/dcramer/django-uuidfield . Without this patch, that field passes the lookup string to the database driver, which raises a DataError to complain about the invalid UUID. It's possible the field ought to signal this error in a different way, which could obviate the need to specify a pattern. --- docs/api-guide/routers.md | 6 ++++++ rest_framework/routers.py | 20 ++++++++++++++------ rest_framework/tests/test_routers.py | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 846ac9f9d..f3beabdd3 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -83,6 +83,12 @@ This behavior can be modified by setting the `trailing_slash` argument to `False Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style. +With `trailing_slash` set to True, the router will match lookup values containing any characters except slashes and dots. When set to False, dots are allowed. To restrict the lookup pattern, set the `lookup_field_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs: + + class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + lookup_field = 'my_model_id' + lookup_value_regex = '[0-9a-f]{32}' + ## DefaultRouter This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes. diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 740d58f0d..8766ecb2f 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -219,13 +219,21 @@ class SimpleRouter(BaseRouter): https://github.com/alanjds/drf-nested-routers """ - if self.trailing_slash: - base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/]+)' - else: - # Don't consume `.json` style suffixes - base_regex = '(?P<{lookup_prefix}{lookup_field}>[^/.]+)' + base_regex = '(?P<{lookup_prefix}{lookup_field}>{lookup_value})' lookup_field = getattr(viewset, 'lookup_field', 'pk') - return base_regex.format(lookup_field=lookup_field, lookup_prefix=lookup_prefix) + try: + lookup_value = viewset.lookup_value_regex + except AttributeError: + if self.trailing_slash: + lookup_value = '[^/]+' + else: + # Don't consume `.json` style suffixes + lookup_value = '[^/.]+' + return base_regex.format( + lookup_prefix=lookup_prefix, + lookup_field=lookup_field, + lookup_value=lookup_value + ) def get_urls(self): """ diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 1c34648f4..0f6d62c7c 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -121,6 +121,27 @@ class TestCustomLookupFields(TestCase): ) +class TestLookupValueRegex(TestCase): + """ + Ensure the router honors lookup_value_regex when applied + to the viewset. + """ + def setUp(self): + class NoteViewSet(viewsets.ModelViewSet): + queryset = RouterTestModel.objects.all() + lookup_field = 'uuid' + lookup_value_regex = '[0-9a-f]{32}' + + self.router = SimpleRouter() + self.router.register(r'notes', NoteViewSet) + self.urls = self.router.urls + + def test_urls_limited_by_lookup_value_regex(self): + expected = ['^notes/$', '^notes/(?P[0-9a-f]{32})/$'] + for idx in range(len(expected)): + self.assertEqual(expected[idx], self.urls[idx].regex.pattern) + + class TestTrailingSlashIncluded(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): From 3cd15fb1713dfc49e1bf1fd48045ca3ae5654e18 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sat, 4 Jan 2014 16:57:50 -0500 Subject: [PATCH 0370/1652] Router: Do not automatically adjust lookup_regex when trailing_slash is True BREAKING CHANGE When trailing_slash is set to True, the router no longer will adjust the lookup regex to allow it to include periods. To simulate the old behavior, the programmer should specify `lookup_regex = '[^/]+'` on the viewset. https://github.com/tomchristie/django-rest-framework/pull/1328#issuecomment-31517099 --- docs/api-guide/routers.md | 2 +- rest_framework/routers.py | 7 ++----- rest_framework/tests/test_routers.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index f3beabdd3..6b4ae6dbd 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -83,7 +83,7 @@ This behavior can be modified by setting the `trailing_slash` argument to `False Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style. -With `trailing_slash` set to True, the router will match lookup values containing any characters except slashes and dots. When set to False, dots are allowed. To restrict the lookup pattern, set the `lookup_field_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs: +The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_field_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs: class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): lookup_field = 'my_model_id' diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 8766ecb2f..df1233fdd 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -224,11 +224,8 @@ class SimpleRouter(BaseRouter): try: lookup_value = viewset.lookup_value_regex except AttributeError: - if self.trailing_slash: - lookup_value = '[^/]+' - else: - # Don't consume `.json` style suffixes - lookup_value = '[^/.]+' + # Don't consume `.json` style suffixes + lookup_value = '[^/.]+' return base_regex.format( lookup_prefix=lookup_prefix, lookup_field=lookup_field, diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 0f6d62c7c..e41da57f5 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -152,7 +152,7 @@ class TestTrailingSlashIncluded(TestCase): self.urls = self.router.urls def test_urls_have_trailing_slash_by_default(self): - expected = ['^notes/$', '^notes/(?P[^/]+)/$'] + expected = ['^notes/$', '^notes/(?P[^/.]+)/$'] for idx in range(len(expected)): self.assertEqual(expected[idx], self.urls[idx].regex.pattern) From 899381575a6038f550a064261ed5c6ba0655211b Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Sat, 4 Jan 2014 17:03:01 -0500 Subject: [PATCH 0371/1652] Fix a typo --- docs/api-guide/routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 6b4ae6dbd..249e99a48 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -83,7 +83,7 @@ This behavior can be modified by setting the `trailing_slash` argument to `False Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style. -The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_field_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs: +The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs: class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): lookup_field = 'my_model_id' From e3ae33017d86bed7fdbf6c76e0129f9361cab04d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 6 Jan 2014 15:01:45 +0000 Subject: [PATCH 0372/1652] Added "nofollow" against docs link. --- rest_framework/templates/rest_framework/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 495163b64..ba45b9bcb 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -33,7 +33,7 @@ @@ -176,9 +176,9 @@ - - - + + + diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 0f980e1cb..a997c7829 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ [python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy -[credits]: http://django-rest-framework.org/topics/credits +[credits]: http://www.django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [marcgibbons]: https://github.com/marcgibbons/ diff --git a/mkdocs.py b/mkdocs.py index 92679a21e..f973096f3 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -18,7 +18,7 @@ if local: suffix = '.html' index = 'index.html' else: - base_url = 'http://django-rest-framework.org' + base_url = 'http://www.django-rest-framework.org' suffix = '' index = '' diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index ba45b9bcb..d19d5a2be 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -33,7 +33,7 @@ - - {% block footer %} - {% endblock %} - - {% block script %} - - - - - {% endblock %} - + {% block script %} + + + + + {% endblock %} + From 4d582fd9ffcb6ec28247e95b63134c00cc131780 Mon Sep 17 00:00:00 2001 From: Cezar Pendarovski Date: Fri, 22 Aug 2014 10:12:52 +0200 Subject: [PATCH 0714/1652] Made all color declarations in bootstrap-tweaks.css consistent --- .../static/rest_framework/css/bootstrap-tweaks.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index e5cc65ab7..6fa1e6cb5 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -42,7 +42,7 @@ a single block in the template. } .nav-list > .active > a, .nav-list > .active > a:hover { - background: #2c2c2c; + background: #2C2C2C; } .navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li { @@ -50,8 +50,8 @@ a single block in the template. } .navbar .navbar-inner .dropdown-menu li a:hover { - background: #eeeeee; - color: #c20000; + background: #EEEEEE; + color: #C20000; } /*=== dabapps bootstrap styles ====*/ @@ -151,7 +151,7 @@ footer { footer p { text-align: center; color: gray; - border-top: 1px solid #DDD; + border-top: 1px solid #DDDDDD; padding-top: 10px; } From 8e3f7700f6ed6fbe1338afc68e27c06d702fac8d Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 25 Aug 2014 12:29:10 +0200 Subject: [PATCH 0715/1652] docs: added drf-hstore to third party fields added a reference to django-rest-framework-hstore in docs/api-guide/fields.md --- docs/api-guide/fields.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 95d9fad33..bfbff2adb 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -366,6 +366,9 @@ The [drf-extra-fields][drf-extra-fields] package provides extra serializer field The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer. +## django-rest-framework-hstore + +The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS @@ -376,3 +379,5 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog [drf-compound-fields]: http://drf-compound-fields.readthedocs.org [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis +[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore +[django-hstore]: https://github.com/djangonauts/django-hstore From eb81c55d16435537ff7801839cc4d2704526914f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 25 Aug 2014 12:32:44 +0200 Subject: [PATCH 0716/1652] docs: added HStoreSerializer to third party serializers added a reference to django-rest-framework-hstore in docs/api-guide/serializers.md --- docs/api-guide/serializers.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 29b7851b6..a3694510e 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -594,7 +594,13 @@ The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEn The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations. +## HStoreSerializer + +The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature. + [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md [mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis +[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore +[django-hstore]: https://github.com/djangonauts/django-hstore From 19076510f4039aa6886854679e21352fffb5354f Mon Sep 17 00:00:00 2001 From: Nathan Hubbell Date: Tue, 26 Aug 2014 17:31:08 -0700 Subject: [PATCH 0717/1652] Update generic-views.md --- 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 e9efe7092..48b77a6e3 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -49,7 +49,7 @@ For more complex cases you might also want to override various methods on the vi serializer = UserSerializer(queryset, many=True) return Response(serializer.data) -For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry. +For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something like the following entry: url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list') From e5d88a80a97e6575c8b2a2ac40cffca6abdf1227 Mon Sep 17 00:00:00 2001 From: Cezar Pendarovski Date: Wed, 27 Aug 2014 09:41:33 +0200 Subject: [PATCH 0718/1652] Put all TextNodes (method names) back to same line with parent element --- .../templates/rest_framework/base.html | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index a19101e39..980cee0c5 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -83,9 +83,7 @@
    - GET - + rel="nofollow" title="Make a GET request on the {{ name }} resource">GET + title="Make an OPTIONS request on the {{ name }} resource">OPTIONS {% endif %} @@ -124,9 +120,7 @@ {% csrf_token %} + title="Make a DELETE request on the {{ name }} resource">DELETE {% endif %} @@ -172,9 +166,7 @@ {{ post_form }}
    + title="Make a POST request on the {{ name }} resource">POST
    @@ -188,9 +180,7 @@ {% include "rest_framework/raw_data_form.html" %}
    + title="Make a POST request on the {{ name }} resource">POST
    @@ -222,9 +212,7 @@
    + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT
    @@ -239,16 +227,12 @@ {% if raw_data_put_form %} + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT {% endif %} {% if raw_data_patch_form %} + value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH {% endif %}
    From be21cafd2bbca36bcf9e55b1565ba57c9e3f76d6 Mon Sep 17 00:00:00 2001 From: Nathan Hubbell Date: Wed, 27 Aug 2014 17:57:40 -0700 Subject: [PATCH 0719/1652] Update generic-views.md Small grammar changes. --- docs/api-guide/generic-views.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 48b77a6e3..2ceb2d573 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -101,7 +101,7 @@ Returns the queryset that should be used for list views, and that should be used This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests. -May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request. +May be overridden to provide dynamic behavior, such as returning a queryset, that is specific to the user making the request. For example: @@ -113,7 +113,7 @@ For example: Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset. -May be overridden to provide more complex behavior such as object lookups based on more than one URL kwarg. +May be overridden to provide more complex behavior, such as object lookups based on more than one URL kwarg. For example: @@ -133,7 +133,7 @@ Note that if your API doesn't include any object level permissions, you may opti Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute. -May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria. +May be overridden to provide more complex behavior with filters, such as using different (or even exlusive) lists of filter_backends depending on different criteria. For example: @@ -149,7 +149,7 @@ For example: Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. -May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users. +May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users. For example: @@ -162,7 +162,7 @@ For example: Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set. -You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response. +You may want to override this method to provide more complex behavior, such as modifying page sizes based on the media type of the response. For example: @@ -204,7 +204,7 @@ You won't typically need to override the following methods, although you might n # Mixins -The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior. +The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior. ## ListModelMixin From 53808892752f7bc0ff4735a8160e9975baf2a56c Mon Sep 17 00:00:00 2001 From: Cezar Pendarovski Date: Thu, 28 Aug 2014 10:39:01 +0200 Subject: [PATCH 0720/1652] Validation errors in the rendered HTML fixed --- rest_framework/templates/rest_framework/base.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 980cee0c5..cee9724d5 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -33,7 +33,7 @@ From 49a493f61f4d5b4447d139d189d78995e65231b2 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Wed, 29 Oct 2014 20:54:31 +0000 Subject: [PATCH 1122/1652] Bring back the promo section --- docs/theme/base.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/theme/base.html b/docs/theme/base.html index e5ad43777..7ca7de145 100644 --- a/docs/theme/base.html +++ b/docs/theme/base.html @@ -191,7 +191,8 @@ a.fusion-poweredby {
  • {{ toc_item.title }}
  • {% endfor %}
    - {{ ad_block }} +
    +
    From 3c49b9fe4648d6fb291f0d34bf2e9ef4b72d45be Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Wed, 29 Oct 2014 21:03:00 +0000 Subject: [PATCH 1123/1652] Add next and previous page. --- docs/theme/base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/theme/base.html b/docs/theme/base.html index 7ca7de145..0f876cc16 100644 --- a/docs/theme/base.html +++ b/docs/theme/base.html @@ -57,8 +57,8 @@ a.fusion-poweredby {
    - {% include "content.html" %} + {% if meta.source %} + {% for filename in meta.source %} + + {{ filename }} + + {% endfor %} + {% endif %} + + {{ content }}
    From 3bfc82068b068defee470910a850757e4c12df3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 31 Oct 2014 11:38:27 -0400 Subject: [PATCH 1134/1652] Update tabbing and cleanup theme templates --- docs_theme/404.html | 197 +++++++++++++++++++---------------- docs_theme/base.html | 241 +++++++++++++++++++++++-------------------- docs_theme/nav.html | 51 +++++---- 3 files changed, 258 insertions(+), 231 deletions(-) diff --git a/docs_theme/404.html b/docs_theme/404.html index 864247e78..44993e37d 100644 --- a/docs_theme/404.html +++ b/docs_theme/404.html @@ -1,50 +1,54 @@ - - - Django REST framework - 404 - Page not found - - - - - - - - - - + + + + Django REST framework - 404 - Page not found + + + + + - - + + + + + - + - - - + + + + + + + + + -
    - +
    + + - - - - - - + + + + - // Dynamically force sidenav to no higher than browser window - $('.side-nav').css('max-height', window.innerHeight - 130); - - $(function(){ - $(window).resize(function(){ - $('.side-nav').css('max-height', window.innerHeight - 130); - }); - }); - - + diff --git a/docs_theme/base.html b/docs_theme/base.html index 67290df60..544e21881 100644 --- a/docs_theme/base.html +++ b/docs_theme/base.html @@ -1,56 +1,62 @@ - - - {{ page_title }} - - - - - - - - - - + + + + {{ page_title }} + + + + + - - + + + + + - - - + +{# TODO: This is a bit of a hack. We don't want to refer to the file specifically. #} -a.fusion-poweredby { - display: block; - margin-top: 10px; -} -@media (max-width: 767px) { - div.promo {display: none;} -} - - - {# TODO: This is a bit of a hack. We don't want to refer to the file specifically. #} - +
    @@ -59,32 +65,34 @@ a.fusion-poweredby {
    - - + +
    @@ -98,18 +106,16 @@ a.fusion-poweredby {
    @@ -127,42 +133,51 @@ a.fusion-poweredby { {% endif %} {{ content }} -
    -
    -
    -
    - -
    -
    + + + + + + + + +
    + + - - - - - + + + + + - + - // Dynamically force sidenav to no higher than browser window - $('.side-nav').css('max-height', window.innerHeight - 130); - - $(function(){ - $(window).resize(function(){ - $('.side-nav').css('max-height', window.innerHeight - 130); - }); - }); - - + diff --git a/docs_theme/nav.html b/docs_theme/nav.html index a7a72d68e..87e197b35 100644 --- a/docs_theme/nav.html +++ b/docs_theme/nav.html @@ -1,11 +1,10 @@ - From 06683b86b2b15153df52fe481b5c4eeb489a80cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 31 Oct 2014 13:02:11 -0400 Subject: [PATCH 1135/1652] Use single quotes for consistency Conflicts: mkdocs.yml --- mkdocs.yml | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2d1886a05..c70de9822 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,30 +13,30 @@ pages: - ['tutorial/4-authentication-and-permissions.md', ] - ['tutorial/5-relationships-and-hyperlinked-apis.md', ] - ['tutorial/6-viewsets-and-routers.md', ] - - ['api-guide/requests.md', "API Guide", ] - - ['api-guide/responses.md', "API Guide", ] - - ['api-guide/views.md', "API Guide", ] - - ['api-guide/generic-views.md', "API Guide", ] - - ['api-guide/viewsets.md', "API Guide", ] - - ['api-guide/routers.md', "API Guide", ] - - ['api-guide/parsers.md', "API Guide", ] - - ['api-guide/renderers.md', "API Guide", ] - - ['api-guide/serializers.md', "API Guide", ] - - ['api-guide/fields.md', "API Guide", ] - - ['api-guide/relations.md', "API Guide", ] - - ['api-guide/validators.md', "API Guide", ] - - ['api-guide/authentication.md', "API Guide", ] - - ['api-guide/permissions.md', "API Guide", ] - - ['api-guide/throttling.md', "API Guide", ] - - ['api-guide/filtering.md', "API Guide", ] - - ['api-guide/pagination.md', "API Guide", ] - - ['api-guide/content-negotiation.md', "API Guide", ] - - ['api-guide/format-suffixes.md', "API Guide", ] - - ['api-guide/reverse.md', "API Guide", ] - - ['api-guide/exceptions.md', "API Guide", ] - - ['api-guide/status-codes.md', "API Guide", ] - - ['api-guide/testing.md', "API Guide", ] - - ['api-guide/settings.md', "API Guide", ] + - ['api-guide/requests.md', 'API Guide', ] + - ['api-guide/responses.md', 'API Guide', ] + - ['api-guide/views.md', 'API Guide', ] + - ['api-guide/generic-views.md', 'API Guide', ] + - ['api-guide/viewsets.md', 'API Guide', ] + - ['api-guide/routers.md', 'API Guide', ] + - ['api-guide/parsers.md', 'API Guide', ] + - ['api-guide/renderers.md', 'API Guide', ] + - ['api-guide/serializers.md', 'API Guide', ] + - ['api-guide/fields.md', 'API Guide', ] + - ['api-guide/relations.md', 'API Guide', ] + - ['api-guide/validators.md', 'API Guide', ] + - ['api-guide/authentication.md', 'API Guide', ] + - ['api-guide/permissions.md', 'API Guide', ] + - ['api-guide/throttling.md', 'API Guide', ] + - ['api-guide/filtering.md', 'API Guide', ] + - ['api-guide/pagination.md', 'API Guide', ] + - ['api-guide/content-negotiation.md', 'API Guide', ] + - ['api-guide/format-suffixes.md', 'API Guide', ] + - ['api-guide/reverse.md', 'API Guide', ] + - ['api-guide/exceptions.md', 'API Guide', ] + - ['api-guide/status-codes.md', 'API Guide', ] + - ['api-guide/testing.md', 'API Guide', ] + - ['api-guide/settings.md', 'API Guide', ] - ['topics/documenting-your-api.md', ] - ['topics/ajax-csrf-cors.md', ] - ['topics/browser-enhancements.md', ] From 200e0b17daecd07de6d1f9926a430d29b3ee948f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 31 Oct 2014 13:03:39 -0400 Subject: [PATCH 1136/1652] Clean up extra white space --- docs/topics/2.2-announcement.md | 8 ++++---- docs/topics/2.3-announcement.md | 8 ++++---- docs/topics/documenting-your-api.md | 8 ++++---- docs/topics/kickstarter-announcement.md | 2 +- docs/topics/release-notes.md | 2 +- docs/topics/rest-framework-2-announcement.md | 4 ++-- docs/topics/writable-nested-serializers.md | 10 +++++----- docs/tutorial/1-serialization.md | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index a997c7829..1df52cff2 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -42,7 +42,7 @@ The 2.2 release makes a few changes to the API, in order to make it more consist The `ManyRelatedField()` style is being deprecated in favor of a new `RelatedField(many=True)` syntax. -For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following: +For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following: class UserSerializer(serializers.HyperlinkedModelSerializer): questions = serializers.PrimaryKeyRelatedField(many=True) @@ -58,10 +58,10 @@ The change also applies to serializers. If you have a nested serializer, you sh class Meta: model = Track fields = ('name', 'duration') - + class AlbumSerializer(serializer.ModelSerializer): tracks = TrackSerializer(many=True) - + class Meta: model = Album fields = ('album_name', 'artist', 'tracks') @@ -87,7 +87,7 @@ For example, is a user account has an optional foreign key to a company, that yo This is in line both with the rest of the serializer fields API, and with Django's `Form` and `ModelForm` API. -Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data. +Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data. The `null=True` argument will continue to function, and will imply `required=False`, but will raise a `PendingDeprecationWarning`. diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md index 7c800afa0..9c9f3e9f6 100644 --- a/docs/topics/2.3-announcement.md +++ b/docs/topics/2.3-announcement.md @@ -27,7 +27,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w class GroupViewSet(viewsets.ModelViewSet): model = Group - + # Routers provide an easy way of automatically determining the URL conf router = routers.DefaultRouter() router.register(r'users', UserViewSet) @@ -197,13 +197,13 @@ Usage of the old-style attributes continues to be supported, but will raise a `P For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation. -## ModelSerializers and reverse relationships +## ModelSerializers and reverse relationships The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed. In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`. -The old-style signature will continue to function but will raise a `PendingDeprecationWarning`. +The old-style signature will continue to function but will raise a `PendingDeprecationWarning`. ## View names and descriptions @@ -211,7 +211,7 @@ The mechanics of how the names and descriptions used in the browseable API are g If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make. -Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. +Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. --- diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index e20f97122..d65e251f1 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -54,7 +54,7 @@ The title that is used in the browsable API is generated from the view class nam For example, the view `UserListView`, will be named `User List` when presented in the browsable API. -When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`. +When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`. #### Setting the description @@ -65,9 +65,9 @@ If the python `markdown` library is installed, then [markdown syntax][markdown] class AccountListView(views.APIView): """ Returns a list of all **active** accounts in the system. - + For more details on how accounts are activated please [see here][ref]. - + [ref]: http://example.com/activating-accounts """ @@ -84,7 +84,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me def metadata(self, request): """ Don't include the view description in OPTIONS responses. - """ + """ data = super(ExampleView, self).metadata(request) data.pop('description') return data diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md index 7d1f6d0eb..e8bad95be 100644 --- a/docs/topics/kickstarter-announcement.md +++ b/docs/topics/kickstarter-announcement.md @@ -160,4 +160,4 @@ The following individuals made a significant financial contribution to the devel ### Supporters -There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you! \ No newline at end of file +There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you! diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 88780c3fb..9fca949ab 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -63,7 +63,7 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: Fix migration in `authtoken` application. * Bugfix: Allow selection of integer keys in nested choices. * Bugfix: Return `None` instead of `'None'` in `CharField` with `allow_none=True`. -* Bugfix: Ensure custom model fields map to equivelent serializer fields more reliably. +* Bugfix: Ensure custom model fields map to equivelent serializer fields more reliably. * Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering. ### 2.4.2 diff --git a/docs/topics/rest-framework-2-announcement.md b/docs/topics/rest-framework-2-announcement.md index f1060d90b..a7746932e 100644 --- a/docs/topics/rest-framework-2-announcement.md +++ b/docs/topics/rest-framework-2-announcement.md @@ -8,7 +8,7 @@ What it is, and why you should care. --- -**Announcement:** REST framework 2 released - Tue 30th Oct 2012 +**Announcement:** REST framework 2 released - Tue 30th Oct 2012 --- @@ -37,7 +37,7 @@ REST framework 2 includes a totally re-worked serialization engine, that was ini * A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API. * Structural concerns are decoupled from encoding concerns. * Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms. -* Validation that can be mapped to obvious and comprehensive error responses. +* Validation that can be mapped to obvious and comprehensive error responses. * Serializers that support both nested, flat, and partially-nested representations. * Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations. diff --git a/docs/topics/writable-nested-serializers.md b/docs/topics/writable-nested-serializers.md index abc6a82f7..ed614bd24 100644 --- a/docs/topics/writable-nested-serializers.md +++ b/docs/topics/writable-nested-serializers.md @@ -8,7 +8,7 @@ Although flat data structures serve to properly delineate between the individual Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependencies between the various model instances, and the need to save or delete multiple instances in a single action. -## One-to-many data structures +## One-to-many data structures *Example of a **read-only** nested serializer. Nothing complex to worry about here.* @@ -16,10 +16,10 @@ Nested data structures are easy enough to work with if they're read-only - simpl class Meta: model = ToDoItem fields = ('text', 'is_completed') - + class ToDoListSerializer(serializers.ModelSerializer): items = ToDoItemSerializer(many=True, read_only=True) - + class Meta: model = ToDoList fields = ('title', 'items') @@ -31,7 +31,7 @@ Some example output from our serializer. 'items': { {'text': 'Compile playlist', 'is_completed': True}, {'text': 'Send invites', 'is_completed': False}, - {'text': 'Clean house', 'is_completed': False} + {'text': 'Clean house', 'is_completed': False} } } @@ -44,4 +44,4 @@ Let's take a look at updating our nested one-to-many data structure. ### Making PATCH requests -[cite]: http://jsonapi.org/format/#url-based-json-api \ No newline at end of file +[cite]: http://jsonapi.org/format/#url-based-json-api diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index db5b9ea7b..f9027b688 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -134,7 +134,7 @@ A serializer class is very similar to a Django `Form` class, and includes simila The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial. -We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit. +We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit. ## Working with Serializers From b8aa7e0c34dc839a47b679aa2402d0f1b98704a0 Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Tue, 18 Nov 2014 17:16:54 +0000 Subject: [PATCH 1137/1652] Fix previous and next links --- docs_theme/nav.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs_theme/nav.html b/docs_theme/nav.html index 87e197b35..0f3b9871c 100644 --- a/docs_theme/nav.html +++ b/docs_theme/nav.html @@ -2,8 +2,12 @@