From 3f79a9a3d3e7692d90476f8a6907957b47aab821 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 22 Mar 2013 22:39:45 +0000 Subject: [PATCH 001/206] 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 002/206] 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 003/206] 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 004/206] 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 005/206] 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 006/206] 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 007/206] 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 f3529f1f4a3b01c2821278da3bafbc04c1c00553 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Fri, 21 Jun 2013 16:26:28 +0100 Subject: [PATCH 008/206] Correct docs' incorrect usage of action decorator If you don't call it, it doesn't work. --- docs/api-guide/viewsets.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 79257e2af..25d11bfb5 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -27,7 +27,7 @@ Let's define a simple viewset that can be used to list or retrieve all the users queryset = User.objects.all() serializer = UserSerializer(queryset, many=True) return Response(serializer.data) - + def retrieve(self, request, pk=None): queryset = User.objects.all() user = get_object_or_404(queryset, pk=pk) @@ -69,7 +69,7 @@ The default routers included with REST framework will provide routes for a stand """ Example empty viewset demonstrating the standard actions that will be handled by a router class. - + If you're using format suffixes, make sure to also include the `format=None` keyword argument for each action. """ @@ -103,12 +103,12 @@ For example: class UserViewSet(viewsets.ModelViewSet): """ - A viewset that provides the standard actions + A viewset that provides the standard actions """ queryset = User.objects.all() serializer_class = UserSerializer - - @action + + @action() def set_password(self, request, pk=None): user = self.get_object() serializer = PasswordSerializer(data=request.DATA) @@ -197,7 +197,7 @@ As with `ModelViewSet`, you'll normally need to provide at least the `queryset` Again, as with `ModelViewSet`, you can use any of the standard attributes and method overrides available to `GenericAPIView`. -# Custom ViewSet base classes +# Custom ViewSet base classes You may need to provide custom `ViewSet` classes that do not have the full set of `ModelViewSet` actions, or that customize the behavior in some other way. @@ -211,7 +211,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope viewsets.GenericViewSet): """ A viewset that provides `retrieve`, `update`, and `list` actions. - + To use it, override the class and set the `.queryset` and `.serializer_class` attributes. """ From fa9f5fb8dcf6d51b2db70d4e2a991779b056d1d4 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Fri, 21 Jun 2013 16:28:17 +0100 Subject: [PATCH 009/206] Allow uppercase methods in action decorator. Previously, using uppercase for the method argument would silently fail to route those methods. --- rest_framework/routers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index f70c2cdb1..ae64cc3ba 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -136,6 +136,7 @@ class SimpleRouter(BaseRouter): attr = getattr(viewset, methodname) httpmethods = getattr(attr, 'bind_to_methods', None) if httpmethods: + httpmethods = [method.lower() for method in httpmethods] dynamic_routes.append((httpmethods, methodname)) ret = [] From 3d4bb4b5533fa281c2f11c12ceb0a9ae61aa0d54 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:03:07 +0100 Subject: [PATCH 010/206] Ensure action kwargs properly handdled. Refs #940. --- htmlcov/coverage_html.js | 372 +++ htmlcov/index.html | 404 ++++ htmlcov/jquery-1.4.3.min.js | 166 ++ htmlcov/jquery.hotkeys.js | 99 + htmlcov/jquery.isonscreen.js | 53 + htmlcov/jquery.tablesorter.min.js | 2 + htmlcov/keybd_closed.png | Bin 0 -> 264 bytes htmlcov/keybd_open.png | Bin 0 -> 267 bytes htmlcov/rest_framework___init__.html | 99 + htmlcov/rest_framework_authentication.html | 767 +++++++ .../rest_framework_authtoken___init__.html | 81 + htmlcov/rest_framework_authtoken_models.html | 151 ++ .../rest_framework_authtoken_serializers.html | 129 ++ htmlcov/rest_framework_authtoken_views.html | 133 ++ htmlcov/rest_framework_decorators.html | 339 +++ htmlcov/rest_framework_exceptions.html | 257 +++ htmlcov/rest_framework_fields.html | 1991 ++++++++++++++++ htmlcov/rest_framework_filters.html | 367 +++ htmlcov/rest_framework_generics.html | 1079 +++++++++ htmlcov/rest_framework_mixins.html | 449 ++++ htmlcov/rest_framework_models.html | 83 + htmlcov/rest_framework_negotiation.html | 259 +++ htmlcov/rest_framework_pagination.html | 269 +++ htmlcov/rest_framework_parsers.html | 671 ++++++ htmlcov/rest_framework_permissions.html | 429 ++++ htmlcov/rest_framework_relations.html | 1347 +++++++++++ htmlcov/rest_framework_renderers.html | 1227 ++++++++++ htmlcov/rest_framework_request.html | 819 +++++++ htmlcov/rest_framework_response.html | 249 ++ htmlcov/rest_framework_reverse.html | 127 ++ htmlcov/rest_framework_routers.html | 595 +++++ htmlcov/rest_framework_serializers.html | 2011 +++++++++++++++++ htmlcov/rest_framework_settings.html | 465 ++++ htmlcov/rest_framework_status.html | 187 ++ htmlcov/rest_framework_throttling.html | 533 +++++ htmlcov/rest_framework_urlpatterns.html | 205 ++ htmlcov/rest_framework_urls.html | 129 ++ htmlcov/rest_framework_utils___init__.html | 81 + htmlcov/rest_framework_utils_breadcrumbs.html | 189 ++ htmlcov/rest_framework_utils_encoders.html | 275 +++ htmlcov/rest_framework_utils_formatting.html | 241 ++ htmlcov/rest_framework_utils_mediatypes.html | 257 +++ htmlcov/rest_framework_views.html | 793 +++++++ htmlcov/rest_framework_viewsets.html | 359 +++ htmlcov/status.dat | 1258 +++++++++++ htmlcov/style.css | 275 +++ rest_framework/tests/test_routers.py | 35 +- 47 files changed, 20303 insertions(+), 3 deletions(-) create mode 100644 htmlcov/coverage_html.js create mode 100644 htmlcov/index.html create mode 100644 htmlcov/jquery-1.4.3.min.js create mode 100644 htmlcov/jquery.hotkeys.js create mode 100644 htmlcov/jquery.isonscreen.js create mode 100644 htmlcov/jquery.tablesorter.min.js create mode 100644 htmlcov/keybd_closed.png create mode 100644 htmlcov/keybd_open.png create mode 100644 htmlcov/rest_framework___init__.html create mode 100644 htmlcov/rest_framework_authentication.html create mode 100644 htmlcov/rest_framework_authtoken___init__.html create mode 100644 htmlcov/rest_framework_authtoken_models.html create mode 100644 htmlcov/rest_framework_authtoken_serializers.html create mode 100644 htmlcov/rest_framework_authtoken_views.html create mode 100644 htmlcov/rest_framework_decorators.html create mode 100644 htmlcov/rest_framework_exceptions.html create mode 100644 htmlcov/rest_framework_fields.html create mode 100644 htmlcov/rest_framework_filters.html create mode 100644 htmlcov/rest_framework_generics.html create mode 100644 htmlcov/rest_framework_mixins.html create mode 100644 htmlcov/rest_framework_models.html create mode 100644 htmlcov/rest_framework_negotiation.html create mode 100644 htmlcov/rest_framework_pagination.html create mode 100644 htmlcov/rest_framework_parsers.html create mode 100644 htmlcov/rest_framework_permissions.html create mode 100644 htmlcov/rest_framework_relations.html create mode 100644 htmlcov/rest_framework_renderers.html create mode 100644 htmlcov/rest_framework_request.html create mode 100644 htmlcov/rest_framework_response.html create mode 100644 htmlcov/rest_framework_reverse.html create mode 100644 htmlcov/rest_framework_routers.html create mode 100644 htmlcov/rest_framework_serializers.html create mode 100644 htmlcov/rest_framework_settings.html create mode 100644 htmlcov/rest_framework_status.html create mode 100644 htmlcov/rest_framework_throttling.html create mode 100644 htmlcov/rest_framework_urlpatterns.html create mode 100644 htmlcov/rest_framework_urls.html create mode 100644 htmlcov/rest_framework_utils___init__.html create mode 100644 htmlcov/rest_framework_utils_breadcrumbs.html create mode 100644 htmlcov/rest_framework_utils_encoders.html create mode 100644 htmlcov/rest_framework_utils_formatting.html create mode 100644 htmlcov/rest_framework_utils_mediatypes.html create mode 100644 htmlcov/rest_framework_views.html create mode 100644 htmlcov/rest_framework_viewsets.html create mode 100644 htmlcov/status.dat create mode 100644 htmlcov/style.css diff --git a/htmlcov/coverage_html.js b/htmlcov/coverage_html.js new file mode 100644 index 000000000..da3e22c81 --- /dev/null +++ b/htmlcov/coverage_html.js @@ -0,0 +1,372 @@ +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shotrtcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a cookie containing previous sort settings: + var sort_list = []; + var cookie_name = "COVERAGE_INDEX_SORT"; + var i; + + // This almost makes it worth installing the jQuery cookie plugin: + if (document.cookie.indexOf(cookie_name) > -1) { + var cookies = document.cookie.split(";"); + for (i = 0; i < cookies.length; i++) { + var parts = cookies[i].split("="); + + if ($.trim(parts[0]) === cookie_name && parts[1]) { + sort_list = eval("[[" + parts[1] + "]]"); + break; + } + } + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 'n') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Return the container of all the code. +coverage.code_container = function () { + return $(".linenos"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.is_transparent = function (color) { + // Different browsers return different colors for "none". + return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + while (true) { + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + if (!c.is_transparent(color)) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_color = color; + while (next_color === color) { + probe++; + probe_line = c.line_elt(probe); + next_color = probe_line.css("background-color"); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + while (probe > 0 && c.is_transparent(color)) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_color = color; + while (prev_color === color) { + probe--; + probe_line = c.line_elt(probe); + prev_color = probe_line.css("background-color"); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_color = probe_line.css("background-color"); + if (!c.is_transparent(the_color)) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var color = the_color; + while (probe > 0 && color === the_color) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + color = probe_line.css("background-color"); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + color = the_color; + while (color === the_color) { + probe++; + probe_line = c.line_elt(probe); + color = probe_line.css("background-color"); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + c.code_container().find(".highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + diff --git a/htmlcov/index.html b/htmlcov/index.html new file mode 100644 index 000000000..983451658 --- /dev/null +++ b/htmlcov/index.html @@ -0,0 +1,404 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total3621406089%
rest_framework/__init__400100%
rest_framework/authentication16933080%
rest_framework/authtoken/__init__000100%
rest_framework/authtoken/models211095%
rest_framework/authtoken/serializers172088%
rest_framework/authtoken/views2100100%
rest_framework/decorators6000100%
rest_framework/exceptions512096%
rest_framework/fields59480087%
rest_framework/filters776092%
rest_framework/generics19634083%
rest_framework/mixins977093%
rest_framework/models000100%
rest_framework/negotiation414090%
rest_framework/pagination4300100%
rest_framework/parsers15313092%
rest_framework/permissions6312081%
rest_framework/relations36588076%
rest_framework/renderers28223092%
rest_framework/request1618095%
rest_framework/response421098%
rest_framework/reverse123075%
rest_framework/routers1087094%
rest_framework/serializers46427094%
rest_framework/settings442095%
rest_framework/status4600100%
rest_framework/throttling9017081%
rest_framework/urlpatterns314087%
rest_framework/urls400100%
rest_framework/utils/__init__000100%
rest_framework/utils/breadcrumbs2700100%
rest_framework/utils/encoders7019073%
rest_framework/utils/formatting391097%
rest_framework/utils/mediatypes4410077%
rest_framework/views14600100%
rest_framework/viewsets392095%
+
+ + + + + diff --git a/htmlcov/jquery-1.4.3.min.js b/htmlcov/jquery-1.4.3.min.js new file mode 100644 index 000000000..c941a5f7a --- /dev/null +++ b/htmlcov/jquery-1.4.3.min.js @@ -0,0 +1,166 @@ +/*! + * jQuery JavaScript Library v1.4.3 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Oct 14 23:10:06 2010 -0400 + */ +(function(E,A){function U(){return false}function ba(){return true}function ja(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ga(a){var b,d,e=[],f=[],h,k,l,n,s,v,B,D;k=c.data(this,this.nodeType?"events":"__events__");if(typeof k==="function")k=k.events;if(!(a.liveFired===this||!k||!k.live||a.button&&a.type==="click")){if(a.namespace)D=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var H=k.live.slice(0);for(n=0;nd)break;a.currentTarget=f.elem;a.data=f.handleObj.data; +a.handleObj=f.handleObj;D=f.handleObj.origHandler.apply(f.elem,arguments);if(D===false||a.isPropagationStopped()){d=f.level;if(D===false)b=false}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(Ha,"`").replace(Ia,"&")}function ka(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Ja.test(b))return c.filter(b, +e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function la(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this,e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var k in e[h])c.event.add(this,h,e[h][k],e[h][k].data)}}})}function Ka(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)} +function ma(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?La:Ma,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a,"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function ca(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Na.test(a)?e(a,h):ca(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)? +e(a,""):c.each(b,function(f,h){ca(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(na.concat.apply([],na.slice(0,b)),function(){d[this]=a});return d}function oa(a){if(!da[a]){var b=c("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";da[a]=d}return da[a]}function ea(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var u=E.document,c=function(){function a(){if(!b.isReady){try{u.documentElement.doScroll("left")}catch(i){setTimeout(a, +1);return}b.ready()}}var b=function(i,r){return new b.fn.init(i,r)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,k=/\S/,l=/^\s+/,n=/\s+$/,s=/\W/,v=/\d/,B=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,D=/^[\],:{}\s]*$/,H=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,G=/(?:^|:|,)(?:\s*\[)+/g,M=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,j=/(msie) ([\w.]+)/,o=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false, +q=[],t,x=Object.prototype.toString,C=Object.prototype.hasOwnProperty,P=Array.prototype.push,N=Array.prototype.slice,R=String.prototype.trim,Q=Array.prototype.indexOf,L={};b.fn=b.prototype={init:function(i,r){var y,z,F;if(!i)return this;if(i.nodeType){this.context=this[0]=i;this.length=1;return this}if(i==="body"&&!r&&u.body){this.context=u;this[0]=u.body;this.selector="body";this.length=1;return this}if(typeof i==="string")if((y=h.exec(i))&&(y[1]||!r))if(y[1]){F=r?r.ownerDocument||r:u;if(z=B.exec(i))if(b.isPlainObject(r)){i= +[u.createElement(z[1])];b.fn.attr.call(i,r,true)}else i=[F.createElement(z[1])];else{z=b.buildFragment([y[1]],[F]);i=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this,i)}else{if((z=u.getElementById(y[2]))&&z.parentNode){if(z.id!==y[2])return f.find(i);this.length=1;this[0]=z}this.context=u;this.selector=i;return this}else if(!r&&!s.test(i)){this.selector=i;this.context=u;i=u.getElementsByTagName(i);return b.merge(this,i)}else return!r||r.jquery?(r||f).find(i):b(r).find(i); +else if(b.isFunction(i))return f.ready(i);if(i.selector!==A){this.selector=i.selector;this.context=i.context}return b.makeArray(i,this)},selector:"",jquery:"1.4.3",length:0,size:function(){return this.length},toArray:function(){return N.call(this,0)},get:function(i){return i==null?this.toArray():i<0?this.slice(i)[0]:this[i]},pushStack:function(i,r,y){var z=b();b.isArray(i)?P.apply(z,i):b.merge(z,i);z.prevObject=this;z.context=this.context;if(r==="find")z.selector=this.selector+(this.selector?" ": +"")+y;else if(r)z.selector=this.selector+"."+r+"("+y+")";return z},each:function(i,r){return b.each(this,i,r)},ready:function(i){b.bindReady();if(b.isReady)i.call(u,b);else q&&q.push(i);return this},eq:function(i){return i===-1?this.slice(i):this.slice(i,+i+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(i){return this.pushStack(b.map(this,function(r,y){return i.call(r, +y,r)}))},end:function(){return this.prevObject||b(null)},push:P,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var i=arguments[0]||{},r=1,y=arguments.length,z=false,F,I,K,J,fa;if(typeof i==="boolean"){z=i;i=arguments[1]||{};r=2}if(typeof i!=="object"&&!b.isFunction(i))i={};if(y===r){i=this;--r}for(;r0)){if(q){for(var r=0;i=q[r++];)i.call(u,b);q=null}b.fn.triggerHandler&&b(u).triggerHandler("ready")}}},bindReady:function(){if(!p){p=true;if(u.readyState==="complete")return setTimeout(b.ready, +1);if(u.addEventListener){u.addEventListener("DOMContentLoaded",t,false);E.addEventListener("load",b.ready,false)}else if(u.attachEvent){u.attachEvent("onreadystatechange",t);E.attachEvent("onload",b.ready);var i=false;try{i=E.frameElement==null}catch(r){}u.documentElement.doScroll&&i&&a()}}},isFunction:function(i){return b.type(i)==="function"},isArray:Array.isArray||function(i){return b.type(i)==="array"},isWindow:function(i){return i&&typeof i==="object"&&"setInterval"in i},isNaN:function(i){return i== +null||!v.test(i)||isNaN(i)},type:function(i){return i==null?String(i):L[x.call(i)]||"object"},isPlainObject:function(i){if(!i||b.type(i)!=="object"||i.nodeType||b.isWindow(i))return false;if(i.constructor&&!C.call(i,"constructor")&&!C.call(i.constructor.prototype,"isPrototypeOf"))return false;for(var r in i);return r===A||C.call(i,r)},isEmptyObject:function(i){for(var r in i)return false;return true},error:function(i){throw i;},parseJSON:function(i){if(typeof i!=="string"||!i)return null;i=b.trim(i); +if(D.test(i.replace(H,"@").replace(w,"]").replace(G,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(i):(new Function("return "+i))();else b.error("Invalid JSON: "+i)},noop:function(){},globalEval:function(i){if(i&&k.test(i)){var r=u.getElementsByTagName("head")[0]||u.documentElement,y=u.createElement("script");y.type="text/javascript";if(b.support.scriptEval)y.appendChild(u.createTextNode(i));else y.text=i;r.insertBefore(y,r.firstChild);r.removeChild(y)}},nodeName:function(i,r){return i.nodeName&&i.nodeName.toUpperCase()=== +r.toUpperCase()},each:function(i,r,y){var z,F=0,I=i.length,K=I===A||b.isFunction(i);if(y)if(K)for(z in i){if(r.apply(i[z],y)===false)break}else for(;F";a=u.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var s=u.createElement("div"); +s.style.width=s.style.paddingLeft="1px";u.body.appendChild(s);c.boxModel=c.support.boxModel=s.offsetWidth===2;if("zoom"in s.style){s.style.display="inline";s.style.zoom=1;c.support.inlineBlockNeedsLayout=s.offsetWidth===2;s.style.display="";s.innerHTML="
";c.support.shrinkWrapBlocks=s.offsetWidth!==2}s.innerHTML="
t
";var v=s.getElementsByTagName("td");c.support.reliableHiddenOffsets=v[0].offsetHeight=== +0;v[0].style.display="";v[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&v[0].offsetHeight===0;s.innerHTML="";u.body.removeChild(s).style.display="none"});a=function(s){var v=u.createElement("div");s="on"+s;var B=s in v;if(!B){v.setAttribute(s,"return;");B=typeof v[s]==="function"}return B};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength", +cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var pa={},Oa=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?pa:a;var e=a.nodeType,f=e?a[c.expando]:null,h=c.cache;if(!(e&&!f&&typeof b==="string"&&d===A)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]= +c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==A)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?pa:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando);else if(d)delete f[e];else for(var k in a)delete a[k]}},acceptData:function(a){if(a.nodeName){var b= +c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){if(typeof a==="undefined")return this.length?c.data(this[0]):null;else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===A){var e=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(e===A&&this.length){e=c.data(this[0],a);if(e===A&&this[0].nodeType===1){e=this[0].getAttribute("data-"+a);if(typeof e=== +"string")try{e=e==="true"?true:e==="false"?false:e==="null"?null:!c.isNaN(e)?parseFloat(e):Oa.test(e)?c.parseJSON(e):e}catch(f){}else e=A}}return e===A&&d[1]?this.data(d[0]):e}else return this.each(function(){var h=c(this),k=[d[0],b];h.triggerHandler("setData"+d[1]+"!",k);c.data(this,a,b);h.triggerHandler("changeData"+d[1]+"!",k)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var e=c.data(a,b);if(!d)return e|| +[];if(!e||c.isArray(d))e=c.data(a,b,c.makeArray(d));else e.push(d);return e}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),e=d.shift();if(e==="inprogress")e=d.shift();if(e){b==="fx"&&d.unshift("inprogress");e.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===A)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this, +a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var qa=/[\n\t]/g,ga=/\s+/,Pa=/\r/g,Qa=/^(?:href|src|style)$/,Ra=/^(?:button|input)$/i,Sa=/^(?:button|input|object|select|textarea)$/i,Ta=/^a(?:rea)?$/i,ra=/^(?:radio|checkbox)$/i;c.fn.extend({attr:function(a,b){return c.access(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this, +a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(s){var v=c(this);v.addClass(a.call(this,s,v.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ga),d=0,e=this.length;d-1)return true;return false}, +val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one";if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var B=c.makeArray(v);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),B)>=0});if(!B.length)this.selectedIndex=-1}else this.value=v}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return A;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==A;b=e&&c.props[b]||b;if(a.nodeType===1){var h=Qa.test(b);if((b in a||a[b]!==A)&&e&&!h){if(f){b==="type"&&Ra.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Sa.test(a.nodeName)||Ta.test(a.nodeName)&&a.href?0:A;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return A;a=!c.support.hrefNormalized&&e&& +h?a.getAttribute(b,2):a.getAttribute(b);return a===null?A:a}}});var X=/\.(.*)$/,ha=/^(?:textarea|input|select)$/i,Ha=/\./g,Ia=/ /g,Ua=/[^\w\s.|`]/g,Va=function(a){return a.replace(Ua,"\\$&")},sa={focusin:0,focusout:0};c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var k=a.nodeType?"events":"__events__",l=h[k],n=h.handle;if(typeof l=== +"function"){n=l.handle;l=l.events}else if(!l){a.nodeType||(h[k]=h=function(){});h.events=l={}}if(!n)h.handle=n=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(n.elem,arguments):A};n.elem=a;b=b.split(" ");for(var s=0,v;k=b[s++];){h=f?c.extend({},f):{handler:d,data:e};if(k.indexOf(".")>-1){v=k.split(".");k=v.shift();h.namespace=v.slice(0).sort().join(".")}else{v=[];h.namespace=""}h.type=k;if(!h.guid)h.guid=d.guid;var B=l[k],D=c.event.special[k]||{};if(!B){B=l[k]=[]; +if(!D.setup||D.setup.call(a,e,v,n)===false)if(a.addEventListener)a.addEventListener(k,n,false);else a.attachEvent&&a.attachEvent("on"+k,n)}if(D.add){D.add.call(a,h);if(!h.handler.guid)h.handler.guid=d.guid}B.push(h);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,k=0,l,n,s,v,B,D,H=a.nodeType?"events":"__events__",w=c.data(a),G=w&&w[H];if(w&&G){if(typeof G==="function"){w=G;G=G.events}if(b&&b.type){d=b.handler;b=b.type}if(!b|| +typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in G)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[k++];){v=f;l=f.indexOf(".")<0;n=[];if(!l){n=f.split(".");f=n.shift();s=RegExp("(^|\\.)"+c.map(n.slice(0).sort(),Va).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(B=G[f])if(d){v=c.event.special[f]||{};for(h=e||0;h=0){a.type= +f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return A;a.result=A;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)=== +false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){e=a.target;var k,l=f.replace(X,""),n=c.nodeName(e,"a")&&l==="click",s=c.event.special[l]||{};if((!s._default||s._default.call(d,a)===false)&&!n&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[l]){if(k=e["on"+l])e["on"+l]=null;c.event.triggered=true;e[l]()}}catch(v){}if(k)e["on"+l]=k;c.event.triggered=false}}},handle:function(a){var b,d,e; +d=[];var f,h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var k=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ha.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=va(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===A||f===e))if(e!=null||f){a.type="change";a.liveFired= +A;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",va(a))}},setup:function(){if(this.type=== +"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ha.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ha.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}u.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){sa[b]++===0&&u.addEventListener(a,d,true)},teardown:function(){--sa[b]=== +0&&u.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=A}var k=b==="one"?c.proxy(f,function(n){c(this).unbind(n,k);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var l=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); +(function(){function a(g,j,o,m,p,q){p=0;for(var t=m.length;p0){C=x;break}}x=x[g]}m[p]=C}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,k=true;[0,0].sort(function(){k=false;return 0});var l=function(g,j,o,m){o=o||[];var p=j=j||u;if(j.nodeType!==1&&j.nodeType!==9)return[];if(!g||typeof g!=="string")return o;var q=[],t,x,C,P,N=true,R=l.isXML(j),Q=g,L;do{d.exec("");if(t=d.exec(Q)){Q=t[3];q.push(t[1]);if(t[2]){P=t[3]; +break}}}while(t);if(q.length>1&&s.exec(g))if(q.length===2&&n.relative[q[0]])x=M(q[0]+q[1],j);else for(x=n.relative[q[0]]?[j]:l(q.shift(),j);q.length;){g=q.shift();if(n.relative[g])g+=q.shift();x=M(g,x)}else{if(!m&&q.length>1&&j.nodeType===9&&!R&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){t=l.find(q.shift(),j,R);j=t.expr?l.filter(t.expr,t.set)[0]:t.set[0]}if(j){t=m?{expr:q.pop(),set:D(m)}:l.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&j.parentNode?j.parentNode:j,R);x=t.expr?l.filter(t.expr, +t.set):t.set;if(q.length>0)C=D(x);else N=false;for(;q.length;){t=L=q.pop();if(n.relative[L])t=q.pop();else L="";if(t==null)t=j;n.relative[L](C,t,R)}}else C=[]}C||(C=x);C||l.error(L||g);if(f.call(C)==="[object Array]")if(N)if(j&&j.nodeType===1)for(g=0;C[g]!=null;g++){if(C[g]&&(C[g]===true||C[g].nodeType===1&&l.contains(j,C[g])))o.push(x[g])}else for(g=0;C[g]!=null;g++)C[g]&&C[g].nodeType===1&&o.push(x[g]);else o.push.apply(o,C);else D(C,o);if(P){l(P,p,o,m);l.uniqueSort(o)}return o};l.uniqueSort=function(g){if(w){h= +k;g.sort(w);if(h)for(var j=1;j0};l.find=function(g,j,o){var m;if(!g)return[];for(var p=0,q=n.order.length;p":function(g,j){var o=typeof j==="string",m,p=0,q=g.length;if(o&&!/\W/.test(j))for(j=j.toLowerCase();p=0))o||m.push(t);else if(o)j[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var j=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=j[1]+(j[2]||1)-0;g[3]=j[3]-0}g[0]=e++;return g},ATTR:function(g,j,o, +m,p,q){j=g[1].replace(/\\/g,"");if(!q&&n.attrMap[j])g[1]=n.attrMap[j];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,j,o,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=l(g[3],null,null,j);else{g=l.filter(g[3],j,o,true^p);o||m.push.apply(m,g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== +true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,j,o){return!!l(o[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== +g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,j){return j===0},last:function(g,j,o,m){return j===m.length-1},even:function(g,j){return j%2===0},odd:function(g,j){return j%2===1},lt:function(g,j,o){return jo[3]-0},nth:function(g,j,o){return o[3]- +0===j},eq:function(g,j,o){return o[3]-0===j}},filter:{PSEUDO:function(g,j,o,m){var p=j[1],q=n.filters[p];if(q)return q(g,o,j,m);else if(p==="contains")return(g.textContent||g.innerText||l.getText([g])||"").indexOf(j[3])>=0;else if(p==="not"){j=j[3];o=0;for(m=j.length;o=0}},ID:function(g,j){return g.nodeType===1&&g.getAttribute("id")===j},TAG:function(g,j){return j==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== +j},CLASS:function(g,j){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(j)>-1},ATTR:function(g,j){var o=j[1];o=n.attrHandle[o]?n.attrHandle[o](g):g[o]!=null?g[o]:g.getAttribute(o);var m=o+"",p=j[2],q=j[4];return o==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&o!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,j,o,m){var p=n.setFilters[j[2]]; +if(p)return p(g,o,j,m)}}},s=n.match.POS,v=function(g,j){return"\\"+(j-0+1)},B;for(B in n.match){n.match[B]=RegExp(n.match[B].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[B]=RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[B].source.replace(/\\(\d+)/g,v))}var D=function(g,j){g=Array.prototype.slice.call(g,0);if(j){j.push.apply(j,g);return j}return g};try{Array.prototype.slice.call(u.documentElement.childNodes,0)}catch(H){D=function(g,j){var o=j||[],m=0;if(f.call(g)==="[object Array]")Array.prototype.push.apply(o, +g);else if(typeof g.length==="number")for(var p=g.length;m";var o=u.documentElement;o.insertBefore(g,o.firstChild);if(u.getElementById(j)){n.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:A:[]};n.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}o.removeChild(g); +o=g=null})();(function(){var g=u.createElement("div");g.appendChild(u.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(j,o){var m=o.getElementsByTagName(j[1]);if(j[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(j){return j.getAttribute("href",2)};g=null})();u.querySelectorAll&& +function(){var g=l,j=u.createElement("div");j.innerHTML="

";if(!(j.querySelectorAll&&j.querySelectorAll(".TEST").length===0)){l=function(m,p,q,t){p=p||u;if(!t&&!l.isXML(p))if(p.nodeType===9)try{return D(p.querySelectorAll(m),q)}catch(x){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var C=p.id,P=p.id="__sizzle__";try{return D(p.querySelectorAll("#"+P+" "+m),q)}catch(N){}finally{if(C)p.id=C;else p.removeAttribute("id")}}return g(m,p,q,t)};for(var o in g)l[o]=g[o]; +j=null}}();(function(){var g=u.documentElement,j=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,o=false;try{j.call(u.documentElement,":sizzle")}catch(m){o=true}if(j)l.matchesSelector=function(p,q){try{if(o||!n.match.PSEUDO.test(q))return j.call(p,q)}catch(t){}return l(q,null,null,[p]).length>0}})();(function(){var g=u.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== +0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(j,o,m){if(typeof o.getElementsByClassName!=="undefined"&&!m)return o.getElementsByClassName(j[1])};g=null}}})();l.contains=u.documentElement.contains?function(g,j){return g!==j&&(g.contains?g.contains(j):true)}:function(g,j){return!!(g.compareDocumentPosition(j)&16)};l.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var M=function(g, +j){for(var o=[],m="",p,q=j.nodeType?[j]:j;p=n.match.PSEUDO.exec(g);){m+=p[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;p=0;for(var t=q.length;p0)for(var h=d;h0},closest:function(a, +b){var d=[],e,f,h=this[0];if(c.isArray(a)){var k={},l,n=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:n})}h=h.parentNode;n++}}return d}k=$a.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h|| +!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context):c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}}); +c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling", +d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Wa.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||Ya.test(e))&&Xa.test(a))f=f.reverse();return this.pushStack(f,a,Za.call(arguments).join(","))}}); +c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===A||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var xa=/ jQuery\d+="(?:\d+|null)"/g, +$=/^\s+/,ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,za=/<([\w:]+)/,ab=/\s]+\/)>/g,O={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"], +area:[1,"",""],_default:[0,"",""]};O.optgroup=O.option;O.tbody=O.tfoot=O.colgroup=O.caption=O.thead;O.th=O.td;if(!c.support.htmlSerialize)O._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==A)return this.empty().append((this[0]&&this[0].ownerDocument||u).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this, +d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})}, +unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a= +c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*")); +c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(xa,"").replace(cb,'="$1">').replace($, +"")],e)[0]}else return this.cloneNode(true)});if(a===true){la(this,b);la(this.find("*"),b.find("*"))}return b},html:function(a){if(a===A)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(xa,""):null;else if(typeof a==="string"&&!Aa.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!O[(za.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ya,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?l.cloneNode(true):l)}k.length&&c.each(k,Ka)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:u;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===u&&!Aa.test(a[0])&&(c.support.checkClone|| +!Ba.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h= +d.length;f0?this.clone(true):this).get();c(d[f])[b](k);e=e.concat(k)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||u;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||u;for(var f=[],h=0,k;(k=a[h])!=null;h++){if(typeof k==="number")k+="";if(k){if(typeof k==="string"&&!bb.test(k))k=b.createTextNode(k);else if(typeof k==="string"){k=k.replace(ya,"<$1>");var l=(za.exec(k)||["",""])[1].toLowerCase(),n=O[l]||O._default, +s=n[0],v=b.createElement("div");for(v.innerHTML=n[1]+k+n[2];s--;)v=v.lastChild;if(!c.support.tbody){s=ab.test(k);l=l==="table"&&!s?v.firstChild&&v.firstChild.childNodes:n[1]===""&&!s?v.childNodes:[];for(n=l.length-1;n>=0;--n)c.nodeName(l[n],"tbody")&&!l[n].childNodes.length&&l[n].parentNode.removeChild(l[n])}!c.support.leadingWhitespace&&$.test(k)&&v.insertBefore(b.createTextNode($.exec(k)[0]),v.firstChild);k=v.childNodes}if(k.nodeType)f.push(k);else f=c.merge(f,k)}}if(d)for(h=0;f[h];h++)if(e&& +c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script"))));d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,k=0,l;(l=a[k])!=null;k++)if(!(l.nodeName&&c.noData[l.nodeName.toLowerCase()]))if(d=l[c.expando]){if((b=e[d])&&b.events)for(var n in b.events)f[n]? +c.event.remove(l,n):c.removeEvent(l,n,b.handle);if(h)delete l[c.expando];else l.removeAttribute&&l.removeAttribute(c.expando);delete e[d]}}});var Ca=/alpha\([^)]*\)/i,db=/opacity=([^)]*)/,eb=/-([a-z])/ig,fb=/([A-Z])/g,Da=/^-?\d+(?:px)?$/i,gb=/^-?\d/,hb={position:"absolute",visibility:"hidden",display:"block"},La=["Left","Right"],Ma=["Top","Bottom"],W,ib=u.defaultView&&u.defaultView.getComputedStyle,jb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===A)return this; +return c.access(this,a,b,true,function(d,e,f){return f!==A?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true,zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),k=a.style,l=c.cssHooks[h];b=c.cssProps[h]|| +h;if(d!==A){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!l||!("set"in l)||(d=l.set(a,d))!==A)try{k[b]=d}catch(n){}}}else{if(l&&"get"in l&&(f=l.get(a,false,e))!==A)return f;return k[b]}}},css:function(a,b,d){var e,f=c.camelCase(b),h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==A)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]= +e[f]},camelCase:function(a){return a.replace(eb,jb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=ma(d,b,f);else c.swap(d,hb,function(){h=ma(d,b,f)});return h+"px"}},set:function(d,e){if(Da.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return db.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"": +b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f=d.filter||"";d.filter=Ca.test(f)?f.replace(Ca,e):d.filter+" "+e}};if(ib)W=function(a,b,d){var e;d=d.replace(fb,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return A;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};else if(u.documentElement.currentStyle)W=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b], +h=a.style;if(!Da.test(f)&&gb.test(f)){d=h.left;e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f};if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var kb=c.now(),lb=/)<[^<]*)*<\/script>/gi, +mb=/^(?:select|textarea)/i,nb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ob=/^(?:GET|HEAD|DELETE)$/,Na=/\[\]$/,T=/\=\?(&|$)/,ia=/\?/,pb=/([?&])_=[^&]*/,qb=/^(\w+:)?\/\/([^\/?#]+)/,rb=/%20/g,sb=/#.*$/,Ea=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ea)return Ea.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d= +b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(k,l){if(l==="success"||l==="notmodified")h.html(f?c("
").append(k.responseText.replace(lb,"")).find(f):k.responseText);d&&h.each(d,[k.responseText,l,k])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& +!this.disabled&&(this.checked||mb.test(this.nodeName)||nb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, +getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", +script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),k=ob.test(h);b.url=b.url.replace(sb,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ia.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| +!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+kb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var l=E[d];E[d]=function(m){f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);if(c.isFunction(l))l(m);else{E[d]=A;try{delete E[d]}catch(p){}}v&&v.removeChild(B)}}if(b.dataType==="script"&&b.cache===null)b.cache= +false;if(b.cache===false&&h==="GET"){var n=c.now(),s=b.url.replace(pb,"$1_="+n);b.url=s+(s===b.url?(ia.test(b.url)?"&":"?")+"_="+n:"")}if(b.data&&h==="GET")b.url+=(ia.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");n=(n=qb.exec(b.url))&&(n[1]&&n[1]!==location.protocol||n[2]!==location.host);if(b.dataType==="script"&&h==="GET"&&n){var v=u.getElementsByTagName("head")[0]||u.documentElement,B=u.createElement("script");if(b.scriptCharset)B.charset=b.scriptCharset;B.src= +b.url;if(!d){var D=false;B.onload=B.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);B.onload=B.onreadystatechange=null;v&&B.parentNode&&v.removeChild(B)}}}v.insertBefore(B,v.firstChild);return A}var H=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!k||a&&a.contentType)w.setRequestHeader("Content-Type", +b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}n||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(G){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& +c.triggerGlobal(b,"ajaxSend",[w,b]);var M=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){H||c.handleComplete(b,w,e,f);H=true;if(w)w.onreadystatechange=c.noop}else if(!H&&w&&(w.readyState===4||m==="timeout")){H=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| +c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&g.call&&g.call(w);M("abort")}}catch(j){}b.async&&b.timeout>0&&setTimeout(function(){w&&!H&&M("timeout")},b.timeout);try{w.send(k||b.data==null?null:b.data)}catch(o){c.handleError(b,w,null,o);c.handleComplete(b,w,e,f)}b.async||M();return w}},param:function(a,b){var d=[],e=function(h,k){k=c.isFunction(k)?k():k;d[d.length]=encodeURIComponent(h)+ +"="+encodeURIComponent(k)};if(b===A)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)ca(f,a[f],b,e);return d.join("&").replace(rb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess",[b,a])},handleComplete:function(a, +b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),e=a.getResponseHeader("Etag"); +if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}});if(E.ActiveXObject)c.ajaxSettings.xhr= +function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var da={},tb=/^(?:toggle|show|hide)$/,ub=/^([+\-]=)?([\d+.\-]+)(.*)$/,aa,na=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show",3),a,b,d);else{a= +0;for(b=this.length;a=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, +d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* +Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(h){return f.step(h)} +this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var f=this;a=c.fx;e.elem=this.elem;if(e()&&c.timers.push(e)&&!aa)aa=setInterval(a.tick,a.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; +this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(l,n){f.style["overflow"+n]=h.overflow[l]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| +this.options.show)for(var k in this.options.curAnim)c.style(this.elem,k,this.options.orig[k]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= +c.timers,b=0;b-1;e={};var s={};if(n)s=f.position();k=n?s.top:parseInt(k,10)||0;l=n?s.left:parseInt(l,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+k;if(b.left!=null)e.left=b.left-h.left+l;"using"in b?b.using.call(a, +e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Fa.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||u.body;a&&!Fa.test(a.nodeName)&& +c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==A)return this.each(function(){if(h=ea(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=ea(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); +c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(h){var k=c(this);k[d](e.call(this,h,k[d]()))});return c.isWindow(f)?f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b]:f.nodeType===9?Math.max(f.documentElement["client"+ +b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]):e===A?parseFloat(c.css(f,d)):this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/htmlcov/jquery.hotkeys.js b/htmlcov/jquery.hotkeys.js new file mode 100644 index 000000000..09b21e03c --- /dev/null +++ b/htmlcov/jquery.hotkeys.js @@ -0,0 +1,99 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text") ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/htmlcov/jquery.isonscreen.js b/htmlcov/jquery.isonscreen.js new file mode 100644 index 000000000..0182ebd21 --- /dev/null +++ b/htmlcov/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/htmlcov/jquery.tablesorter.min.js b/htmlcov/jquery.tablesorter.min.js new file mode 100644 index 000000000..64c700712 --- /dev/null +++ b/htmlcov/jquery.tablesorter.min.js @@ -0,0 +1,2 @@ + +(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('
').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;ib)?1:0));};function sortTextDesc(a,b){return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;iD6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9@bl*u7(4JIy_w*Lo808 zo$Afkpupp@{Fv_bobxQ#pD>iB3oNa1d9=pM`D99*FvsH{pKJfpB1-4UD;=6}F=+gKX>Gx9b=!>PY1_pdfo@{(boFyt=akR{ E04sl8JOBUy literal 0 HcmV?d00001 diff --git a/htmlcov/keybd_open.png b/htmlcov/keybd_open.png new file mode 100644 index 0000000000000000000000000000000000000000..a77961db5424cfff43a63d399972ee85fc0dfdb1 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^%0SG+!3HE>D6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9%kc-1xY}mZci7-5R21$ zCp+>TR^VYdE*ieC^FGV{Cyeh_21=Rotz3KNq=!VmdK II;Vst00jnQH~;_u literal 0 HcmV?d00001 diff --git a/htmlcov/rest_framework___init__.html b/htmlcov/rest_framework___init__.html new file mode 100644 index 000000000..9cb0c53ac --- /dev/null +++ b/htmlcov/rest_framework___init__.html @@ -0,0 +1,99 @@ + + + + + + + + Coverage for rest_framework/__init__: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+
+ + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+ +
+

__version__ = '2.3.5' 

+

 

+

VERSION = __version__  # synonym 

+

 

+

# Header encoding (see RFC5987) 

+

HTTP_HEADER_ENCODING = 'iso-8859-1' 

+

 

+

# Default datetime input and output formats 

+

ISO_8601 = 'iso-8601' 

+ +
+ + + + + + diff --git a/htmlcov/rest_framework_authentication.html b/htmlcov/rest_framework_authentication.html new file mode 100644 index 000000000..899d06777 --- /dev/null +++ b/htmlcov/rest_framework_authentication.html @@ -0,0 +1,767 @@ + + + + + + + + Coverage for rest_framework/authentication: 80% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+ +
+

""" 

+

Provides various authentication policies. 

+

""" 

+

from __future__ import unicode_literals 

+

import base64 

+

from datetime import datetime 

+

 

+

from django.contrib.auth import authenticate 

+

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 

+

from rest_framework.authtoken.models import Token 

+

 

+

 

+

def get_authorization_header(request): 

+

    """ 

+

    Return request's 'Authorization:' header, as a bytestring. 

+

 

+

    Hide some test client ickyness where the header can be unicode. 

+

    """ 

+

    auth = request.META.get('HTTP_AUTHORIZATION', b'') 

+

    if type(auth) == type(''): 

+

        # Work around django test client oddness 

+

        auth = auth.encode(HTTP_HEADER_ENCODING) 

+

    return auth 

+

 

+

 

+

class BaseAuthentication(object): 

+

    """ 

+

    All authentication classes should extend BaseAuthentication. 

+

    """ 

+

 

+

    def authenticate(self, request): 

+

        """ 

+

        Authenticate the request and return a two-tuple of (user, token). 

+

        """ 

+

        raise NotImplementedError(".authenticate() must be overridden.") 

+

 

+

    def authenticate_header(self, request): 

+

        """ 

+

        Return a string to be used as the value of the `WWW-Authenticate` 

+

        header in a `401 Unauthenticated` response, or `None` if the 

+

        authentication scheme should return `403 Permission Denied` responses. 

+

        """ 

+

        pass 

+

 

+

 

+

class BasicAuthentication(BaseAuthentication): 

+

    """ 

+

    HTTP Basic authentication against username/password. 

+

    """ 

+

    www_authenticate_realm = 'api' 

+

 

+

    def authenticate(self, request): 

+

        """ 

+

        Returns a `User` if a correct username and password have been supplied 

+

        using HTTP Basic authentication.  Otherwise returns `None`. 

+

        """ 

+

        auth = get_authorization_header(request).split() 

+

 

+

        if not auth or auth[0].lower() != b'basic': 

+

            return None 

+

 

+

        if len(auth) == 1: 

+

            msg = 'Invalid basic header. No credentials provided.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

        elif len(auth) > 2: 

+

            msg = 'Invalid basic header. Credentials string should not contain spaces.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        try: 

+

            auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') 

+

        except (TypeError, UnicodeDecodeError): 

+

            msg = 'Invalid basic header. Credentials not correctly base64 encoded' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        userid, password = auth_parts[0], auth_parts[2] 

+

        return self.authenticate_credentials(userid, password) 

+

 

+

    def authenticate_credentials(self, userid, password): 

+

        """ 

+

        Authenticate the userid and password against username and password. 

+

        """ 

+

        user = authenticate(username=userid, password=password) 

+

        if user is None or not user.is_active: 

+

            raise exceptions.AuthenticationFailed('Invalid username/password') 

+

        return (user, None) 

+

 

+

    def authenticate_header(self, request): 

+

        return 'Basic realm="%s"' % self.www_authenticate_realm 

+

 

+

 

+

class SessionAuthentication(BaseAuthentication): 

+

    """ 

+

    Use Django's session framework for authentication. 

+

    """ 

+

 

+

    def authenticate(self, request): 

+

        """ 

+

        Returns a `User` if the request session currently has a logged in user. 

+

        Otherwise returns `None`. 

+

        """ 

+

 

+

        # Get the underlying HttpRequest object 

+

        http_request = request._request 

+

        user = getattr(http_request, 'user', None) 

+

 

+

        # Unauthenticated, CSRF validation not required 

+

        if not user or not user.is_active: 

+

            return None 

+

 

+

        # Enforce CSRF validation for session based authentication. 

+

        class CSRFCheck(CsrfViewMiddleware): 

+

            def _reject(self, request, reason): 

+

                # Return the failure reason instead of an HttpResponse 

+

                return reason 

+

 

+

        reason = CSRFCheck().process_view(http_request, None, (), {}) 

+

        if reason: 

+

            # CSRF failed, bail with explicit error message 

+

            raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) 

+

 

+

        # CSRF passed with authenticated user 

+

        return (user, None) 

+

 

+

 

+

class TokenAuthentication(BaseAuthentication): 

+

    """ 

+

    Simple token based authentication. 

+

 

+

    Clients should authenticate by passing the token key in the "Authorization" 

+

    HTTP header, prepended with the string "Token ".  For example: 

+

 

+

        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a 

+

    """ 

+

 

+

    model = Token 

+

    """ 

+

    A custom token model may be used, but must have the following properties. 

+

 

+

    * key -- The string identifying the token 

+

    * user -- The user to which the token belongs 

+

    """ 

+

 

+

    def authenticate(self, request): 

+

        auth = get_authorization_header(request).split() 

+

 

+

        if not auth or auth[0].lower() != b'token': 

+

            return None 

+

 

+

        if len(auth) == 1: 

+

            msg = 'Invalid token header. No credentials provided.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

        elif len(auth) > 2: 

+

            msg = 'Invalid token header. Token string should not contain spaces.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        return self.authenticate_credentials(auth[1]) 

+

 

+

    def authenticate_credentials(self, key): 

+

        try: 

+

            token = self.model.objects.get(key=key) 

+

        except self.model.DoesNotExist: 

+

            raise exceptions.AuthenticationFailed('Invalid token') 

+

 

+

        if not token.user.is_active: 

+

            raise exceptions.AuthenticationFailed('User inactive or deleted') 

+

 

+

        return (token.user, token) 

+

 

+

    def authenticate_header(self, request): 

+

        return 'Token' 

+

 

+

 

+

class OAuthAuthentication(BaseAuthentication): 

+

    """ 

+

    OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`. 

+

 

+

    Note: The `oauth2` package actually provides oauth1.0a support.  Urg. 

+

          We import it from the `compat` module as `oauth`. 

+

    """ 

+

    www_authenticate_realm = 'api' 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        super(OAuthAuthentication, self).__init__(*args, **kwargs) 

+

 

+

        if oauth is None: 

+

            raise ImproperlyConfigured( 

+

                "The 'oauth2' package could not be imported." 

+

                "It is required for use with the 'OAuthAuthentication' class.") 

+

 

+

        if oauth_provider is None: 

+

            raise ImproperlyConfigured( 

+

                "The 'django-oauth-plus' package could not be imported." 

+

                "It is required for use with the 'OAuthAuthentication' class.") 

+

 

+

    def authenticate(self, request): 

+

        """ 

+

        Returns two-tuple of (user, token) if authentication succeeds, 

+

        or None otherwise. 

+

        """ 

+

        try: 

+

            oauth_request = oauth_provider.utils.get_oauth_request(request) 

+

        except oauth.Error as err: 

+

            raise exceptions.AuthenticationFailed(err.message) 

+

 

+

        if not oauth_request: 

+

            return None 

+

 

+

        oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES 

+

 

+

        found = any(param for param in oauth_params if param in oauth_request) 

+

        missing = list(param for param in oauth_params if param not in oauth_request) 

+

 

+

        if not found: 

+

            # OAuth authentication was not attempted. 

+

            return None 

+

 

+

        if missing: 

+

            # OAuth was attempted but missing parameters. 

+

            msg = 'Missing parameters: %s' % (', '.join(missing)) 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        if not self.check_nonce(request, oauth_request): 

+

            msg = 'Nonce check failed' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        try: 

+

            consumer_key = oauth_request.get_parameter('oauth_consumer_key') 

+

            consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key) 

+

        except oauth_provider.store.InvalidConsumerError: 

+

            msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key') 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        if consumer.status != oauth_provider.consts.ACCEPTED: 

+

            msg = 'Invalid consumer key status: %s' % consumer.get_status_display() 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        try: 

+

            token_param = oauth_request.get_parameter('oauth_token') 

+

            token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param) 

+

        except oauth_provider.store.InvalidTokenError: 

+

            msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token') 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        try: 

+

            self.validate_token(request, consumer, token) 

+

        except oauth.Error as err: 

+

            raise exceptions.AuthenticationFailed(err.message) 

+

 

+

        user = token.user 

+

 

+

        if not user.is_active: 

+

            msg = 'User inactive or deleted: %s' % user.username 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        return (token.user, token) 

+

 

+

    def authenticate_header(self, request): 

+

        """ 

+

        If permission is denied, return a '401 Unauthorized' response, 

+

        with an appropraite 'WWW-Authenticate' header. 

+

        """ 

+

        return 'OAuth realm="%s"' % self.www_authenticate_realm 

+

 

+

    def validate_token(self, request, consumer, token): 

+

        """ 

+

        Check the token and raise an `oauth.Error` exception if invalid. 

+

        """ 

+

        oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request) 

+

        oauth_server.verify_request(oauth_request, consumer, token) 

+

 

+

    def check_nonce(self, request, oauth_request): 

+

        """ 

+

        Checks nonce of request, and return True if valid. 

+

        """ 

+

        return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) 

+

 

+

 

+

class OAuth2Authentication(BaseAuthentication): 

+

    """ 

+

    OAuth 2 authentication backend using `django-oauth2-provider` 

+

    """ 

+

    www_authenticate_realm = 'api' 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        super(OAuth2Authentication, self).__init__(*args, **kwargs) 

+

 

+

        if oauth2_provider is None: 

+

            raise ImproperlyConfigured( 

+

                "The 'django-oauth2-provider' package could not be imported. " 

+

                "It is required for use with the 'OAuth2Authentication' class.") 

+

 

+

    def authenticate(self, request): 

+

        """ 

+

        Returns two-tuple of (user, token) if authentication succeeds, 

+

        or None otherwise. 

+

        """ 

+

 

+

        auth = get_authorization_header(request).split() 

+

 

+

        if not auth or auth[0].lower() != b'bearer': 

+

            return None 

+

 

+

        if len(auth) == 1: 

+

            msg = 'Invalid bearer header. No credentials provided.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

        elif len(auth) > 2: 

+

            msg = 'Invalid bearer header. Token string should not contain spaces.' 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        return self.authenticate_credentials(request, auth[1]) 

+

 

+

    def authenticate_credentials(self, request, access_token): 

+

        """ 

+

        Authenticate the request, given the access token. 

+

        """ 

+

 

+

        try: 

+

            token = oauth2_provider.models.AccessToken.objects.select_related('user') 

+

            # TODO: Change to timezone aware datetime when oauth2_provider add 

+

            # support to it. 

+

            token = token.get(token=access_token, expires__gt=datetime.now()) 

+

        except oauth2_provider.models.AccessToken.DoesNotExist: 

+

            raise exceptions.AuthenticationFailed('Invalid token') 

+

 

+

        user = token.user 

+

 

+

        if not user.is_active: 

+

            msg = 'User inactive or deleted: %s' % user.username 

+

            raise exceptions.AuthenticationFailed(msg) 

+

 

+

        return (user, token) 

+

 

+

    def authenticate_header(self, request): 

+

        """ 

+

        Bearer is the only finalized type currently 

+

 

+

        Check details on the `OAuth2Authentication.authenticate` method 

+

        """ 

+

        return 'Bearer realm="%s"' % self.www_authenticate_realm 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_authtoken___init__.html b/htmlcov/rest_framework_authtoken___init__.html new file mode 100644 index 000000000..f72574937 --- /dev/null +++ b/htmlcov/rest_framework_authtoken___init__.html @@ -0,0 +1,81 @@ + + + + + + + + Coverage for rest_framework/authtoken/__init__: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/htmlcov/rest_framework_authtoken_models.html b/htmlcov/rest_framework_authtoken_models.html new file mode 100644 index 000000000..27d2fff1d --- /dev/null +++ b/htmlcov/rest_framework_authtoken_models.html @@ -0,0 +1,151 @@ + + + + + + + + Coverage for rest_framework/authtoken/models: 95% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+ +
+

import uuid 

+

import hmac 

+

from hashlib import sha1 

+

from rest_framework.compat import User 

+

from django.conf import settings 

+

from django.db import models 

+

 

+

 

+

class Token(models.Model): 

+

    """ 

+

    The default authorization token model. 

+

    """ 

+

    key = models.CharField(max_length=40, primary_key=True) 

+

    user = models.OneToOneField(User, related_name='auth_token') 

+

    created = models.DateTimeField(auto_now_add=True) 

+

 

+

    class Meta: 

+

        # Work around for a bug in Django: 

+

        # https://code.djangoproject.com/ticket/19422 

+

        # 

+

        # Also see corresponding ticket: 

+

        # https://github.com/tomchristie/django-rest-framework/issues/705 

+

        abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS 

+

 

+

    def save(self, *args, **kwargs): 

+

        if not self.key: 

+

            self.key = self.generate_key() 

+

        return super(Token, self).save(*args, **kwargs) 

+

 

+

    def generate_key(self): 

+

        unique = uuid.uuid4() 

+

        return hmac.new(unique.bytes, digestmod=sha1).hexdigest() 

+

 

+

    def __unicode__(self): 

+

        return self.key 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_authtoken_serializers.html b/htmlcov/rest_framework_authtoken_serializers.html new file mode 100644 index 000000000..8997d9a7b --- /dev/null +++ b/htmlcov/rest_framework_authtoken_serializers.html @@ -0,0 +1,129 @@ + + + + + + + + Coverage for rest_framework/authtoken/serializers: 88% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+ +
+

from django.contrib.auth import authenticate 

+

from rest_framework import serializers 

+

 

+

 

+

class AuthTokenSerializer(serializers.Serializer): 

+

    username = serializers.CharField() 

+

    password = serializers.CharField() 

+

 

+

    def validate(self, attrs): 

+

        username = attrs.get('username') 

+

        password = attrs.get('password') 

+

 

+

        if username and password: 

+

            user = authenticate(username=username, password=password) 

+

 

+

            if user: 

+

                if not user.is_active: 

+

                    raise serializers.ValidationError('User account is disabled.') 

+

                attrs['user'] = user 

+

                return attrs 

+

            else: 

+

                raise serializers.ValidationError('Unable to login with provided credentials.') 

+

        else: 

+

            raise serializers.ValidationError('Must include "username" and "password"') 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_authtoken_views.html b/htmlcov/rest_framework_authtoken_views.html new file mode 100644 index 000000000..d13746ea8 --- /dev/null +++ b/htmlcov/rest_framework_authtoken_views.html @@ -0,0 +1,133 @@ + + + + + + + + Coverage for rest_framework/authtoken/views: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+ +
+

from rest_framework.views import APIView 

+

from rest_framework import status 

+

from rest_framework import parsers 

+

from rest_framework import renderers 

+

from rest_framework.response import Response 

+

from rest_framework.authtoken.models import Token 

+

from rest_framework.authtoken.serializers import AuthTokenSerializer 

+

 

+

 

+

class ObtainAuthToken(APIView): 

+

    throttle_classes = () 

+

    permission_classes = () 

+

    parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) 

+

    renderer_classes = (renderers.JSONRenderer,) 

+

    serializer_class = AuthTokenSerializer 

+

    model = Token 

+

 

+

    def post(self, request): 

+

        serializer = self.serializer_class(data=request.DATA) 

+

        if serializer.is_valid(): 

+

            token, created = Token.objects.get_or_create(user=serializer.object['user']) 

+

            return Response({'token': token.key}) 

+

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

+

 

+

 

+

obtain_auth_token = ObtainAuthToken.as_view() 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_decorators.html b/htmlcov/rest_framework_decorators.html new file mode 100644 index 000000000..6ad6f6b51 --- /dev/null +++ b/htmlcov/rest_framework_decorators.html @@ -0,0 +1,339 @@ + + + + + + + + Coverage for rest_framework/decorators: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+ +
+

""" 

+

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 

+

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 

+

 

+

 

+

def api_view(http_method_names): 

+

 

+

    """ 

+

    Decorator that converts a function-based view into an APIView subclass. 

+

    Takes a list of allowed methods for the view as an argument. 

+

    """ 

+

 

+

    def decorator(func): 

+

 

+

        WrappedAPIView = type( 

+

            six.PY3 and 'WrappedAPIView' or b'WrappedAPIView', 

+

            (APIView,), 

+

            {'__doc__': func.__doc__} 

+

        ) 

+

 

+

        # Note, the above allows us to set the docstring. 

+

        # It is the equivalent of: 

+

        # 

+

        #     class WrappedAPIView(APIView): 

+

        #         pass 

+

        #     WrappedAPIView.__doc__ = func.doc    <--- Not possible to do this 

+

 

+

        # api_view applied without (method_names) 

+

        assert not(isinstance(http_method_names, types.FunctionType)), \ 

+

            '@api_view missing list of allowed HTTP methods' 

+

 

+

        # api_view applied with eg. string instead of list of strings 

+

        assert isinstance(http_method_names, (list, tuple)), \ 

+

            '@api_view expected a list of strings, received %s' % type(http_method_names).__name__ 

+

 

+

        allowed_methods = set(http_method_names) | set(('options',)) 

+

        WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] 

+

 

+

        def handler(self, *args, **kwargs): 

+

            return func(*args, **kwargs) 

+

 

+

        for method in http_method_names: 

+

            setattr(WrappedAPIView, method.lower(), handler) 

+

 

+

        WrappedAPIView.__name__ = func.__name__ 

+

 

+

        WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes', 

+

                                                  APIView.renderer_classes) 

+

 

+

        WrappedAPIView.parser_classes = getattr(func, 'parser_classes', 

+

                                                APIView.parser_classes) 

+

 

+

        WrappedAPIView.authentication_classes = getattr(func, 'authentication_classes', 

+

                                                        APIView.authentication_classes) 

+

 

+

        WrappedAPIView.throttle_classes = getattr(func, 'throttle_classes', 

+

                                                  APIView.throttle_classes) 

+

 

+

        WrappedAPIView.permission_classes = getattr(func, 'permission_classes', 

+

                                                    APIView.permission_classes) 

+

 

+

        return WrappedAPIView.as_view() 

+

    return decorator 

+

 

+

 

+

def renderer_classes(renderer_classes): 

+

    def decorator(func): 

+

        func.renderer_classes = renderer_classes 

+

        return func 

+

    return decorator 

+

 

+

 

+

def parser_classes(parser_classes): 

+

    def decorator(func): 

+

        func.parser_classes = parser_classes 

+

        return func 

+

    return decorator 

+

 

+

 

+

def authentication_classes(authentication_classes): 

+

    def decorator(func): 

+

        func.authentication_classes = authentication_classes 

+

        return func 

+

    return decorator 

+

 

+

 

+

def throttle_classes(throttle_classes): 

+

    def decorator(func): 

+

        func.throttle_classes = throttle_classes 

+

        return func 

+

    return decorator 

+

 

+

 

+

def permission_classes(permission_classes): 

+

    def decorator(func): 

+

        func.permission_classes = permission_classes 

+

        return func 

+

    return decorator 

+

 

+

 

+

def 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.kwargs = kwargs 

+

        return func 

+

    return decorator 

+

 

+

 

+

def 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.kwargs = kwargs 

+

        return func 

+

    return decorator 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_exceptions.html b/htmlcov/rest_framework_exceptions.html new file mode 100644 index 000000000..d975a8481 --- /dev/null +++ b/htmlcov/rest_framework_exceptions.html @@ -0,0 +1,257 @@ + + + + + + + + Coverage for rest_framework/exceptions: 96% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+ +
+

""" 

+

Handled exceptions raised by REST framework. 

+

 

+

In addition Django's built in 403 and 404 exceptions are handled. 

+

(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) 

+

""" 

+

from __future__ import unicode_literals 

+

from rest_framework import status 

+

 

+

 

+

class APIException(Exception): 

+

    """ 

+

    Base class for REST framework exceptions. 

+

    Subclasses should provide `.status_code` and `.detail` properties. 

+

    """ 

+

    pass 

+

 

+

 

+

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 

+

 

+

 

+

class NotAcceptable(APIException): 

+

    status_code = status.HTTP_406_NOT_ACCEPTABLE 

+

    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 

+

        self.available_renderers = available_renderers 

+

 

+

 

+

class UnsupportedMediaType(APIException): 

+

    status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE 

+

    default_detail = "Unsupported media type '%s' in request." 

+

 

+

    def __init__(self, media_type, detail=None): 

+

        self.detail = (detail or self.default_detail) % media_type 

+

 

+

 

+

class Throttled(APIException): 

+

    status_code = status.HTTP_429_TOO_MANY_REQUESTS 

+

    default_detail = "Request was throttled." 

+

    extra_detail = "Expected available in %d second%s." 

+

 

+

    def __init__(self, wait=None, detail=None): 

+

        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 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_fields.html b/htmlcov/rest_framework_fields.html new file mode 100644 index 000000000..cf2731d25 --- /dev/null +++ b/htmlcov/rest_framework_fields.html @@ -0,0 +1,1991 @@ + + + + + + + + Coverage for rest_framework/fields: 87% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+

530

+

531

+

532

+

533

+

534

+

535

+

536

+

537

+

538

+

539

+

540

+

541

+

542

+

543

+

544

+

545

+

546

+

547

+

548

+

549

+

550

+

551

+

552

+

553

+

554

+

555

+

556

+

557

+

558

+

559

+

560

+

561

+

562

+

563

+

564

+

565

+

566

+

567

+

568

+

569

+

570

+

571

+

572

+

573

+

574

+

575

+

576

+

577

+

578

+

579

+

580

+

581

+

582

+

583

+

584

+

585

+

586

+

587

+

588

+

589

+

590

+

591

+

592

+

593

+

594

+

595

+

596

+

597

+

598

+

599

+

600

+

601

+

602

+

603

+

604

+

605

+

606

+

607

+

608

+

609

+

610

+

611

+

612

+

613

+

614

+

615

+

616

+

617

+

618

+

619

+

620

+

621

+

622

+

623

+

624

+

625

+

626

+

627

+

628

+

629

+

630

+

631

+

632

+

633

+

634

+

635

+

636

+

637

+

638

+

639

+

640

+

641

+

642

+

643

+

644

+

645

+

646

+

647

+

648

+

649

+

650

+

651

+

652

+

653

+

654

+

655

+

656

+

657

+

658

+

659

+

660

+

661

+

662

+

663

+

664

+

665

+

666

+

667

+

668

+

669

+

670

+

671

+

672

+

673

+

674

+

675

+

676

+

677

+

678

+

679

+

680

+

681

+

682

+

683

+

684

+

685

+

686

+

687

+

688

+

689

+

690

+

691

+

692

+

693

+

694

+

695

+

696

+

697

+

698

+

699

+

700

+

701

+

702

+

703

+

704

+

705

+

706

+

707

+

708

+

709

+

710

+

711

+

712

+

713

+

714

+

715

+

716

+

717

+

718

+

719

+

720

+

721

+

722

+

723

+

724

+

725

+

726

+

727

+

728

+

729

+

730

+

731

+

732

+

733

+

734

+

735

+

736

+

737

+

738

+

739

+

740

+

741

+

742

+

743

+

744

+

745

+

746

+

747

+

748

+

749

+

750

+

751

+

752

+

753

+

754

+

755

+

756

+

757

+

758

+

759

+

760

+

761

+

762

+

763

+

764

+

765

+

766

+

767

+

768

+

769

+

770

+

771

+

772

+

773

+

774

+

775

+

776

+

777

+

778

+

779

+

780

+

781

+

782

+

783

+

784

+

785

+

786

+

787

+

788

+

789

+

790

+

791

+

792

+

793

+

794

+

795

+

796

+

797

+

798

+

799

+

800

+

801

+

802

+

803

+

804

+

805

+

806

+

807

+

808

+

809

+

810

+

811

+

812

+

813

+

814

+

815

+

816

+

817

+

818

+

819

+

820

+

821

+

822

+

823

+

824

+

825

+

826

+

827

+

828

+

829

+

830

+

831

+

832

+

833

+

834

+

835

+

836

+

837

+

838

+

839

+

840

+

841

+

842

+

843

+

844

+

845

+

846

+

847

+

848

+

849

+

850

+

851

+

852

+

853

+

854

+

855

+

856

+

857

+

858

+

859

+

860

+

861

+

862

+

863

+

864

+

865

+

866

+

867

+

868

+

869

+

870

+

871

+

872

+

873

+

874

+

875

+

876

+

877

+

878

+

879

+

880

+

881

+

882

+

883

+

884

+

885

+

886

+

887

+

888

+

889

+

890

+

891

+

892

+

893

+

894

+

895

+

896

+

897

+

898

+

899

+

900

+

901

+

902

+

903

+

904

+

905

+

906

+

907

+

908

+

909

+

910

+

911

+

912

+

913

+

914

+

915

+

916

+

917

+

918

+

919

+

920

+

921

+

922

+

923

+

924

+

925

+

926

+

927

+

928

+

929

+

930

+

931

+

932

+

933

+

934

+

935

+

936

+

937

+

938

+

939

+

940

+

941

+

942

+

943

+

944

+

945

+

946

+

947

+

948

+

949

+

950

+

951

+

952

+

953

+

954

+

955

+ +
+

""" 

+

Serializer fields perform validation on incoming data. 

+

 

+

They are very similar to Django's form fields. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

import copy 

+

import datetime 

+

import inspect 

+

import re 

+

import warnings 

+

from decimal import Decimal, DecimalException 

+

from django import forms 

+

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.forms import widgets 

+

from django.utils.encoding import is_protected_type 

+

from django.utils.translation import ugettext_lazy as _ 

+

from django.utils.datastructures import SortedDict 

+

from rest_framework import ISO_8601 

+

from rest_framework.compat import ( 

+

    timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text, 

+

    force_text, is_non_str_iterable 

+

) 

+

from rest_framework.settings import api_settings 

+

 

+

 

+

def is_simple_callable(obj): 

+

    """ 

+

    True if the object is a callable that takes no arguments. 

+

    """ 

+

    function = inspect.isfunction(obj) 

+

    method = inspect.ismethod(obj) 

+

 

+

    if not (function or method): 

+

        return False 

+

 

+

    args, _, _, defaults = inspect.getargspec(obj) 

+

    len_args = len(args) if function else len(args) - 1 

+

    len_defaults = len(defaults) if defaults else 0 

+

    return len_args <= len_defaults 

+

 

+

 

+

def get_component(obj, attr_name): 

+

    """ 

+

    Given an object, and an attribute name, 

+

    return that attribute on the object. 

+

    """ 

+

    if isinstance(obj, dict): 

+

        val = obj.get(attr_name) 

+

    else: 

+

        val = getattr(obj, attr_name) 

+

 

+

    if is_simple_callable(val): 

+

        return val() 

+

    return val 

+

 

+

 

+

def readable_datetime_formats(formats): 

+

    format = ', '.join(formats).replace(ISO_8601, 

+

             'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') 

+

    return humanize_strptime(format) 

+

 

+

 

+

def readable_date_formats(formats): 

+

    format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]') 

+

    return humanize_strptime(format) 

+

 

+

 

+

def readable_time_formats(formats): 

+

    format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') 

+

    return humanize_strptime(format) 

+

 

+

 

+

def humanize_strptime(format_string): 

+

    # Note that we're missing some of the locale specific mappings that 

+

    # don't really make sense. 

+

    mapping = { 

+

        "%Y": "YYYY", 

+

        "%y": "YY", 

+

        "%m": "MM", 

+

        "%b": "[Jan-Dec]", 

+

        "%B": "[January-December]", 

+

        "%d": "DD", 

+

        "%H": "hh", 

+

        "%I": "hh",  # Requires '%p' to differentiate from '%H'. 

+

        "%M": "mm", 

+

        "%S": "ss", 

+

        "%f": "uuuuuu", 

+

        "%a": "[Mon-Sun]", 

+

        "%A": "[Monday-Sunday]", 

+

        "%p": "[AM|PM]", 

+

        "%z": "[+HHMM|-HHMM]" 

+

    } 

+

    for key, val in mapping.items(): 

+

        format_string = format_string.replace(key, val) 

+

    return format_string 

+

 

+

 

+

class Field(object): 

+

    read_only = True 

+

    creation_counter = 0 

+

    empty = '' 

+

    type_name = None 

+

    partial = False 

+

    use_files = False 

+

    form_field_class = forms.CharField 

+

    type_label = 'field' 

+

 

+

    def __init__(self, source=None, label=None, help_text=None): 

+

        self.parent = None 

+

 

+

        self.creation_counter = Field.creation_counter 

+

        Field.creation_counter += 1 

+

 

+

        self.source = source 

+

 

+

        if label is not None: 

+

            self.label = smart_text(label) 

+

 

+

        if help_text is not None: 

+

            self.help_text = smart_text(help_text) 

+

 

+

    def initialize(self, parent, field_name): 

+

        """ 

+

        Called to set up a field prior to field_to_native or field_from_native. 

+

 

+

        parent - The parent serializer. 

+

        model_field - The model field this field corresponds to, if one exists. 

+

        """ 

+

        self.parent = parent 

+

        self.root = parent.root or parent 

+

        self.context = self.root.context 

+

        self.partial = self.root.partial 

+

        if self.partial: 

+

            self.required = False 

+

 

+

    def field_from_native(self, data, files, field_name, into): 

+

        """ 

+

        Given a dictionary and a field name, updates the dictionary `into`, 

+

        with the field and it's deserialized value. 

+

        """ 

+

        return 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        """ 

+

        Given and object and a field name, returns the value that should be 

+

        serialized for that field. 

+

        """ 

+

        if obj is None: 

+

            return self.empty 

+

 

+

        if self.source == '*': 

+

            return self.to_native(obj) 

+

 

+

        source = self.source or field_name 

+

        value = obj 

+

 

+

        for component in source.split('.'): 

+

            value = get_component(value, component) 

+

            if value is None: 

+

                break 

+

 

+

        return self.to_native(value) 

+

 

+

    def to_native(self, value): 

+

        """ 

+

        Converts the field's value into it's simple representation. 

+

        """ 

+

        if is_simple_callable(value): 

+

            value = value() 

+

 

+

        if is_protected_type(value): 

+

            return value 

+

        elif (is_non_str_iterable(value) and 

+

              not isinstance(value, (dict, six.string_types))): 

+

            return [self.to_native(item) for item in value] 

+

        elif isinstance(value, dict): 

+

            # Make sure we preserve field ordering, if it exists 

+

            ret = SortedDict() 

+

            for key, val in value.items(): 

+

                ret[key] = self.to_native(val) 

+

            return ret 

+

        return force_text(value) 

+

 

+

    def attributes(self): 

+

        """ 

+

        Returns a dictionary of attributes to be used when serializing to xml. 

+

        """ 

+

        if self.type_name: 

+

            return {'type': self.type_name} 

+

        return {} 

+

 

+

    def metadata(self): 

+

        metadata = SortedDict() 

+

        metadata['type'] = self.type_label 

+

        metadata['required'] = getattr(self, 'required', False) 

+

        optional_attrs = ['read_only', 'label', 'help_text', 

+

                          'min_length', 'max_length'] 

+

        for attr in optional_attrs: 

+

            value = getattr(self, attr, None) 

+

            if value is not None and value != '': 

+

                metadata[attr] = force_text(value, strings_only=True) 

+

        return metadata 

+

 

+

 

+

class WritableField(Field): 

+

    """ 

+

    Base for read/write fields. 

+

    """ 

+

    default_validators = [] 

+

    default_error_messages = { 

+

        'required': _('This field is required.'), 

+

        'invalid': _('Invalid value.'), 

+

    } 

+

    widget = widgets.TextInput 

+

    default = None 

+

 

+

    def __init__(self, source=None, label=None, help_text=None, 

+

                 read_only=False, required=None, 

+

                 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 

+

        if required is None: 

+

            self.required = not(read_only) 

+

        else: 

+

            assert not (read_only and required), "Cannot set required=True and read_only=True" 

+

            self.required = required 

+

 

+

        messages = {} 

+

        for c in reversed(self.__class__.__mro__): 

+

            messages.update(getattr(c, 'default_error_messages', {})) 

+

        messages.update(error_messages or {}) 

+

        self.error_messages = messages 

+

 

+

        self.validators = self.default_validators + validators 

+

        self.default = default if default is not None else self.default 

+

 

+

        # Widgets are ony used for HTML forms. 

+

        widget = widget or self.widget 

+

        if isinstance(widget, type): 

+

            widget = widget() 

+

        self.widget = widget 

+

 

+

    def __deepcopy__(self, memo): 

+

        result = copy.copy(self) 

+

        memo[id(self)] = result 

+

        result.validators = self.validators[:] 

+

        return result 

+

 

+

    def validate(self, value): 

+

        if value in validators.EMPTY_VALUES and self.required: 

+

            raise ValidationError(self.error_messages['required']) 

+

 

+

    def run_validators(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return 

+

        errors = [] 

+

        for v in self.validators: 

+

            try: 

+

                v(value) 

+

            except ValidationError as e: 

+

                if hasattr(e, 'code') and e.code in self.error_messages: 

+

                    message = self.error_messages[e.code] 

+

                    if e.params: 

+

                        message = message % e.params 

+

                    errors.append(message) 

+

                else: 

+

                    errors.extend(e.messages) 

+

        if errors: 

+

            raise ValidationError(errors) 

+

 

+

    def field_from_native(self, data, files, field_name, into): 

+

        """ 

+

        Given a dictionary and a field name, updates the dictionary `into`, 

+

        with the field and it's deserialized value. 

+

        """ 

+

        if self.read_only: 

+

            return 

+

 

+

        try: 

+

            if self.use_files: 

+

                files = files or {} 

+

                native = files[field_name] 

+

            else: 

+

                native = data[field_name] 

+

        except KeyError: 

+

            if self.default is not None and not self.partial: 

+

                # Note: partial updates shouldn't set defaults 

+

                if is_simple_callable(self.default): 

+

                    native = self.default() 

+

                else: 

+

                    native = self.default 

+

            else: 

+

                if self.required: 

+

                    raise ValidationError(self.error_messages['required']) 

+

                return 

+

 

+

        value = self.from_native(native) 

+

        if self.source == '*': 

+

            if value: 

+

                into.update(value) 

+

        else: 

+

            self.validate(value) 

+

            self.run_validators(value) 

+

            into[self.source or field_name] = value 

+

 

+

    def from_native(self, value): 

+

        """ 

+

        Reverts a simple representation back to the field's value. 

+

        """ 

+

        return value 

+

 

+

 

+

class ModelField(WritableField): 

+

    """ 

+

    A generic field that can be used against an arbitrary model field. 

+

    """ 

+

    def __init__(self, *args, **kwargs): 

+

        try: 

+

            self.model_field = kwargs.pop('model_field') 

+

        except KeyError: 

+

            raise ValueError("ModelField requires 'model_field' kwarg") 

+

 

+

        self.min_length = kwargs.pop('min_length', 

+

                                     getattr(self.model_field, 'min_length', None)) 

+

        self.max_length = kwargs.pop('max_length', 

+

                                     getattr(self.model_field, 'max_length', None)) 

+

        self.min_value = kwargs.pop('min_value', 

+

                                    getattr(self.model_field, 'min_value', None)) 

+

        self.max_value = kwargs.pop('max_value', 

+

                                    getattr(self.model_field, 'max_value', None)) 

+

 

+

        super(ModelField, self).__init__(*args, **kwargs) 

+

 

+

        if self.min_length is not None: 

+

            self.validators.append(validators.MinLengthValidator(self.min_length)) 

+

        if self.max_length is not None: 

+

            self.validators.append(validators.MaxLengthValidator(self.max_length)) 

+

        if self.min_value is not None: 

+

            self.validators.append(validators.MinValueValidator(self.min_value)) 

+

        if self.max_value is not None: 

+

            self.validators.append(validators.MaxValueValidator(self.max_value)) 

+

 

+

    def from_native(self, value): 

+

        rel = getattr(self.model_field, "rel", None) 

+

        if rel is not None: 

+

            return rel.to._meta.get_field(rel.field_name).to_python(value) 

+

        else: 

+

            return self.model_field.to_python(value) 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        value = self.model_field._get_val_from_obj(obj) 

+

        if is_protected_type(value): 

+

            return value 

+

        return self.model_field.value_to_string(obj) 

+

 

+

    def attributes(self): 

+

        return { 

+

            "type": self.model_field.get_internal_type() 

+

        } 

+

 

+

 

+

##### Typed Fields ##### 

+

 

+

class BooleanField(WritableField): 

+

    type_name = 'BooleanField' 

+

    type_label = 'boolean' 

+

    form_field_class = forms.BooleanField 

+

    widget = widgets.CheckboxInput 

+

    default_error_messages = { 

+

        'invalid': _("'%s' value must be either True or False."), 

+

    } 

+

    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 from_native(self, value): 

+

        if value in ('true', 't', 'True', '1'): 

+

            return True 

+

        if value in ('false', 'f', 'False', '0'): 

+

            return False 

+

        return bool(value) 

+

 

+

 

+

class CharField(WritableField): 

+

    type_name = 'CharField' 

+

    type_label = 'string' 

+

    form_field_class = forms.CharField 

+

 

+

    def __init__(self, max_length=None, min_length=None, *args, **kwargs): 

+

        self.max_length, self.min_length = max_length, min_length 

+

        super(CharField, self).__init__(*args, **kwargs) 

+

        if min_length is not None: 

+

            self.validators.append(validators.MinLengthValidator(min_length)) 

+

        if max_length is not None: 

+

            self.validators.append(validators.MaxLengthValidator(max_length)) 

+

 

+

    def from_native(self, value): 

+

        if isinstance(value, six.string_types) or value is None: 

+

            return value 

+

        return smart_text(value) 

+

 

+

 

+

class URLField(CharField): 

+

    type_name = 'URLField' 

+

    type_label = 'url' 

+

 

+

    def __init__(self, **kwargs): 

+

        kwargs['validators'] = [validators.URLValidator()] 

+

        super(URLField, self).__init__(**kwargs) 

+

 

+

 

+

class SlugField(CharField): 

+

    type_name = 'SlugField' 

+

    type_label = 'slug' 

+

    form_field_class = forms.SlugField 

+

 

+

    default_error_messages = { 

+

        'invalid': _("Enter a valid 'slug' consisting of letters, numbers," 

+

                     " underscores or hyphens."), 

+

    } 

+

    default_validators = [validators.validate_slug] 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        super(SlugField, self).__init__(*args, **kwargs) 

+

 

+

 

+

class ChoiceField(WritableField): 

+

    type_name = 'ChoiceField' 

+

    type_label = 'multiple choice' 

+

    form_field_class = forms.ChoiceField 

+

    widget = widgets.Select 

+

    default_error_messages = { 

+

        'invalid_choice': _('Select a valid choice. %(value)s is not one of ' 

+

                            'the available choices.'), 

+

    } 

+

 

+

    def __init__(self, choices=(), *args, **kwargs): 

+

        super(ChoiceField, self).__init__(*args, **kwargs) 

+

        self.choices = choices 

+

        if not self.required: 

+

            self.choices = BLANK_CHOICE_DASH + self.choices 

+

 

+

    def _get_choices(self): 

+

        return self._choices 

+

 

+

    def _set_choices(self, value): 

+

        # Setting choices also sets the choices on the widget. 

+

        # choices can be any iterable, but we call list() on it because 

+

        # it will be consumed more than once. 

+

        self._choices = self.widget.choices = list(value) 

+

 

+

    choices = property(_get_choices, _set_choices) 

+

 

+

    def validate(self, value): 

+

        """ 

+

        Validates that the input is in self.choices. 

+

        """ 

+

        super(ChoiceField, self).validate(value) 

+

        if value and not self.valid_value(value): 

+

            raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) 

+

 

+

    def valid_value(self, value): 

+

        """ 

+

        Check to see if the provided value is a valid choice. 

+

        """ 

+

        for k, v in self.choices: 

+

            if isinstance(v, (list, tuple)): 

+

                # This is an optgroup, so look inside the group for options 

+

                for k2, v2 in v: 

+

                    if value == smart_text(k2): 

+

                        return True 

+

            else: 

+

                if value == smart_text(k) or value == k: 

+

                    return True 

+

        return False 

+

 

+

 

+

class EmailField(CharField): 

+

    type_name = 'EmailField' 

+

    type_label = 'email' 

+

    form_field_class = forms.EmailField 

+

 

+

    default_error_messages = { 

+

        'invalid': _('Enter a valid e-mail address.'), 

+

    } 

+

    default_validators = [validators.validate_email] 

+

 

+

    def from_native(self, value): 

+

        ret = super(EmailField, self).from_native(value) 

+

        if ret is None: 

+

            return None 

+

        return ret.strip() 

+

 

+

 

+

class RegexField(CharField): 

+

    type_name = 'RegexField' 

+

    type_label = 'regex' 

+

    form_field_class = forms.RegexField 

+

 

+

    def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): 

+

        super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) 

+

        self.regex = regex 

+

 

+

    def _get_regex(self): 

+

        return self._regex 

+

 

+

    def _set_regex(self, regex): 

+

        if isinstance(regex, six.string_types): 

+

            regex = re.compile(regex) 

+

        self._regex = regex 

+

        if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: 

+

            self.validators.remove(self._regex_validator) 

+

        self._regex_validator = validators.RegexValidator(regex=regex) 

+

        self.validators.append(self._regex_validator) 

+

 

+

    regex = property(_get_regex, _set_regex) 

+

 

+

 

+

class DateField(WritableField): 

+

    type_name = 'DateField' 

+

    type_label = 'date' 

+

    widget = widgets.DateInput 

+

    form_field_class = forms.DateField 

+

 

+

    default_error_messages = { 

+

        'invalid': _("Date has wrong format. Use one of these formats instead: %s"), 

+

    } 

+

    empty = None 

+

    input_formats = api_settings.DATE_INPUT_FORMATS 

+

    format = api_settings.DATE_FORMAT 

+

 

+

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

+

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

+

        self.format = format if format is not None else self.format 

+

        super(DateField, self).__init__(*args, **kwargs) 

+

 

+

    def from_native(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        if isinstance(value, datetime.datetime): 

+

            if timezone and settings.USE_TZ and timezone.is_aware(value): 

+

                # Convert aware datetimes to the default time zone 

+

                # before casting them to dates (#17742). 

+

                default_timezone = timezone.get_default_timezone() 

+

                value = timezone.make_naive(value, default_timezone) 

+

            return value.date() 

+

        if isinstance(value, datetime.date): 

+

            return value 

+

 

+

        for format in self.input_formats: 

+

            if format.lower() == ISO_8601: 

+

                try: 

+

                    parsed = parse_date(value) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    if parsed is not None: 

+

                        return parsed 

+

            else: 

+

                try: 

+

                    parsed = datetime.datetime.strptime(value, format) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    return parsed.date() 

+

 

+

        msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats) 

+

        raise ValidationError(msg) 

+

 

+

    def to_native(self, value): 

+

        if value is None or self.format is None: 

+

            return value 

+

 

+

        if isinstance(value, datetime.datetime): 

+

            value = value.date() 

+

 

+

        if self.format.lower() == ISO_8601: 

+

            return value.isoformat() 

+

        return value.strftime(self.format) 

+

 

+

 

+

class DateTimeField(WritableField): 

+

    type_name = 'DateTimeField' 

+

    type_label = 'datetime' 

+

    widget = widgets.DateTimeInput 

+

    form_field_class = forms.DateTimeField 

+

 

+

    default_error_messages = { 

+

        'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), 

+

    } 

+

    empty = None 

+

    input_formats = api_settings.DATETIME_INPUT_FORMATS 

+

    format = api_settings.DATETIME_FORMAT 

+

 

+

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

+

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

+

        self.format = format if format is not None else self.format 

+

        super(DateTimeField, self).__init__(*args, **kwargs) 

+

 

+

    def from_native(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        if isinstance(value, datetime.datetime): 

+

            return value 

+

        if isinstance(value, datetime.date): 

+

            value = datetime.datetime(value.year, value.month, value.day) 

+

            if settings.USE_TZ: 

+

                # For backwards compatibility, interpret naive datetimes in 

+

                # local time. This won't work during DST change, but we can't 

+

                # do much about it, so we let the exceptions percolate up the 

+

                # call stack. 

+

                warnings.warn("DateTimeField received a naive datetime (%s)" 

+

                              " while time zone support is active." % value, 

+

                              RuntimeWarning) 

+

                default_timezone = timezone.get_default_timezone() 

+

                value = timezone.make_aware(value, default_timezone) 

+

            return value 

+

 

+

        for format in self.input_formats: 

+

            if format.lower() == ISO_8601: 

+

                try: 

+

                    parsed = parse_datetime(value) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    if parsed is not None: 

+

                        return parsed 

+

            else: 

+

                try: 

+

                    parsed = datetime.datetime.strptime(value, format) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    return parsed 

+

 

+

        msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats) 

+

        raise ValidationError(msg) 

+

 

+

    def to_native(self, value): 

+

        if value is None or self.format is None: 

+

            return value 

+

 

+

        if self.format.lower() == ISO_8601: 

+

            ret = value.isoformat() 

+

            if ret.endswith('+00:00'): 

+

                ret = ret[:-6] + 'Z' 

+

            return ret 

+

        return value.strftime(self.format) 

+

 

+

 

+

class TimeField(WritableField): 

+

    type_name = 'TimeField' 

+

    type_label = 'time' 

+

    widget = widgets.TimeInput 

+

    form_field_class = forms.TimeField 

+

 

+

    default_error_messages = { 

+

        'invalid': _("Time has wrong format. Use one of these formats instead: %s"), 

+

    } 

+

    empty = None 

+

    input_formats = api_settings.TIME_INPUT_FORMATS 

+

    format = api_settings.TIME_FORMAT 

+

 

+

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

+

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

+

        self.format = format if format is not None else self.format 

+

        super(TimeField, self).__init__(*args, **kwargs) 

+

 

+

    def from_native(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        if isinstance(value, datetime.time): 

+

            return value 

+

 

+

        for format in self.input_formats: 

+

            if format.lower() == ISO_8601: 

+

                try: 

+

                    parsed = parse_time(value) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    if parsed is not None: 

+

                        return parsed 

+

            else: 

+

                try: 

+

                    parsed = datetime.datetime.strptime(value, format) 

+

                except (ValueError, TypeError): 

+

                    pass 

+

                else: 

+

                    return parsed.time() 

+

 

+

        msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats) 

+

        raise ValidationError(msg) 

+

 

+

    def to_native(self, value): 

+

        if value is None or self.format is None: 

+

            return value 

+

 

+

        if isinstance(value, datetime.datetime): 

+

            value = value.time() 

+

 

+

        if self.format.lower() == ISO_8601: 

+

            return value.isoformat() 

+

        return value.strftime(self.format) 

+

 

+

 

+

class IntegerField(WritableField): 

+

    type_name = 'IntegerField' 

+

    type_label = 'integer' 

+

    form_field_class = forms.IntegerField 

+

 

+

    default_error_messages = { 

+

        'invalid': _('Enter a whole number.'), 

+

        'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'), 

+

        'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'), 

+

    } 

+

 

+

    def __init__(self, max_value=None, min_value=None, *args, **kwargs): 

+

        self.max_value, self.min_value = max_value, min_value 

+

        super(IntegerField, self).__init__(*args, **kwargs) 

+

 

+

        if max_value is not None: 

+

            self.validators.append(validators.MaxValueValidator(max_value)) 

+

        if min_value is not None: 

+

            self.validators.append(validators.MinValueValidator(min_value)) 

+

 

+

    def from_native(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        try: 

+

            value = int(str(value)) 

+

        except (ValueError, TypeError): 

+

            raise ValidationError(self.error_messages['invalid']) 

+

        return value 

+

 

+

 

+

class FloatField(WritableField): 

+

    type_name = 'FloatField' 

+

    type_label = 'float' 

+

    form_field_class = forms.FloatField 

+

 

+

    default_error_messages = { 

+

        'invalid': _("'%s' value must be a float."), 

+

    } 

+

 

+

    def from_native(self, value): 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        try: 

+

            return float(value) 

+

        except (TypeError, ValueError): 

+

            msg = self.error_messages['invalid'] % value 

+

            raise ValidationError(msg) 

+

 

+

 

+

class DecimalField(WritableField): 

+

    type_name = 'DecimalField' 

+

    type_label = 'decimal' 

+

    form_field_class = forms.DecimalField 

+

 

+

    default_error_messages = { 

+

        'invalid': _('Enter a number.'), 

+

        'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'), 

+

        'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'), 

+

        'max_digits': _('Ensure that there are no more than %s digits in total.'), 

+

        'max_decimal_places': _('Ensure that there are no more than %s decimal places.'), 

+

        'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.') 

+

    } 

+

 

+

    def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): 

+

        self.max_value, self.min_value = max_value, min_value 

+

        self.max_digits, self.decimal_places = max_digits, decimal_places 

+

        super(DecimalField, self).__init__(*args, **kwargs) 

+

 

+

        if max_value is not None: 

+

            self.validators.append(validators.MaxValueValidator(max_value)) 

+

        if min_value is not None: 

+

            self.validators.append(validators.MinValueValidator(min_value)) 

+

 

+

    def from_native(self, value): 

+

        """ 

+

        Validates that the input is a decimal number. Returns a Decimal 

+

        instance. Returns None for empty values. Ensures that there are no more 

+

        than max_digits in the number, and no more than decimal_places digits 

+

        after the decimal point. 

+

        """ 

+

        if value in validators.EMPTY_VALUES: 

+

            return None 

+

        value = smart_text(value).strip() 

+

        try: 

+

            value = Decimal(value) 

+

        except DecimalException: 

+

            raise ValidationError(self.error_messages['invalid']) 

+

        return value 

+

 

+

    def validate(self, value): 

+

        super(DecimalField, self).validate(value) 

+

        if value in validators.EMPTY_VALUES: 

+

            return 

+

        # Check for NaN, Inf and -Inf values. We can't compare directly for NaN, 

+

        # since it is never equal to itself. However, NaN is the only value that 

+

        # isn't equal to itself, so we can use this to identify NaN 

+

        if value != value or value == Decimal("Inf") or value == Decimal("-Inf"): 

+

            raise ValidationError(self.error_messages['invalid']) 

+

        sign, digittuple, exponent = value.as_tuple() 

+

        decimals = abs(exponent) 

+

        # digittuple doesn't include any leading zeros. 

+

        digits = len(digittuple) 

+

        if decimals > digits: 

+

            # We have leading zeros up to or past the decimal point.  Count 

+

            # everything past the decimal point as a digit.  We do not count 

+

            # 0 before the decimal point as a digit since that would mean 

+

            # we would not allow max_digits = decimal_places. 

+

            digits = decimals 

+

        whole_digits = digits - decimals 

+

 

+

        if self.max_digits is not None and digits > self.max_digits: 

+

            raise ValidationError(self.error_messages['max_digits'] % self.max_digits) 

+

        if self.decimal_places is not None and decimals > self.decimal_places: 

+

            raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places) 

+

        if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places): 

+

            raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places)) 

+

        return value 

+

 

+

 

+

class FileField(WritableField): 

+

    use_files = True 

+

    type_name = 'FileField' 

+

    type_label = 'file upload' 

+

    form_field_class = forms.FileField 

+

    widget = widgets.FileInput 

+

 

+

    default_error_messages = { 

+

        'invalid': _("No file was submitted. Check the encoding type on the form."), 

+

        'missing': _("No file was submitted."), 

+

        'empty': _("The submitted file is empty."), 

+

        'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'), 

+

        'contradiction': _('Please either submit a file or check the clear checkbox, not both.') 

+

    } 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        self.max_length = kwargs.pop('max_length', None) 

+

        self.allow_empty_file = kwargs.pop('allow_empty_file', False) 

+

        super(FileField, self).__init__(*args, **kwargs) 

+

 

+

    def from_native(self, data): 

+

        if data in validators.EMPTY_VALUES: 

+

            return None 

+

 

+

        # UploadedFile objects should have name and size attributes. 

+

        try: 

+

            file_name = data.name 

+

            file_size = data.size 

+

        except AttributeError: 

+

            raise ValidationError(self.error_messages['invalid']) 

+

 

+

        if self.max_length is not None and len(file_name) > self.max_length: 

+

            error_values = {'max': self.max_length, 'length': len(file_name)} 

+

            raise ValidationError(self.error_messages['max_length'] % error_values) 

+

        if not file_name: 

+

            raise ValidationError(self.error_messages['invalid']) 

+

        if not self.allow_empty_file and not file_size: 

+

            raise ValidationError(self.error_messages['empty']) 

+

 

+

        return data 

+

 

+

    def to_native(self, value): 

+

        return value.name 

+

 

+

 

+

class ImageField(FileField): 

+

    use_files = True 

+

    type_name = 'ImageField' 

+

    type_label = 'image upload' 

+

    form_field_class = forms.ImageField 

+

 

+

    default_error_messages = { 

+

        'invalid_image': _("Upload a valid image. The file you uploaded was " 

+

                           "either not an image or a corrupted image."), 

+

    } 

+

 

+

    def from_native(self, data): 

+

        """ 

+

        Checks that the file-upload field data contains a valid image (GIF, JPG, 

+

        PNG, possibly others -- whatever the Python Imaging Library supports). 

+

        """ 

+

        f = super(ImageField, self).from_native(data) 

+

        if f is None: 

+

            return None 

+

 

+

        from 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 

+

        # have to read the data into memory. 

+

        if hasattr(data, 'temporary_file_path'): 

+

            file = data.temporary_file_path() 

+

        else: 

+

            if hasattr(data, 'read'): 

+

                file = BytesIO(data.read()) 

+

            else: 

+

                file = BytesIO(data['content']) 

+

 

+

        try: 

+

            # load() could spot a truncated JPEG, but it loads the entire 

+

            # image in memory, which is a DoS vector. See #3848 and #18520. 

+

            # verify() must be called immediately after the constructor. 

+

            Image.open(file).verify() 

+

        except ImportError: 

+

            # Under PyPy, it is possible to import PIL. However, the underlying 

+

            # _imaging C module isn't available, so an ImportError will be 

+

            # raised. Catch and re-raise. 

+

            raise 

+

        except Exception:  # Python Imaging Library doesn't recognize it as an image 

+

            raise ValidationError(self.error_messages['invalid_image']) 

+

        if hasattr(f, 'seek') and callable(f.seek): 

+

            f.seek(0) 

+

        return f 

+

 

+

 

+

class SerializerMethodField(Field): 

+

    """ 

+

    A field that gets its value by calling a method on the serializer it's attached to. 

+

    """ 

+

 

+

    def __init__(self, method_name): 

+

        self.method_name = method_name 

+

        super(SerializerMethodField, self).__init__() 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        value = getattr(self.parent, self.method_name)(obj) 

+

        return self.to_native(value) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_filters.html b/htmlcov/rest_framework_filters.html new file mode 100644 index 000000000..28b6eaae5 --- /dev/null +++ b/htmlcov/rest_framework_filters.html @@ -0,0 +1,367 @@ + + + + + + + + Coverage for rest_framework/filters: 92% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+ +
+

""" 

+

Provides generic filtering backends that can be used to filter the results 

+

returned by list views. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.db import models 

+

from rest_framework.compat import django_filters, six 

+

from functools import reduce 

+

import operator 

+

 

+

FilterSet = django_filters and django_filters.FilterSet or None 

+

 

+

 

+

class BaseFilterBackend(object): 

+

    """ 

+

    A base class from which all filter backend classes should inherit. 

+

    """ 

+

 

+

    def filter_queryset(self, request, queryset, view): 

+

        """ 

+

        Return a filtered queryset. 

+

        """ 

+

        raise NotImplementedError(".filter_queryset() must be overridden.") 

+

 

+

 

+

class DjangoFilterBackend(BaseFilterBackend): 

+

    """ 

+

    A filter backend that uses django-filter. 

+

    """ 

+

    default_filter_set = FilterSet 

+

 

+

    def __init__(self): 

+

        assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' 

+

 

+

    def get_filter_class(self, view, queryset=None): 

+

        """ 

+

        Return the django-filters `FilterSet` used to filter the queryset. 

+

        """ 

+

        filter_class = getattr(view, 'filter_class', None) 

+

        filter_fields = getattr(view, 'filter_fields', None) 

+

 

+

        if filter_class: 

+

            filter_model = filter_class.Meta.model 

+

 

+

            assert issubclass(filter_model, queryset.model), \ 

+

                'FilterSet model %s does not match queryset model %s' % \ 

+

                (filter_model, queryset.model) 

+

 

+

            return filter_class 

+

 

+

        if filter_fields: 

+

            class AutoFilterSet(self.default_filter_set): 

+

                class Meta: 

+

                    model = queryset.model 

+

                    fields = filter_fields 

+

            return AutoFilterSet 

+

 

+

        return None 

+

 

+

    def filter_queryset(self, request, queryset, view): 

+

        filter_class = self.get_filter_class(view, queryset) 

+

 

+

        if filter_class: 

+

            return filter_class(request.QUERY_PARAMS, queryset=queryset).qs 

+

 

+

        return queryset 

+

 

+

 

+

class SearchFilter(BaseFilterBackend): 

+

    search_param = 'search'  # The URL query parameter used for the search. 

+

 

+

    def get_search_terms(self, request): 

+

        """ 

+

        Search terms are set by a ?search=... query parameter, 

+

        and may be comma and/or whitespace delimited. 

+

        """ 

+

        params = request.QUERY_PARAMS.get(self.search_param, '') 

+

        return params.replace(',', ' ').split() 

+

 

+

    def construct_search(self, field_name): 

+

        if field_name.startswith('^'): 

+

            return "%s__istartswith" % field_name[1:] 

+

        elif field_name.startswith('='): 

+

            return "%s__iexact" % field_name[1:] 

+

        elif field_name.startswith('@'): 

+

            return "%s__search" % field_name[1:] 

+

        else: 

+

            return "%s__icontains" % field_name 

+

 

+

    def filter_queryset(self, request, queryset, view): 

+

        search_fields = getattr(view, 'search_fields', None) 

+

 

+

        if not search_fields: 

+

            return queryset 

+

 

+

        orm_lookups = [self.construct_search(str(search_field)) 

+

                       for search_field in search_fields] 

+

 

+

        for search_term in self.get_search_terms(request): 

+

            or_queries = [models.Q(**{orm_lookup: search_term}) 

+

                          for orm_lookup in orm_lookups] 

+

            queryset = queryset.filter(reduce(operator.or_, or_queries)) 

+

 

+

        return queryset 

+

 

+

 

+

class OrderingFilter(BaseFilterBackend): 

+

    ordering_param = 'ordering'  # The URL query parameter used for the ordering. 

+

 

+

    def get_ordering(self, request): 

+

        """ 

+

        Search terms are set by a ?search=... query parameter, 

+

        and may be comma and/or whitespace delimited. 

+

        """ 

+

        params = request.QUERY_PARAMS.get(self.ordering_param) 

+

        if params: 

+

            return [param.strip() for param in params.split(',')] 

+

 

+

    def get_default_ordering(self, view): 

+

        ordering = getattr(view, 'ordering', None) 

+

        if isinstance(ordering, six.string_types): 

+

            return (ordering,) 

+

        return ordering 

+

 

+

    def remove_invalid_fields(self, queryset, ordering): 

+

        field_names = [field.name for field in queryset.model._meta.fields] 

+

        return [term for term in ordering if term.lstrip('-') in field_names] 

+

 

+

    def filter_queryset(self, request, queryset, view): 

+

        ordering = self.get_ordering(request) 

+

 

+

        if ordering: 

+

            # Skip any incorrect parameters 

+

            ordering = self.remove_invalid_fields(queryset, ordering) 

+

 

+

        if not ordering: 

+

            # Use 'ordering' attribtue by default 

+

            ordering = self.get_default_ordering(view) 

+

 

+

        if ordering: 

+

            return queryset.order_by(*ordering) 

+

 

+

        return queryset 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_generics.html b/htmlcov/rest_framework_generics.html new file mode 100644 index 000000000..5f5851cbd --- /dev/null +++ b/htmlcov/rest_framework_generics.html @@ -0,0 +1,1079 @@ + + + + + + + + Coverage for rest_framework/generics: 83% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+ +
+

""" 

+

Generic views that provide commonly needed behaviour. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from django.core.exceptions import ImproperlyConfigured, PermissionDenied 

+

from django.core.paginator import Paginator, InvalidPage 

+

from django.http import Http404 

+

from django.shortcuts import get_object_or_404 as _get_object_or_404 

+

from django.utils.translation import ugettext as _ 

+

from rest_framework import views, mixins, exceptions 

+

from rest_framework.request import clone_request 

+

from rest_framework.settings import api_settings 

+

import warnings 

+

 

+

 

+

def get_object_or_404(queryset, **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) 

+

    except (TypeError, ValueError): 

+

        raise Http404 

+

 

+

 

+

class GenericAPIView(views.APIView): 

+

    """ 

+

    Base class for all other generic views. 

+

    """ 

+

 

+

    # You'll need to either set these attributes, 

+

    # or override `get_queryset()`/`get_serializer_class()`. 

+

    queryset = None 

+

    serializer_class = None 

+

 

+

    # 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. 

+

    model = None 

+

 

+

    # If you want to use object lookups other than pk, set this attribute. 

+

    # For more complex lookup requirements override `get_object()`. 

+

    lookup_field = 'pk' 

+

 

+

    # Pagination settings 

+

    paginate_by = api_settings.PAGINATE_BY 

+

    paginate_by_param = api_settings.PAGINATE_BY_PARAM 

+

    pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS 

+

    page_kwarg = 'page' 

+

 

+

    # The filter backend classes to use for queryset filtering 

+

    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS 

+

 

+

    # The following attributes may be subject to change, 

+

    # and should be considered private API. 

+

    model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS 

+

    paginator_class = Paginator 

+

 

+

    ###################################### 

+

    # These are pending deprecation... 

+

 

+

    pk_url_kwarg = 'pk' 

+

    slug_url_kwarg = 'slug' 

+

    slug_field = 'slug' 

+

    allow_empty = True 

+

    filter_backend = api_settings.FILTER_BACKEND 

+

 

+

    def get_serializer_context(self): 

+

        """ 

+

        Extra context provided to the serializer class. 

+

        """ 

+

        return { 

+

            'request': self.request, 

+

            'format': self.format_kwarg, 

+

            'view': self 

+

        } 

+

 

+

    def get_serializer(self, instance=None, data=None, 

+

                       files=None, many=False, partial=False): 

+

        """ 

+

        Return the serializer instance that should be used for validating and 

+

        deserializing input, and for serializing output. 

+

        """ 

+

        serializer_class = self.get_serializer_class() 

+

        context = self.get_serializer_context() 

+

        return serializer_class(instance, data=data, files=files, 

+

                                many=many, partial=partial, context=context) 

+

 

+

    def get_pagination_serializer(self, page): 

+

        """ 

+

        Return a serializer instance to use with paginated data. 

+

        """ 

+

        class SerializerClass(self.pagination_serializer_class): 

+

            class Meta: 

+

                object_serializer_class = self.get_serializer_class() 

+

 

+

        pagination_serializer_class = SerializerClass 

+

        context = self.get_serializer_context() 

+

        return pagination_serializer_class(instance=page, context=context) 

+

 

+

    def paginate_queryset(self, queryset, page_size=None): 

+

        """ 

+

        Paginate a queryset if required, either returning a page object, 

+

        or `None` if pagination is not configured for this view. 

+

        """ 

+

        deprecated_style = False 

+

        if page_size is not None: 

+

            warnings.warn('The `page_size` parameter to `paginate_queryset()` ' 

+

                          'is due to be 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) 

+

            deprecated_style = True 

+

        else: 

+

            # Determine the required page size. 

+

            # If pagination is not configured, simply return None. 

+

            page_size = self.get_paginate_by() 

+

            if not page_size: 

+

                return None 

+

 

+

        if not self.allow_empty: 

+

            warnings.warn( 

+

                'The `allow_empty` parameter is due to be deprecated. ' 

+

                'To use `allow_empty=False` style behavior, You should override ' 

+

                '`get_queryset()` and explicitly raise a 404 on empty querysets.', 

+

                PendingDeprecationWarning, stacklevel=2 

+

            ) 

+

 

+

        paginator = self.paginator_class(queryset, page_size, 

+

                                         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 

+

        try: 

+

            page_number = int(page) 

+

        except ValueError: 

+

            if page == 'last': 

+

                page_number = paginator.num_pages 

+

            else: 

+

                raise Http404(_("Page is not 'last', nor can it be converted to an int.")) 

+

        try: 

+

            page = paginator.page(page_number) 

+

        except InvalidPage as e: 

+

            raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { 

+

                                'page_number': page_number, 

+

                                'message': str(e) 

+

            }) 

+

 

+

        if deprecated_style: 

+

            return (paginator, page, page.object_list, page.has_other_pages()) 

+

        return page 

+

 

+

    def filter_queryset(self, queryset): 

+

        """ 

+

        Given a queryset, filter it with whichever filter backend is in use. 

+

 

+

        You are unlikely to want to override this method, although you may need 

+

        to call it either from a list view, or from a custom `get_object` 

+

        method if you want to apply the configured filtering backend to the 

+

        default queryset. 

+

        """ 

+

        filter_backends = self.filter_backends or [] 

+

        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` ' 

+

                'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' 

+

                'a *list* of filter backend classes.', 

+

                PendingDeprecationWarning, stacklevel=2 

+

            ) 

+

            filter_backends = [self.filter_backend] 

+

 

+

        for backend in filter_backends: 

+

            queryset = backend().filter_queryset(self.request, queryset, self) 

+

        return queryset 

+

 

+

    ######################## 

+

    ### The following methods provide default implementations 

+

    ### that you may want to override for more complex cases. 

+

 

+

    def get_paginate_by(self, queryset=None): 

+

        """ 

+

        Return the size of pages to use with pagination. 

+

 

+

        If `PAGINATE_BY_PARAM` is set it will attempt to get the page size 

+

        from a named query parameter in the url, eg. ?page_size=100 

+

 

+

        Otherwise defaults to using `self.paginate_by`. 

+

        """ 

+

        if queryset is not None: 

+

            warnings.warn('The `queryset` parameter to `get_paginate_by()` ' 

+

                          'is due to be deprecated.', 

+

                          PendingDeprecationWarning, stacklevel=2) 

+

 

+

        if self.paginate_by_param: 

+

            query_params = self.request.QUERY_PARAMS 

+

            try: 

+

                return int(query_params[self.paginate_by_param]) 

+

            except (KeyError, ValueError): 

+

                pass 

+

 

+

        return self.paginate_by 

+

 

+

    def get_serializer_class(self): 

+

        """ 

+

        Return the class to use for the serializer. 

+

        Defaults to using `self.serializer_class`. 

+

 

+

        You may want to override this if you need to provide different 

+

        serializations depending on the incoming request. 

+

 

+

        (Eg. admins get full serialization, others get basic serialization) 

+

        """ 

+

        serializer_class = self.serializer_class 

+

        if serializer_class is not None: 

+

            return serializer_class 

+

 

+

        assert self.model is not None, \ 

+

            "'%s' should either include a 'serializer_class' attribute, " \ 

+

            "or use the 'model' attribute as a shortcut for " \ 

+

            "automatically generating a serializer class." \ 

+

            % self.__class__.__name__ 

+

 

+

        class DefaultSerializer(self.model_serializer_class): 

+

            class Meta: 

+

                model = self.model 

+

        return DefaultSerializer 

+

 

+

    def get_queryset(self): 

+

        """ 

+

        Get the list of items for this view. 

+

        This must be an iterable, and may be a queryset. 

+

        Defaults to using `self.queryset`. 

+

 

+

        You may want to override this if you need to provide different 

+

        querysets depending on the incoming request. 

+

 

+

        (Eg. return a list of items that is specific to the user) 

+

        """ 

+

        if self.queryset is not None: 

+

            return self.queryset._clone() 

+

 

+

        if self.model is not None: 

+

            return self.model._default_manager.all() 

+

 

+

        raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" 

+

                                    % self.__class__.__name__) 

+

 

+

    def get_object(self, queryset=None): 

+

        """ 

+

        Returns the object the view is displaying. 

+

 

+

        You may want to override this if you need to provide non-standard 

+

        queryset lookups.  Eg if objects are referenced using multiple 

+

        keyword arguments in the url conf. 

+

        """ 

+

        # Determine the base queryset to use. 

+

        if queryset is None: 

+

            queryset = self.filter_queryset(self.get_queryset()) 

+

        else: 

+

            pass  # Deprecation warning 

+

 

+

        # Perform the lookup filtering. 

+

        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} 

+

        elif pk is not None and self.lookup_field == 'pk': 

+

            warnings.warn( 

+

                'The `pk_url_kwarg` attribute is due to be deprecated. ' 

+

                'Use the `lookup_field` attribute instead', 

+

                PendingDeprecationWarning 

+

            ) 

+

            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. ' 

+

                'Use the `lookup_field` attribute instead', 

+

                PendingDeprecationWarning 

+

            ) 

+

            filter_kwargs = {self.slug_field: slug} 

+

        else: 

+

            raise ImproperlyConfigured( 

+

                'Expected view %s to be called with a URL keyword argument ' 

+

                'named "%s". Fix your URL conf, or set the `.lookup_field` ' 

+

                'attribute on the view correctly.' % 

+

                (self.__class__.__name__, self.lookup_field) 

+

            ) 

+

 

+

        obj = get_object_or_404(queryset, **filter_kwargs) 

+

 

+

        # May raise a permission denied 

+

        self.check_object_permissions(self.request, obj) 

+

 

+

        return obj 

+

 

+

    ######################## 

+

    ### The following are placeholder methods, 

+

    ### and are intended to be overridden. 

+

    ### 

+

    ### The are not called by GenericAPIView directly, 

+

    ### but are used by the mixin methods. 

+

 

+

    def pre_save(self, obj): 

+

        """ 

+

        Placeholder method for calling before saving an object. 

+

 

+

        May be used to set attributes on the object that are implicit 

+

        in either the request, or the url. 

+

        """ 

+

        pass 

+

 

+

    def post_save(self, obj, created=False): 

+

        """ 

+

        Placeholder method for calling after saving an object. 

+

        """ 

+

        pass 

+

 

+

    def metadata(self, request): 

+

        """ 

+

        Return a dictionary of metadata about the view. 

+

        Used to return responses for OPTIONS requests. 

+

 

+

        We override the default behavior, and add some extra information 

+

        about the required request body for POST and PUT operations. 

+

        """ 

+

        ret = super(GenericAPIView, self).metadata(request) 

+

 

+

        actions = {} 

+

        for method in ('PUT', 'POST'): 

+

            if method not in self.allowed_methods: 

+

                continue 

+

 

+

            cloned_request = clone_request(request, method) 

+

            try: 

+

                # Test global permissions 

+

                self.check_permissions(cloned_request) 

+

                # Test object permissions 

+

                if method == 'PUT': 

+

                    self.get_object() 

+

            except (exceptions.APIException, PermissionDenied, Http404): 

+

                pass 

+

            else: 

+

                # If user has appropriate permissions for the view, include 

+

                # appropriate metadata about the fields that should be supplied. 

+

                serializer = self.get_serializer() 

+

                actions[method] = serializer.metadata() 

+

 

+

        if actions: 

+

            ret['actions'] = actions 

+

 

+

        return ret 

+

 

+

 

+

########################################################## 

+

### Concrete view classes that provide method handlers ### 

+

### by composing the mixin classes with the base view. ### 

+

########################################################## 

+

 

+

class CreateAPIView(mixins.CreateModelMixin, 

+

                    GenericAPIView): 

+

 

+

    """ 

+

    Concrete view for creating a model instance. 

+

    """ 

+

    def post(self, request, *args, **kwargs): 

+

        return self.create(request, *args, **kwargs) 

+

 

+

 

+

class ListAPIView(mixins.ListModelMixin, 

+

                  GenericAPIView): 

+

    """ 

+

    Concrete view for listing a queryset. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.list(request, *args, **kwargs) 

+

 

+

 

+

class RetrieveAPIView(mixins.RetrieveModelMixin, 

+

                      GenericAPIView): 

+

    """ 

+

    Concrete view for retrieving a model instance. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.retrieve(request, *args, **kwargs) 

+

 

+

 

+

class DestroyAPIView(mixins.DestroyModelMixin, 

+

                     GenericAPIView): 

+

 

+

    """ 

+

    Concrete view for deleting a model instance. 

+

    """ 

+

    def delete(self, request, *args, **kwargs): 

+

        return self.destroy(request, *args, **kwargs) 

+

 

+

 

+

class UpdateAPIView(mixins.UpdateModelMixin, 

+

                    GenericAPIView): 

+

 

+

    """ 

+

    Concrete view for updating a model instance. 

+

    """ 

+

    def put(self, request, *args, **kwargs): 

+

        return self.update(request, *args, **kwargs) 

+

 

+

    def patch(self, request, *args, **kwargs): 

+

        return self.partial_update(request, *args, **kwargs) 

+

 

+

 

+

class ListCreateAPIView(mixins.ListModelMixin, 

+

                        mixins.CreateModelMixin, 

+

                        GenericAPIView): 

+

    """ 

+

    Concrete view for listing a queryset or creating a model instance. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.list(request, *args, **kwargs) 

+

 

+

    def post(self, request, *args, **kwargs): 

+

        return self.create(request, *args, **kwargs) 

+

 

+

 

+

class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, 

+

                            mixins.UpdateModelMixin, 

+

                            GenericAPIView): 

+

    """ 

+

    Concrete view for retrieving, updating a model instance. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.retrieve(request, *args, **kwargs) 

+

 

+

    def put(self, request, *args, **kwargs): 

+

        return self.update(request, *args, **kwargs) 

+

 

+

    def patch(self, request, *args, **kwargs): 

+

        return self.partial_update(request, *args, **kwargs) 

+

 

+

 

+

class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, 

+

                             mixins.DestroyModelMixin, 

+

                             GenericAPIView): 

+

    """ 

+

    Concrete view for retrieving or deleting a model instance. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.retrieve(request, *args, **kwargs) 

+

 

+

    def delete(self, request, *args, **kwargs): 

+

        return self.destroy(request, *args, **kwargs) 

+

 

+

 

+

class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, 

+

                                   mixins.UpdateModelMixin, 

+

                                   mixins.DestroyModelMixin, 

+

                                   GenericAPIView): 

+

    """ 

+

    Concrete view for retrieving, updating or deleting a model instance. 

+

    """ 

+

    def get(self, request, *args, **kwargs): 

+

        return self.retrieve(request, *args, **kwargs) 

+

 

+

    def put(self, request, *args, **kwargs): 

+

        return self.update(request, *args, **kwargs) 

+

 

+

    def patch(self, request, *args, **kwargs): 

+

        return self.partial_update(request, *args, **kwargs) 

+

 

+

    def delete(self, request, *args, **kwargs): 

+

        return self.destroy(request, *args, **kwargs) 

+

 

+

 

+

########################## 

+

### Deprecated classes ### 

+

########################## 

+

 

+

class MultipleObjectAPIView(GenericAPIView): 

+

    def __init__(self, *args, **kwargs): 

+

        warnings.warn( 

+

            'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' 

+

            'You should simply subclass `GenericAPIView` instead.', 

+

            PendingDeprecationWarning, stacklevel=2 

+

        ) 

+

        super(MultipleObjectAPIView, self).__init__(*args, **kwargs) 

+

 

+

 

+

class SingleObjectAPIView(GenericAPIView): 

+

    def __init__(self, *args, **kwargs): 

+

        warnings.warn( 

+

            'Subclassing `SingleObjectAPIView` is due to be deprecated. ' 

+

            'You should simply subclass `GenericAPIView` instead.', 

+

            PendingDeprecationWarning, stacklevel=2 

+

        ) 

+

        super(SingleObjectAPIView, self).__init__(*args, **kwargs) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_mixins.html b/htmlcov/rest_framework_mixins.html new file mode 100644 index 000000000..fa62f2ae8 --- /dev/null +++ b/htmlcov/rest_framework_mixins.html @@ -0,0 +1,449 @@ + + + + + + + + Coverage for rest_framework/mixins: 93% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+ +
+

""" 

+

Basic building blocks for generic class based views. 

+

 

+

We don't bind behaviour to http method handlers yet, 

+

which allows mixin classes to be composed in interesting ways. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from django.http import Http404 

+

from rest_framework import status 

+

from rest_framework.response import Response 

+

from rest_framework.request import clone_request 

+

import warnings 

+

 

+

 

+

def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): 

+

    """ 

+

    Given a model instance, and an optional pk and slug field, 

+

    return the full list of all other field names on that model. 

+

 

+

    For use when performing full_clean on a model instance, 

+

    so we only clean the required fields. 

+

    """ 

+

    include = [] 

+

 

+

    if pk: 

+

        # Pending deprecation 

+

        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 

+

        include.append(slug_field) 

+

 

+

    if lookup_field and lookup_field != 'pk': 

+

        include.append(lookup_field) 

+

 

+

    return [field.name for field in obj._meta.fields if field.name not in include] 

+

 

+

 

+

class CreateModelMixin(object): 

+

    """ 

+

    Create a model instance. 

+

    """ 

+

    def create(self, request, *args, **kwargs): 

+

        serializer = self.get_serializer(data=request.DATA, files=request.FILES) 

+

 

+

        if serializer.is_valid(): 

+

            self.pre_save(serializer.object) 

+

            self.object = serializer.save(force_insert=True) 

+

            self.post_save(self.object, created=True) 

+

            headers = self.get_success_headers(serializer.data) 

+

            return Response(serializer.data, status=status.HTTP_201_CREATED, 

+

                            headers=headers) 

+

 

+

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

+

 

+

    def get_success_headers(self, data): 

+

        try: 

+

            return {'Location': data['url']} 

+

        except (TypeError, KeyError): 

+

            return {} 

+

 

+

 

+

class ListModelMixin(object): 

+

    """ 

+

    List a queryset. 

+

    """ 

+

    empty_error = "Empty list and '%(class_name)s.allow_empty' is False." 

+

 

+

    def list(self, request, *args, **kwargs): 

+

        self.object_list = self.filter_queryset(self.get_queryset()) 

+

 

+

        # Default is to allow empty querysets.  This can be altered by setting 

+

        # `.allow_empty = False`, to raise 404 errors on empty querysets. 

+

        if not self.allow_empty and not self.object_list: 

+

            warnings.warn( 

+

                'The `allow_empty` parameter is due to be deprecated. ' 

+

                'To use `allow_empty=False` style behavior, You should override ' 

+

                '`get_queryset()` and explicitly raise a 404 on empty querysets.', 

+

                PendingDeprecationWarning 

+

            ) 

+

            class_name = self.__class__.__name__ 

+

            error_msg = self.empty_error % {'class_name': class_name} 

+

            raise Http404(error_msg) 

+

 

+

        # Switch between paginated or standard style responses 

+

        page = self.paginate_queryset(self.object_list) 

+

        if page is not None: 

+

            serializer = self.get_pagination_serializer(page) 

+

        else: 

+

            serializer = self.get_serializer(self.object_list, many=True) 

+

 

+

        return Response(serializer.data) 

+

 

+

 

+

class RetrieveModelMixin(object): 

+

    """ 

+

    Retrieve a model instance. 

+

    """ 

+

    def retrieve(self, request, *args, **kwargs): 

+

        self.object = self.get_object() 

+

        serializer = self.get_serializer(self.object) 

+

        return Response(serializer.data) 

+

 

+

 

+

class UpdateModelMixin(object): 

+

    """ 

+

    Update a model instance. 

+

    """ 

+

    def update(self, request, *args, **kwargs): 

+

        partial = kwargs.pop('partial', False) 

+

        self.object = self.get_object_or_none() 

+

 

+

        if self.object is None: 

+

            created = True 

+

            save_kwargs = {'force_insert': True} 

+

            success_status_code = status.HTTP_201_CREATED 

+

        else: 

+

            created = False 

+

            save_kwargs = {'force_update': True} 

+

            success_status_code = status.HTTP_200_OK 

+

 

+

        serializer = self.get_serializer(self.object, data=request.DATA, 

+

                                         files=request.FILES, partial=partial) 

+

 

+

        if serializer.is_valid(): 

+

            self.pre_save(serializer.object) 

+

            self.object = serializer.save(**save_kwargs) 

+

            self.post_save(self.object, created=created) 

+

            return Response(serializer.data, status=success_status_code) 

+

 

+

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

+

 

+

    def partial_update(self, request, *args, **kwargs): 

+

        kwargs['partial'] = True 

+

        return self.update(request, *args, **kwargs) 

+

 

+

    def get_object_or_none(self): 

+

        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')) 

+

 

+

    def pre_save(self, obj): 

+

        """ 

+

        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) 

+

        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 

+

 

+

        if lookup: 

+

            setattr(obj, self.lookup_field, lookup) 

+

 

+

        if pk: 

+

            setattr(obj, 'pk', pk) 

+

 

+

        if slug: 

+

            setattr(obj, slug_field, slug) 

+

 

+

        # Ensure we clean the attributes so that we don't eg return integer 

+

        # pk using a string representation, as provided by the url conf kwarg. 

+

        if hasattr(obj, 'full_clean'): 

+

            exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field) 

+

            obj.full_clean(exclude) 

+

 

+

 

+

class DestroyModelMixin(object): 

+

    """ 

+

    Destroy a model instance. 

+

    """ 

+

    def destroy(self, request, *args, **kwargs): 

+

        obj = self.get_object() 

+

        obj.delete() 

+

        return Response(status=status.HTTP_204_NO_CONTENT) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_models.html b/htmlcov/rest_framework_models.html new file mode 100644 index 000000000..6786c620a --- /dev/null +++ b/htmlcov/rest_framework_models.html @@ -0,0 +1,83 @@ + + + + + + + + Coverage for rest_framework/models: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+ +
+

# Just to keep things like ./manage.py test happy 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_negotiation.html b/htmlcov/rest_framework_negotiation.html new file mode 100644 index 000000000..7ed526c97 --- /dev/null +++ b/htmlcov/rest_framework_negotiation.html @@ -0,0 +1,259 @@ + + + + + + + + Coverage for rest_framework/negotiation: 90% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+ +
+

""" 

+

Content negotiation deals with selecting an appropriate renderer given the 

+

incoming request.  Typically this will be based on the request's Accept header. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.http import Http404 

+

from rest_framework import exceptions 

+

from rest_framework.settings import api_settings 

+

from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches 

+

from rest_framework.utils.mediatypes import _MediaType 

+

 

+

 

+

class BaseContentNegotiation(object): 

+

    def select_parser(self, request, parsers): 

+

        raise NotImplementedError('.select_parser() must be implemented') 

+

 

+

    def select_renderer(self, request, renderers, format_suffix=None): 

+

        raise NotImplementedError('.select_renderer() must be implemented') 

+

 

+

 

+

class DefaultContentNegotiation(BaseContentNegotiation): 

+

    settings = api_settings 

+

 

+

    def select_parser(self, request, parsers): 

+

        """ 

+

        Given a list of parsers and a media type, return the appropriate 

+

        parser to handle the incoming request. 

+

        """ 

+

        for parser in parsers: 

+

            if media_type_matches(parser.media_type, request.content_type): 

+

                return parser 

+

        return None 

+

 

+

    def select_renderer(self, request, renderers, format_suffix=None): 

+

        """ 

+

        Given a request and a list of renderers, return a two-tuple of: 

+

        (renderer, media type). 

+

        """ 

+

        # Allow URL style format override.  eg. "?format=json 

+

        format_query_param = self.settings.URL_FORMAT_OVERRIDE 

+

        format = format_suffix or request.QUERY_PARAMS.get(format_query_param) 

+

 

+

        if format: 

+

            renderers = self.filter_renderers(renderers, format) 

+

 

+

        accepts = self.get_accept_list(request) 

+

 

+

        # Check the acceptable media types against each renderer, 

+

        # attempting more specific media types first 

+

        # NB. The inner loop here isn't as bad as it first looks :) 

+

        #     Worst case is we're looping over len(accept_list) * len(self.renderers) 

+

        for media_type_set in order_by_precedence(accepts): 

+

            for renderer in renderers: 

+

                for media_type in media_type_set: 

+

                    if media_type_matches(renderer.media_type, media_type): 

+

                        # Return the most specific media type as accepted. 

+

                        if (_MediaType(renderer.media_type).precedence > 

+

                            _MediaType(media_type).precedence): 

+

                            # Eg client requests '*/*' 

+

                            # Accepted media type is 'application/json' 

+

                            return renderer, renderer.media_type 

+

                        else: 

+

                            # Eg client requests 'application/json; indent=8' 

+

                            # Accepted media type is 'application/json; indent=8' 

+

                            return renderer, media_type 

+

 

+

        raise exceptions.NotAcceptable(available_renderers=renderers) 

+

 

+

    def filter_renderers(self, renderers, format): 

+

        """ 

+

        If there is a '.json' style format suffix, filter the renderers 

+

        so that we only negotiation against those that accept that format. 

+

        """ 

+

        renderers = [renderer for renderer in renderers 

+

                     if renderer.format == format] 

+

        if not renderers: 

+

            raise Http404 

+

        return renderers 

+

 

+

    def get_accept_list(self, request): 

+

        """ 

+

        Given the incoming request, return a tokenised list of media 

+

        type strings. 

+

 

+

        Allows URL style accept override.  eg. "?accept=application/json" 

+

        """ 

+

        header = request.META.get('HTTP_ACCEPT', '*/*') 

+

        header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) 

+

        return [token.strip() for token in header.split(',')] 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_pagination.html b/htmlcov/rest_framework_pagination.html new file mode 100644 index 000000000..5a3f76d82 --- /dev/null +++ b/htmlcov/rest_framework_pagination.html @@ -0,0 +1,269 @@ + + + + + + + + Coverage for rest_framework/pagination: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+ +
+

""" 

+

Pagination serializers determine the structure of the output that should 

+

be used for paginated responses. 

+

""" 

+

from __future__ import unicode_literals 

+

from rest_framework import serializers 

+

from rest_framework.templatetags.rest_framework import replace_query_param 

+

 

+

 

+

class NextPageField(serializers.Field): 

+

    """ 

+

    Field that returns a link to the next page in paginated results. 

+

    """ 

+

    page_field = 'page' 

+

 

+

    def to_native(self, value): 

+

        if not value.has_next(): 

+

            return None 

+

        page = value.next_page_number() 

+

        request = self.context.get('request') 

+

        url = request and request.build_absolute_uri() or '' 

+

        return replace_query_param(url, self.page_field, page) 

+

 

+

 

+

class PreviousPageField(serializers.Field): 

+

    """ 

+

    Field that returns a link to the previous page in paginated results. 

+

    """ 

+

    page_field = 'page' 

+

 

+

    def to_native(self, value): 

+

        if not value.has_previous(): 

+

            return None 

+

        page = value.previous_page_number() 

+

        request = self.context.get('request') 

+

        url = request and request.build_absolute_uri() or '' 

+

        return replace_query_param(url, self.page_field, page) 

+

 

+

 

+

class DefaultObjectSerializer(serializers.Field): 

+

    """ 

+

    If no object serializer is specified, then this serializer will be applied 

+

    as the default. 

+

    """ 

+

 

+

    def __init__(self, source=None, context=None): 

+

        # Note: Swallow context kwarg - only required for eg. ModelSerializer. 

+

        super(DefaultObjectSerializer, self).__init__(source=source) 

+

 

+

 

+

class PaginationSerializerOptions(serializers.SerializerOptions): 

+

    """ 

+

    An object that stores the options that may be provided to a 

+

    pagination serializer by using the inner `Meta` class. 

+

 

+

    Accessible on the instance as `serializer.opts`. 

+

    """ 

+

    def __init__(self, meta): 

+

        super(PaginationSerializerOptions, self).__init__(meta) 

+

        self.object_serializer_class = getattr(meta, 'object_serializer_class', 

+

                                               DefaultObjectSerializer) 

+

 

+

 

+

class BasePaginationSerializer(serializers.Serializer): 

+

    """ 

+

    A base class for pagination serializers to inherit from, 

+

    to make implementing custom serializers more easy. 

+

    """ 

+

    _options_class = PaginationSerializerOptions 

+

    results_field = 'results' 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        """ 

+

        Override init to add in the object serializer field on-the-fly. 

+

        """ 

+

        super(BasePaginationSerializer, self).__init__(*args, **kwargs) 

+

        results_field = self.results_field 

+

        object_serializer = self.opts.object_serializer_class 

+

 

+

        if 'context' in kwargs: 

+

            context_kwarg = {'context': kwargs['context']} 

+

        else: 

+

            context_kwarg = {} 

+

 

+

        self.fields[results_field] = object_serializer(source='object_list', **context_kwarg) 

+

 

+

 

+

class PaginationSerializer(BasePaginationSerializer): 

+

    """ 

+

    A default implementation of a pagination serializer. 

+

    """ 

+

    count = serializers.Field(source='paginator.count') 

+

    next = NextPageField(source='*') 

+

    previous = PreviousPageField(source='*') 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_parsers.html b/htmlcov/rest_framework_parsers.html new file mode 100644 index 000000000..92f1db62d --- /dev/null +++ b/htmlcov/rest_framework_parsers.html @@ -0,0 +1,671 @@ + + + + + + + + Coverage for rest_framework/parsers: 92% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+ +
+

""" 

+

Parsers are used to parse the content of incoming HTTP requests. 

+

 

+

They give us a generic way of being able to handle various media types 

+

on the request, such as form content or json encoded data. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.conf import settings 

+

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.exceptions import ParseError 

+

from rest_framework.compat import six 

+

import json 

+

import datetime 

+

import decimal 

+

 

+

 

+

class DataAndFiles(object): 

+

    def __init__(self, data, files): 

+

        self.data = data 

+

        self.files = files 

+

 

+

 

+

class BaseParser(object): 

+

    """ 

+

    All parsers should extend `BaseParser`, specifying a `media_type` 

+

    attribute, and overriding the `.parse()` method. 

+

    """ 

+

 

+

    media_type = None 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Given a stream to read from, return the parsed representation. 

+

        Should return parsed data, or a `DataAndFiles` object consisting of the 

+

        parsed data and files. 

+

        """ 

+

        raise NotImplementedError(".parse() must be overridden.") 

+

 

+

 

+

class JSONParser(BaseParser): 

+

    """ 

+

    Parses JSON-serialized data. 

+

    """ 

+

 

+

    media_type = 'application/json' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Returns a 2-tuple of `(data, files)`. 

+

 

+

        `data` will be an object which is the parsed content of the response. 

+

        `files` will always be `None`. 

+

        """ 

+

        parser_context = parser_context or {} 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

 

+

        try: 

+

            data = stream.read().decode(encoding) 

+

            return json.loads(data) 

+

        except ValueError as exc: 

+

            raise ParseError('JSON parse error - %s' % six.text_type(exc)) 

+

 

+

 

+

class YAMLParser(BaseParser): 

+

    """ 

+

    Parses YAML-serialized data. 

+

    """ 

+

 

+

    media_type = 'application/yaml' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Returns a 2-tuple of `(data, files)`. 

+

 

+

        `data` will be an object which is the parsed content of the response. 

+

        `files` will always be `None`. 

+

        """ 

+

        assert yaml, 'YAMLParser requires pyyaml to be installed' 

+

 

+

        parser_context = parser_context or {} 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

 

+

        try: 

+

            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)) 

+

 

+

 

+

class FormParser(BaseParser): 

+

    """ 

+

    Parser for form data. 

+

    """ 

+

 

+

    media_type = 'application/x-www-form-urlencoded' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Returns a 2-tuple of `(data, files)`. 

+

 

+

        `data` will be a :class:`QueryDict` containing all the form parameters. 

+

        `files` will always be :const:`None`. 

+

        """ 

+

        parser_context = parser_context or {} 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

        data = QueryDict(stream.read(), encoding=encoding) 

+

        return data 

+

 

+

 

+

class MultiPartParser(BaseParser): 

+

    """ 

+

    Parser for multipart form data, which may include file data. 

+

    """ 

+

 

+

    media_type = 'multipart/form-data' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Returns a DataAndFiles object. 

+

 

+

        `.data` will be a `QueryDict` containing all the form parameters. 

+

        `.files` will be a `QueryDict` containing all the form files. 

+

        """ 

+

        parser_context = parser_context or {} 

+

        request = parser_context['request'] 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

        meta = request.META 

+

        upload_handlers = request.upload_handlers 

+

 

+

        try: 

+

            parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding) 

+

            data, files = parser.parse() 

+

            return DataAndFiles(data, files) 

+

        except MultiPartParserError as exc: 

+

            raise ParseError('Multipart form parse error - %s' % six.u(exc)) 

+

 

+

 

+

class XMLParser(BaseParser): 

+

    """ 

+

    XML parser. 

+

    """ 

+

 

+

    media_type = 'application/xml' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        assert etree, 'XMLParser requires defusedxml to be installed' 

+

 

+

        parser_context = parser_context or {} 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

        parser = etree.DefusedXMLParser(encoding=encoding) 

+

        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)) 

+

        data = self._xml_convert(tree.getroot()) 

+

 

+

        return data 

+

 

+

    def _xml_convert(self, element): 

+

        """ 

+

        convert the xml `element` into the corresponding python object 

+

        """ 

+

 

+

        children = list(element) 

+

 

+

        if len(children) == 0: 

+

            return self._type_convert(element.text) 

+

        else: 

+

            # if the fist child tag is list-item means all children are list-item 

+

            if children[0].tag == "list-item": 

+

                data = [] 

+

                for child in children: 

+

                    data.append(self._xml_convert(child)) 

+

            else: 

+

                data = {} 

+

                for child in children: 

+

                    data[child.tag] = self._xml_convert(child) 

+

 

+

            return data 

+

 

+

    def _type_convert(self, value): 

+

        """ 

+

        Converts the value returned by the XMl parse into the equivalent 

+

        Python type 

+

        """ 

+

        if value is None: 

+

            return value 

+

 

+

        try: 

+

            return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') 

+

        except ValueError: 

+

            pass 

+

 

+

        try: 

+

            return int(value) 

+

        except ValueError: 

+

            pass 

+

 

+

        try: 

+

            return decimal.Decimal(value) 

+

        except decimal.InvalidOperation: 

+

            pass 

+

 

+

        return value 

+

 

+

 

+

class FileUploadParser(BaseParser): 

+

    """ 

+

    Parser for file upload data. 

+

    """ 

+

    media_type = '*/*' 

+

 

+

    def parse(self, stream, media_type=None, parser_context=None): 

+

        """ 

+

        Returns a DataAndFiles object. 

+

 

+

        `.data` will be None (we expect request body to be a file content). 

+

        `.files` will be a `QueryDict` containing one 'file' element. 

+

        """ 

+

 

+

        parser_context = parser_context or {} 

+

        request = parser_context['request'] 

+

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

+

        meta = request.META 

+

        upload_handlers = request.upload_handlers 

+

        filename = self.get_filename(stream, media_type, parser_context) 

+

 

+

        # Note that this code is extracted from Django's handling of 

+

        # file uploads in MultiPartParser. 

+

        content_type = meta.get('HTTP_CONTENT_TYPE', 

+

                                meta.get('CONTENT_TYPE', '')) 

+

        try: 

+

            content_length = int(meta.get('HTTP_CONTENT_LENGTH', 

+

                                          meta.get('CONTENT_LENGTH', 0))) 

+

        except (ValueError, TypeError): 

+

            content_length = None 

+

 

+

        # See if the handler will want to take care of the parsing. 

+

        for handler in upload_handlers: 

+

            result = handler.handle_raw_input(None, 

+

                                              meta, 

+

                                              content_length, 

+

                                              None, 

+

                                              encoding) 

+

            if result is not None: 

+

                return DataAndFiles(None, {'file': result[1]}) 

+

 

+

        # This is the standard case. 

+

        possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] 

+

        chunk_size = min([2 ** 31 - 4] + possible_sizes) 

+

        chunks = ChunkIter(stream, chunk_size) 

+

        counters = [0] * len(upload_handlers) 

+

 

+

        for handler in upload_handlers: 

+

            try: 

+

                handler.new_file(None, filename, content_type, 

+

                                 content_length, encoding) 

+

            except StopFutureHandlers: 

+

                break 

+

 

+

        for chunk in chunks: 

+

            for i, handler in enumerate(upload_handlers): 

+

                chunk_length = len(chunk) 

+

                chunk = handler.receive_data_chunk(chunk, counters[i]) 

+

                counters[i] += chunk_length 

+

                if chunk is None: 

+

                    break 

+

 

+

        for i, handler in enumerate(upload_handlers): 

+

            file_obj = handler.file_complete(counters[i]) 

+

            if file_obj: 

+

                return DataAndFiles(None, {'file': file_obj}) 

+

        raise ParseError("FileUpload parse error - " 

+

                         "none of upload handlers can handle the stream") 

+

 

+

    def get_filename(self, stream, media_type, parser_context): 

+

        """ 

+

        Detects the uploaded file name. First searches a 'filename' url kwarg. 

+

        Then tries to parse Content-Disposition header. 

+

        """ 

+

        try: 

+

            return parser_context['kwargs']['filename'] 

+

        except KeyError: 

+

            pass 

+

 

+

        try: 

+

            meta = parser_context['request'].META 

+

            disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION']) 

+

            return disposition[1]['filename'] 

+

        except (AttributeError, KeyError): 

+

            pass 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_permissions.html b/htmlcov/rest_framework_permissions.html new file mode 100644 index 000000000..20a29522b --- /dev/null +++ b/htmlcov/rest_framework_permissions.html @@ -0,0 +1,429 @@ + + + + + + + + Coverage for rest_framework/permissions: 81% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+ +
+

""" 

+

Provides a set of pluggable permission policies. 

+

""" 

+

from __future__ import unicode_literals 

+

import inspect 

+

import warnings 

+

 

+

SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] 

+

 

+

from rest_framework.compat import oauth2_provider_scope, oauth2_constants 

+

 

+

 

+

class BasePermission(object): 

+

    """ 

+

    A base class from which all permission classes should inherit. 

+

    """ 

+

 

+

    def has_permission(self, request, view): 

+

        """ 

+

        Return `True` if permission is granted, `False` otherwise. 

+

        """ 

+

        return True 

+

 

+

    def has_object_permission(self, request, view, obj): 

+

        """ 

+

        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 

+

 

+

 

+

class AllowAny(BasePermission): 

+

    """ 

+

    Allow any access. 

+

    This isn't strictly required, since you could use an empty 

+

    permission_classes list, but it's useful because it makes the intention 

+

    more explicit. 

+

    """ 

+

    def has_permission(self, request, view): 

+

        return True 

+

 

+

 

+

class IsAuthenticated(BasePermission): 

+

    """ 

+

    Allows access only to authenticated users. 

+

    """ 

+

 

+

    def has_permission(self, request, view): 

+

        if request.user and request.user.is_authenticated(): 

+

            return True 

+

        return False 

+

 

+

 

+

class IsAdminUser(BasePermission): 

+

    """ 

+

    Allows access only to admin users. 

+

    """ 

+

 

+

    def has_permission(self, request, view): 

+

        if request.user and request.user.is_staff: 

+

            return True 

+

        return False 

+

 

+

 

+

class IsAuthenticatedOrReadOnly(BasePermission): 

+

    """ 

+

    The request is authenticated as a user, or is a read-only request. 

+

    """ 

+

 

+

    def has_permission(self, request, view): 

+

        if (request.method in SAFE_METHODS or 

+

            request.user and 

+

            request.user.is_authenticated()): 

+

            return True 

+

        return False 

+

 

+

 

+

class DjangoModelPermissions(BasePermission): 

+

    """ 

+

    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 model. 

+

 

+

    This permission can only be applied against view classes that 

+

    provide a `.model` or `.queryset` attribute. 

+

    """ 

+

 

+

    # Map methods into required permission codes. 

+

    # Override this if you need to also provide 'view' permissions, 

+

    # or if you want to provide custom permission codes. 

+

    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'], 

+

    } 

+

 

+

    authenticated_users_only = True 

+

 

+

    def get_required_permissions(self, method, model_cls): 

+

        """ 

+

        Given a model and an HTTP method, return the list of permission 

+

        codes that the user is required to have. 

+

        """ 

+

        kwargs = { 

+

            'app_label': model_cls._meta.app_label, 

+

            'model_name': model_cls._meta.module_name 

+

        } 

+

        return [perm % kwargs for perm in self.perms_map[method]] 

+

 

+

    def has_permission(self, request, 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 

+

 

+

        # Workaround to ensure DjangoModelPermissions are not applied 

+

        # to the root view when using DefaultRouter. 

+

        if model_cls is None and getattr(view, '_ignore_model_permissions', False): 

+

            return True 

+

 

+

        assert model_cls, ('Cannot apply DjangoModelPermissions on a view that' 

+

                           ' does not have `.model` or `.queryset` property.') 

+

 

+

        perms = self.get_required_permissions(request.method, model_cls) 

+

 

+

        if (request.user and 

+

            (request.user.is_authenticated() or not self.authenticated_users_only) and 

+

            request.user.has_perms(perms)): 

+

            return True 

+

        return False 

+

 

+

 

+

class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): 

+

    """ 

+

    Similar to DjangoModelPermissions, except that anonymous users are 

+

    allowed read-only access. 

+

    """ 

+

    authenticated_users_only = False 

+

 

+

 

+

class TokenHasReadWriteScope(BasePermission): 

+

    """ 

+

    The request is authenticated as a user and the token used has the right scope 

+

    """ 

+

 

+

    def has_permission(self, request, view): 

+

        token = request.auth 

+

        read_only = request.method in SAFE_METHODS 

+

 

+

        if not token: 

+

            return False 

+

 

+

        if hasattr(token, 'resource'):  # OAuth 1 

+

            return read_only or not request.auth.resource.is_readonly 

+

        elif hasattr(token, 'scope'):  # OAuth 2 

+

            required = oauth2_constants.READ if read_only else oauth2_constants.WRITE 

+

            return oauth2_provider_scope.check(required, request.auth.scope) 

+

 

+

        assert False, ('TokenHasReadWriteScope requires either the' 

+

        '`OAuthAuthentication` or `OAuth2Authentication` authentication ' 

+

        'class to be used.') 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_relations.html b/htmlcov/rest_framework_relations.html new file mode 100644 index 000000000..29ad3cf65 --- /dev/null +++ b/htmlcov/rest_framework_relations.html @@ -0,0 +1,1347 @@ + + + + + + + + Coverage for rest_framework/relations: 76% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+

530

+

531

+

532

+

533

+

534

+

535

+

536

+

537

+

538

+

539

+

540

+

541

+

542

+

543

+

544

+

545

+

546

+

547

+

548

+

549

+

550

+

551

+

552

+

553

+

554

+

555

+

556

+

557

+

558

+

559

+

560

+

561

+

562

+

563

+

564

+

565

+

566

+

567

+

568

+

569

+

570

+

571

+

572

+

573

+

574

+

575

+

576

+

577

+

578

+

579

+

580

+

581

+

582

+

583

+

584

+

585

+

586

+

587

+

588

+

589

+

590

+

591

+

592

+

593

+

594

+

595

+

596

+

597

+

598

+

599

+

600

+

601

+

602

+

603

+

604

+

605

+

606

+

607

+

608

+

609

+

610

+

611

+

612

+

613

+

614

+

615

+

616

+

617

+

618

+

619

+

620

+

621

+

622

+

623

+

624

+

625

+

626

+

627

+

628

+

629

+

630

+

631

+

632

+

633

+ +
+

""" 

+

Serializer fields that deal with relationships. 

+

 

+

These fields allow you to specify the style that should be used to represent 

+

model relationships, including hyperlinks, primary keys, or slugs. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.core.exceptions import ObjectDoesNotExist, ValidationError 

+

from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch 

+

from django import forms 

+

from django.db.models.fields import BLANK_CHOICE_DASH 

+

from django.forms import widgets 

+

from django.forms.models import ModelChoiceIterator 

+

from django.utils.translation import ugettext_lazy as _ 

+

from rest_framework.fields import Field, WritableField, get_component, is_simple_callable 

+

from rest_framework.reverse import reverse 

+

from rest_framework.compat import urlparse 

+

from rest_framework.compat import smart_text 

+

import warnings 

+

 

+

 

+

##### Relational fields ##### 

+

 

+

 

+

# Not actually Writable, but subclasses may need to be. 

+

class RelatedField(WritableField): 

+

    """ 

+

    Base class for related model fields. 

+

 

+

    This represents a relationship using the unicode representation of the target. 

+

    """ 

+

    widget = widgets.Select 

+

    many_widget = widgets.SelectMultiple 

+

    form_field_class = forms.ChoiceField 

+

    many_form_field_class = forms.MultipleChoiceField 

+

 

+

    cache_choices = False 

+

    empty_label = None 

+

    read_only = True 

+

    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: 

+

            self.widget = self.many_widget 

+

            self.form_field_class = self.many_form_field_class 

+

 

+

        kwargs['read_only'] = kwargs.pop('read_only', self.read_only) 

+

        super(RelatedField, self).__init__(*args, **kwargs) 

+

 

+

        if not self.required: 

+

            self.empty_label = BLANK_CHOICE_DASH[0][1] 

+

 

+

        self.queryset = queryset 

+

 

+

    def initialize(self, parent, field_name): 

+

        super(RelatedField, self).initialize(parent, field_name) 

+

        if self.queryset is None and not self.read_only: 

+

            try: 

+

                manager = getattr(self.parent.opts.model, self.source or field_name) 

+

                if hasattr(manager, 'related'):  # Forward 

+

                    self.queryset = manager.related.model._default_manager.all() 

+

                else:  # Reverse 

+

                    self.queryset = manager.field.rel.to._default_manager.all() 

+

            except Exception: 

+

                msg = ('Serializer related fields must include a `queryset`' + 

+

                       ' argument or set `read_only=True') 

+

                raise Exception(msg) 

+

 

+

    ### We need this stuff to make form choices work... 

+

 

+

    def prepare_value(self, obj): 

+

        return self.to_native(obj) 

+

 

+

    def label_from_instance(self, obj): 

+

        """ 

+

        Return a readable representation for use with eg. select widgets. 

+

        """ 

+

        desc = smart_text(obj) 

+

        ident = smart_text(self.to_native(obj)) 

+

        if desc == ident: 

+

            return desc 

+

        return "%s - %s" % (desc, ident) 

+

 

+

    def _get_queryset(self): 

+

        return self._queryset 

+

 

+

    def _set_queryset(self, queryset): 

+

        self._queryset = queryset 

+

        self.widget.choices = self.choices 

+

 

+

    queryset = property(_get_queryset, _set_queryset) 

+

 

+

    def _get_choices(self): 

+

        # If self._choices is set, then somebody must have manually set 

+

        # the property self.choices. In this case, just return self._choices. 

+

        if hasattr(self, '_choices'): 

+

            return self._choices 

+

 

+

        # Otherwise, execute the QuerySet in self.queryset to determine the 

+

        # choices dynamically. Return a fresh ModelChoiceIterator that has not been 

+

        # consumed. Note that we're instantiating a new ModelChoiceIterator *each* 

+

        # time _get_choices() is called (and, thus, each time self.choices is 

+

        # accessed) so that we can ensure the QuerySet has not been consumed. This 

+

        # construct might look complicated but it allows for lazy evaluation of 

+

        # the queryset. 

+

        return ModelChoiceIterator(self) 

+

 

+

    def _set_choices(self, value): 

+

        # Setting choices also sets the choices on the widget. 

+

        # choices can be any iterable, but we call list() on it because 

+

        # it will be consumed more than once. 

+

        self._choices = self.widget.choices = list(value) 

+

 

+

    choices = property(_get_choices, _set_choices) 

+

 

+

    ### Regular serializer stuff... 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        try: 

+

            if self.source == '*': 

+

                return self.to_native(obj) 

+

 

+

            source = self.source or field_name 

+

            value = obj 

+

 

+

            for component in source.split('.'): 

+

                value = get_component(value, component) 

+

                if value is None: 

+

                    break 

+

        except ObjectDoesNotExist: 

+

            return None 

+

 

+

        if value is None: 

+

            return None 

+

 

+

        if self.many: 

+

            if is_simple_callable(getattr(value, 'all', None)): 

+

                return [self.to_native(item) for item in value.all()] 

+

            else: 

+

                # Also support non-queryset iterables. 

+

                # This allows us to also support plain lists of related items. 

+

                return [self.to_native(item) for item in value] 

+

        return self.to_native(value) 

+

 

+

    def field_from_native(self, data, files, field_name, into): 

+

        if self.read_only: 

+

            return 

+

 

+

        try: 

+

            if self.many: 

+

                try: 

+

                    # Form data 

+

                    value = data.getlist(field_name) 

+

                    if value == [''] or value == []: 

+

                        raise KeyError 

+

                except AttributeError: 

+

                    # Non-form data 

+

                    value = data[field_name] 

+

            else: 

+

                value = data[field_name] 

+

        except KeyError: 

+

            if self.partial: 

+

                return 

+

            value = [] if self.many else None 

+

 

+

        if value in (None, '') and self.required: 

+

            raise ValidationError(self.error_messages['required']) 

+

        elif value in (None, ''): 

+

            into[(self.source or field_name)] = None 

+

        elif self.many: 

+

            into[(self.source or field_name)] = [self.from_native(item) for item in value] 

+

        else: 

+

            into[(self.source or field_name)] = self.from_native(value) 

+

 

+

 

+

### PrimaryKey relationships 

+

 

+

class PrimaryKeyRelatedField(RelatedField): 

+

    """ 

+

    Represents a relationship as a pk value. 

+

    """ 

+

    read_only = False 

+

 

+

    default_error_messages = { 

+

        'does_not_exist': _("Invalid pk '%s' - object does not exist."), 

+

        'incorrect_type': _('Incorrect type.  Expected pk value, received %s.'), 

+

    } 

+

 

+

    # TODO: Remove these field hacks... 

+

    def prepare_value(self, obj): 

+

        return self.to_native(obj.pk) 

+

 

+

    def label_from_instance(self, obj): 

+

        """ 

+

        Return a readable representation for use with eg. select widgets. 

+

        """ 

+

        desc = smart_text(obj) 

+

        ident = smart_text(self.to_native(obj.pk)) 

+

        if desc == ident: 

+

            return desc 

+

        return "%s - %s" % (desc, ident) 

+

 

+

    # TODO: Possibly change this to just take `obj`, through prob less performant 

+

    def to_native(self, pk): 

+

        return pk 

+

 

+

    def from_native(self, data): 

+

        if self.queryset is None: 

+

            raise Exception('Writable related fields must include a `queryset` argument') 

+

 

+

        try: 

+

            return self.queryset.get(pk=data) 

+

        except ObjectDoesNotExist: 

+

            msg = self.error_messages['does_not_exist'] % smart_text(data) 

+

            raise ValidationError(msg) 

+

        except (TypeError, ValueError): 

+

            received = type(data).__name__ 

+

            msg = self.error_messages['incorrect_type'] % received 

+

            raise ValidationError(msg) 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        if self.many: 

+

            # To-many relationship 

+

 

+

            queryset = None 

+

            if not self.source: 

+

                # Prefer obj.serializable_value for performance reasons 

+

                try: 

+

                    queryset = obj.serializable_value(field_name) 

+

                except AttributeError: 

+

                    pass 

+

            if queryset is None: 

+

                # RelatedManager (reverse relationship) 

+

                source = self.source or field_name 

+

                queryset = obj 

+

                for component in source.split('.'): 

+

                    queryset = get_component(queryset, component) 

+

 

+

            # Forward relationship 

+

            if is_simple_callable(getattr(queryset, 'all', None)): 

+

                return [self.to_native(item.pk) for item in queryset.all()] 

+

            else: 

+

                # Also support non-queryset iterables. 

+

                # This allows us to also support plain lists of related items. 

+

                return [self.to_native(item.pk) for item in queryset] 

+

 

+

        # To-one relationship 

+

        try: 

+

            # Prefer obj.serializable_value for performance reasons 

+

            pk = obj.serializable_value(self.source or field_name) 

+

        except AttributeError: 

+

            # RelatedObject (reverse relationship) 

+

            try: 

+

                pk = getattr(obj, self.source or field_name).pk 

+

            except ObjectDoesNotExist: 

+

                return None 

+

 

+

        # Forward relationship 

+

        return self.to_native(pk) 

+

 

+

 

+

### Slug relationships 

+

 

+

 

+

class SlugRelatedField(RelatedField): 

+

    """ 

+

    Represents a relationship using a unique field on the target. 

+

    """ 

+

    read_only = False 

+

 

+

    default_error_messages = { 

+

        'does_not_exist': _("Object with %s=%s does not exist."), 

+

        'invalid': _('Invalid value.'), 

+

    } 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        self.slug_field = kwargs.pop('slug_field', None) 

+

        assert self.slug_field, 'slug_field is required' 

+

        super(SlugRelatedField, self).__init__(*args, **kwargs) 

+

 

+

    def to_native(self, obj): 

+

        return getattr(obj, self.slug_field) 

+

 

+

    def from_native(self, data): 

+

        if self.queryset is None: 

+

            raise Exception('Writable related fields must include a `queryset` argument') 

+

 

+

        try: 

+

            return self.queryset.get(**{self.slug_field: data}) 

+

        except ObjectDoesNotExist: 

+

            raise ValidationError(self.error_messages['does_not_exist'] % 

+

                                  (self.slug_field, smart_text(data))) 

+

        except (TypeError, ValueError): 

+

            msg = self.error_messages['invalid'] 

+

            raise ValidationError(msg) 

+

 

+

 

+

### Hyperlinked relationships 

+

 

+

class HyperlinkedRelatedField(RelatedField): 

+

    """ 

+

    Represents a relationship using hyperlinking. 

+

    """ 

+

    read_only = False 

+

    lookup_field = 'pk' 

+

 

+

    default_error_messages = { 

+

        'no_match': _('Invalid hyperlink - No URL match'), 

+

        'incorrect_match': _('Invalid hyperlink - Incorrect URL match'), 

+

        'configuration_error': _('Invalid hyperlink due to configuration error'), 

+

        'does_not_exist': _("Invalid hyperlink - object does not exist."), 

+

        'incorrect_type': _('Incorrect type.  Expected url string, received %s.'), 

+

    } 

+

 

+

    # These are all pending deprecation 

+

    pk_url_kwarg = 'pk' 

+

    slug_field = 'slug' 

+

    slug_url_kwarg = None  # Defaults to same as `slug_field` unless overridden 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        try: 

+

            self.view_name = kwargs.pop('view_name') 

+

        except KeyError: 

+

            raise ValueError("Hyperlinked field requires 'view_name' kwarg") 

+

 

+

        self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) 

+

        self.format = kwargs.pop('format', None) 

+

 

+

        # These are pending deprecation 

+

        if 'pk_url_kwarg' in kwargs: 

+

            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' 

+

            warnings.warn(msg, PendingDeprecationWarning, 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) 

+

        if 'slug_field' in kwargs: 

+

            msg = 'slug_field is pending deprecation. Use lookup_field instead.' 

+

            warnings.warn(msg, PendingDeprecationWarning, 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) 

+

        default_slug_kwarg = self.slug_url_kwarg or self.slug_field 

+

        self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) 

+

 

+

        super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) 

+

 

+

    def get_url(self, obj, view_name, request, format): 

+

        """ 

+

        Given an object, return the URL that hyperlinks to the object. 

+

 

+

        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) 

+

        kwargs = {self.lookup_field: lookup_field} 

+

        try: 

+

            return reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

        except NoReverseMatch: 

+

            pass 

+

 

+

        if self.pk_url_kwarg != 'pk': 

+

            # Only try pk if it has been explicitly set. 

+

            # Otherwise, the default `lookup_field = 'pk'` has us covered. 

+

            pk = obj.pk 

+

            kwargs = {self.pk_url_kwarg: pk} 

+

            try: 

+

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

            except NoReverseMatch: 

+

                pass 

+

 

+

        slug = getattr(obj, self.slug_field, None) 

+

        if slug is not None: 

+

            # Only try slug if it corresponds to an attribute on the object. 

+

            kwargs = {self.slug_url_kwarg: slug} 

+

            try: 

+

                ret = reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

                if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug': 

+

                    # 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.' \ 

+

                          'You should set `lookup_field=slug` on the HyperlinkedRelatedField.' 

+

                    warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) 

+

                return ret 

+

            except NoReverseMatch: 

+

                pass 

+

 

+

        raise NoReverseMatch() 

+

 

+

    def get_object(self, queryset, view_name, view_args, view_kwargs): 

+

        """ 

+

        Return the object corresponding to a matched URL. 

+

 

+

        Takes the matched URL conf arguments, and the queryset, and should 

+

        return an object instance, or raise an `ObjectDoesNotExist` exception. 

+

        """ 

+

        lookup = view_kwargs.get(self.lookup_field, None) 

+

        pk = view_kwargs.get(self.pk_url_kwarg, None) 

+

        slug = view_kwargs.get(self.slug_url_kwarg, None) 

+

 

+

        if lookup is not None: 

+

            filter_kwargs = {self.lookup_field: lookup} 

+

        elif pk is not None: 

+

            filter_kwargs = {'pk': pk} 

+

        elif slug is not None: 

+

            filter_kwargs = {self.slug_field: slug} 

+

        else: 

+

            raise ObjectDoesNotExist() 

+

 

+

        return queryset.get(**filter_kwargs) 

+

 

+

    def to_native(self, obj): 

+

        view_name = self.view_name 

+

        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) 

+

 

+

        # If the object has not yet been saved then we cannot hyperlink to it. 

+

        if getattr(obj, 'pk', None) is None: 

+

            return 

+

 

+

        # Return the hyperlink, or error if incorrectly configured. 

+

        try: 

+

            return self.get_url(obj, view_name, request, format) 

+

        except NoReverseMatch: 

+

            msg = ( 

+

                'Could not resolve URL for hyperlinked relationship using ' 

+

                'view name "%s". You may have failed to include the related ' 

+

                'model in your API, or incorrectly configured the ' 

+

                '`lookup_field` attribute on this field.' 

+

            ) 

+

            raise Exception(msg % view_name) 

+

 

+

    def from_native(self, value): 

+

        # Convert URL -> model instance pk 

+

        # TODO: Use values_list 

+

        queryset = self.queryset 

+

        if queryset is None: 

+

            raise Exception('Writable related fields must include a `queryset` argument') 

+

 

+

        try: 

+

            http_prefix = value.startswith(('http:', 'https:')) 

+

        except AttributeError: 

+

            msg = self.error_messages['incorrect_type'] 

+

            raise ValidationError(msg % type(value).__name__) 

+

 

+

        if http_prefix: 

+

            # If needed convert absolute URLs to relative path 

+

            value = urlparse.urlparse(value).path 

+

            prefix = get_script_prefix() 

+

            if value.startswith(prefix): 

+

                value = '/' + value[len(prefix):] 

+

 

+

        try: 

+

            match = resolve(value) 

+

        except Exception: 

+

            raise ValidationError(self.error_messages['no_match']) 

+

 

+

        if match.view_name != self.view_name: 

+

            raise ValidationError(self.error_messages['incorrect_match']) 

+

 

+

        try: 

+

            return self.get_object(queryset, match.view_name, 

+

                                   match.args, match.kwargs) 

+

        except (ObjectDoesNotExist, TypeError, ValueError): 

+

            raise ValidationError(self.error_messages['does_not_exist']) 

+

 

+

 

+

class HyperlinkedIdentityField(Field): 

+

    """ 

+

    Represents the instance, or a property on the instance, using hyperlinking. 

+

    """ 

+

    lookup_field = 'pk' 

+

    read_only = True 

+

 

+

    # These are all pending deprecation 

+

    pk_url_kwarg = 'pk' 

+

    slug_field = 'slug' 

+

    slug_url_kwarg = None  # Defaults to same as `slug_field` unless overridden 

+

 

+

    def __init__(self, *args, **kwargs): 

+

        try: 

+

            self.view_name = kwargs.pop('view_name') 

+

        except KeyError: 

+

            msg = "HyperlinkedIdentityField requires 'view_name' argument" 

+

            raise ValueError(msg) 

+

 

+

        self.format = kwargs.pop('format', None) 

+

        lookup_field = kwargs.pop('lookup_field', None) 

+

        self.lookup_field = lookup_field or self.lookup_field 

+

 

+

        # These are pending deprecation 

+

        if 'pk_url_kwarg' in kwargs: 

+

            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' 

+

            warnings.warn(msg, PendingDeprecationWarning, 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) 

+

        if 'slug_field' in kwargs: 

+

            msg = 'slug_field is pending deprecation. Use lookup_field instead.' 

+

            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) 

+

 

+

        self.slug_field = kwargs.pop('slug_field', self.slug_field) 

+

        default_slug_kwarg = self.slug_url_kwarg or self.slug_field 

+

        self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) 

+

        self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) 

+

 

+

        super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        request = self.context.get('request', None) 

+

        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) 

+

 

+

        # By default use whatever format is given for the current context 

+

        # unless the target is a different type to the source. 

+

        # 

+

        # Eg. Consider a HyperlinkedIdentityField pointing from a json 

+

        # representation to an html property of that representation... 

+

        # 

+

        # '/snippets/1/' should link to '/snippets/1/highlight/' 

+

        # ...but... 

+

        # '/snippets/1/.json' should link to '/snippets/1/highlight/.html' 

+

        if format and self.format and self.format != format: 

+

            format = self.format 

+

 

+

        # Return the hyperlink, or error if incorrectly configured. 

+

        try: 

+

            return self.get_url(obj, view_name, request, format) 

+

        except NoReverseMatch: 

+

            msg = ( 

+

                'Could not resolve URL for hyperlinked relationship using ' 

+

                'view name "%s". You may have failed to include the related ' 

+

                'model in your API, or incorrectly configured the ' 

+

                '`lookup_field` attribute on this field.' 

+

            ) 

+

            raise Exception(msg % view_name) 

+

 

+

    def get_url(self, obj, view_name, request, format): 

+

        """ 

+

        Given an object, return the URL that hyperlinks to the object. 

+

 

+

        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) 

+

        kwargs = {self.lookup_field: lookup_field} 

+

        try: 

+

            return reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

        except NoReverseMatch: 

+

            pass 

+

 

+

        if self.pk_url_kwarg != 'pk': 

+

            # Only try pk lookup if it has been explicitly set. 

+

            # Otherwise, the default `lookup_field = 'pk'` has us covered. 

+

            kwargs = {self.pk_url_kwarg: obj.pk} 

+

            try: 

+

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

            except NoReverseMatch: 

+

                pass 

+

 

+

        slug = getattr(obj, self.slug_field, None) 

+

        if slug: 

+

            # Only use slug lookup if a slug field exists on the model 

+

            kwargs = {self.slug_url_kwarg: slug} 

+

            try: 

+

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

+

            except NoReverseMatch: 

+

                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/htmlcov/rest_framework_renderers.html b/htmlcov/rest_framework_renderers.html new file mode 100644 index 000000000..58c71b855 --- /dev/null +++ b/htmlcov/rest_framework_renderers.html @@ -0,0 +1,1227 @@ + + + + + + + + Coverage for rest_framework/renderers: 92% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+

530

+

531

+

532

+

533

+

534

+

535

+

536

+

537

+

538

+

539

+

540

+

541

+

542

+

543

+

544

+

545

+

546

+

547

+

548

+

549

+

550

+

551

+

552

+

553

+

554

+

555

+

556

+

557

+

558

+

559

+

560

+

561

+

562

+

563

+

564

+

565

+

566

+

567

+

568

+

569

+

570

+

571

+

572

+

573

+ +
+

""" 

+

Renderers are used to serialize a response into specific media types. 

+

 

+

They give us a generic way of being able to handle various media types 

+

on the response, such as JSON encoded data or HTML output. 

+

 

+

REST framework also provides an HTML renderer the renders the browsable API. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

import copy 

+

import json 

+

from django import forms 

+

from django.core.exceptions import ImproperlyConfigured 

+

from django.http.multipartparser import parse_header 

+

from django.template import RequestContext, loader, Template 

+

from django.utils.xmlutils import SimplerXMLGenerator 

+

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.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 

+

 

+

 

+

class BaseRenderer(object): 

+

    """ 

+

    All renderers should extend this class, setting the `media_type` 

+

    and `format` attributes, and override the `.render()` method. 

+

    """ 

+

 

+

    media_type = None 

+

    format = None 

+

    charset = 'utf-8' 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        raise NotImplemented('Renderer class requires .render() to be implemented') 

+

 

+

 

+

class JSONRenderer(BaseRenderer): 

+

    """ 

+

    Renderer which serializes to JSON. 

+

    Applies JSON's backslash-u character escaping for non-ascii characters. 

+

    """ 

+

 

+

    media_type = 'application/json' 

+

    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. 

+

    # See: http://www.ietf.org/rfc/rfc4627.txt 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Render `data` into JSON. 

+

        """ 

+

        if data is None: 

+

            return '' 

+

 

+

        # If 'indent' is provided in the context, then pretty print the result. 

+

        # E.g. If we're being called by the BrowsableAPIRenderer. 

+

        renderer_context = renderer_context or {} 

+

        indent = renderer_context.get('indent', None) 

+

 

+

        if accepted_media_type: 

+

            # If the media type looks like 'application/json; indent=4', 

+

            # then pretty print the result. 

+

            base_media_type, params = parse_header(accepted_media_type.encode('ascii')) 

+

            indent = params.get('indent', indent) 

+

            try: 

+

                indent = max(min(int(indent), 8), 0) 

+

            except (ValueError, TypeError): 

+

                indent = None 

+

 

+

        ret = json.dumps(data, cls=self.encoder_class, 

+

            indent=indent, ensure_ascii=self.ensure_ascii) 

+

 

+

        # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, 

+

        # but if ensure_ascii=False, the return type is underspecified, 

+

        # 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 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. 

+

    """ 

+

 

+

 

+

class JSONPRenderer(JSONRenderer): 

+

    """ 

+

    Renderer which serializes to json, 

+

    wrapping the json output in a callback function. 

+

    """ 

+

 

+

    media_type = 'application/javascript' 

+

    format = 'jsonp' 

+

    callback_parameter = 'callback' 

+

    default_callback = 'callback' 

+

 

+

    def get_callback(self, renderer_context): 

+

        """ 

+

        Determine the name of the callback to wrap around the json output. 

+

        """ 

+

        request = renderer_context.get('request', None) 

+

        params = request and request.QUERY_PARAMS or {} 

+

        return params.get(self.callback_parameter, self.default_callback) 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Renders into jsonp, wrapping the json output in a callback function. 

+

 

+

        Clients may set the callback function name using a query parameter 

+

        on the URL, for example: ?callback=exampleCallbackName 

+

        """ 

+

        renderer_context = renderer_context or {} 

+

        callback = self.get_callback(renderer_context) 

+

        json = super(JSONPRenderer, self).render(data, accepted_media_type, 

+

                                                 renderer_context) 

+

        return callback.encode(self.charset) + b'(' + json + b');' 

+

 

+

 

+

class XMLRenderer(BaseRenderer): 

+

    """ 

+

    Renderer which serializes to XML. 

+

    """ 

+

 

+

    media_type = 'application/xml' 

+

    format = 'xml' 

+

    charset = 'utf-8' 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Renders *obj* into serialized XML. 

+

        """ 

+

        if data is None: 

+

            return '' 

+

 

+

        stream = StringIO() 

+

 

+

        xml = SimplerXMLGenerator(stream, self.charset) 

+

        xml.startDocument() 

+

        xml.startElement("root", {}) 

+

 

+

        self._to_xml(xml, data) 

+

 

+

        xml.endElement("root") 

+

        xml.endDocument() 

+

        return stream.getvalue() 

+

 

+

    def _to_xml(self, xml, data): 

+

        if isinstance(data, (list, tuple)): 

+

            for item in data: 

+

                xml.startElement("list-item", {}) 

+

                self._to_xml(xml, item) 

+

                xml.endElement("list-item") 

+

 

+

        elif isinstance(data, dict): 

+

            for key, value in six.iteritems(data): 

+

                xml.startElement(key, {}) 

+

                self._to_xml(xml, value) 

+

                xml.endElement(key) 

+

 

+

        elif data is None: 

+

            # Don't output any value 

+

            pass 

+

 

+

        else: 

+

            xml.characters(smart_text(data)) 

+

 

+

 

+

class YAMLRenderer(BaseRenderer): 

+

    """ 

+

    Renderer which serializes to YAML. 

+

    """ 

+

 

+

    media_type = 'application/yaml' 

+

    format = 'yaml' 

+

    encoder = encoders.SafeDumper 

+

    charset = 'utf-8' 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Renders *obj* into serialized YAML. 

+

        """ 

+

        assert yaml, 'YAMLRenderer requires pyyaml to be installed' 

+

 

+

        if data is None: 

+

            return '' 

+

 

+

        return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder) 

+

 

+

 

+

class TemplateHTMLRenderer(BaseRenderer): 

+

    """ 

+

    An HTML renderer for use with templates. 

+

 

+

    The data supplied to the Response object should be a dictionary that will 

+

    be used as context for the template. 

+

 

+

    The template name is determined by (in order of preference): 

+

 

+

    1. An explicit `.template_name` attribute set on the response. 

+

    2. An explicit `.template_name` attribute set on this class. 

+

    3. The return result of calling `view.get_template_names()`. 

+

 

+

    For example: 

+

        data = {'users': User.objects.all()} 

+

        return Response(data, template_name='users.html') 

+

 

+

    For pre-rendered HTML, see StaticHTMLRenderer. 

+

    """ 

+

 

+

    media_type = 'text/html' 

+

    format = 'html' 

+

    template_name = None 

+

    exception_template_names = [ 

+

        '%(status_code)s.html', 

+

        'api_exception.html' 

+

    ] 

+

    charset = 'utf-8' 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Renders data to HTML, using Django's standard template rendering. 

+

 

+

        The template name is determined by (in order of preference): 

+

 

+

        1. An explicit .template_name set on the response. 

+

        2. An explicit .template_name set on this class. 

+

        3. The return result of calling view.get_template_names(). 

+

        """ 

+

        renderer_context = renderer_context or {} 

+

        view = renderer_context['view'] 

+

        request = renderer_context['request'] 

+

        response = renderer_context['response'] 

+

 

+

        if response.exception: 

+

            template = self.get_exception_template(response) 

+

        else: 

+

            template_names = self.get_template_names(response, view) 

+

            template = self.resolve_template(template_names) 

+

 

+

        context = self.resolve_context(data, request, response) 

+

        return template.render(context) 

+

 

+

    def resolve_template(self, template_names): 

+

        return loader.select_template(template_names) 

+

 

+

    def resolve_context(self, data, request, response): 

+

        if response.exception: 

+

            data['status_code'] = response.status_code 

+

        return RequestContext(request, data) 

+

 

+

    def get_template_names(self, response, view): 

+

        if response.template_name: 

+

            return [response.template_name] 

+

        elif self.template_name: 

+

            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') 

+

 

+

    def get_exception_template(self, response): 

+

        template_names = [name % {'status_code': response.status_code} 

+

                          for name in self.exception_template_names] 

+

 

+

        try: 

+

            # Try to find an appropriate error template 

+

            return self.resolve_template(template_names) 

+

        except Exception: 

+

            # Fall back to using eg '404 Not Found' 

+

            return Template('%d %s' % (response.status_code, 

+

                                       response.status_text.title())) 

+

 

+

 

+

# Note, subclass TemplateHTMLRenderer simply for the exception behavior 

+

class StaticHTMLRenderer(TemplateHTMLRenderer): 

+

    """ 

+

    An HTML renderer class that simply returns pre-rendered HTML. 

+

 

+

    The data supplied to the Response object should be a string representing 

+

    the pre-rendered HTML content. 

+

 

+

    For example: 

+

        data = '<html><body>example</body></html>' 

+

        return Response(data) 

+

 

+

    For template rendered HTML, see TemplateHTMLRenderer. 

+

    """ 

+

    media_type = 'text/html' 

+

    format = 'html' 

+

    charset = 'utf-8' 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        renderer_context = renderer_context or {} 

+

        response = renderer_context['response'] 

+

 

+

        if response and response.exception: 

+

            request = renderer_context['request'] 

+

            template = self.get_exception_template(response) 

+

            context = self.resolve_context(data, request, response) 

+

            return template.render(context) 

+

 

+

        return data 

+

 

+

 

+

class BrowsableAPIRenderer(BaseRenderer): 

+

    """ 

+

    HTML renderer used to self-document the API. 

+

    """ 

+

    media_type = 'text/html' 

+

    format = 'api' 

+

    template = 'rest_framework/api.html' 

+

    charset = 'utf-8' 

+

 

+

    def get_default_renderer(self, view): 

+

        """ 

+

        Return an instance of the first valid renderer. 

+

        (Don't use another documenting renderer.) 

+

        """ 

+

        renderers = [renderer for renderer in view.renderer_classes 

+

                     if not issubclass(renderer, BrowsableAPIRenderer)] 

+

        if not renderers: 

+

            return None 

+

        return renderers[0]() 

+

 

+

    def get_content(self, renderer, data, 

+

                    accepted_media_type, renderer_context): 

+

        """ 

+

        Get the content as if it had been rendered by the default 

+

        non-documenting renderer. 

+

        """ 

+

        if not renderer: 

+

            return '[No renderers were found]' 

+

 

+

        renderer_context['indent'] = 4 

+

        content = renderer.render(data, accepted_media_type, renderer_context) 

+

 

+

        if renderer.charset is None: 

+

            return '[%d bytes of binary content]' % len(content) 

+

 

+

        return content 

+

 

+

    def show_form_for_method(self, view, method, request, obj): 

+

        """ 

+

        Returns True if a form should be shown for this method. 

+

        """ 

+

        if not method in view.allowed_methods: 

+

            return  # Not a valid method 

+

 

+

        if not api_settings.FORM_METHOD_OVERRIDE: 

+

            return  # Cannot use form overloading 

+

 

+

        try: 

+

            view.check_permissions(request) 

+

            if obj is not None: 

+

                view.check_object_permissions(request, obj) 

+

        except exceptions.APIException: 

+

            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 

+

        # correct form for each method. 

+

        restore = view.request 

+

        request = clone_request(request, method) 

+

        view.request = request 

+

        try: 

+

            return self.get_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): 

+

        """ 

+

        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. 

+

        """ 

+

        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 not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: 

+

            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 

+

 

+

    def get_raw_data_form(self, view, method, request, media_types): 

+

        """ 

+

        Returns a form that allows for arbitrary content types to be tunneled 

+

        via standard HTML forms. 

+

        (Which are typically application/x-www-form-urlencoded) 

+

        """ 

+

 

+

        # 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 

+

 

+

        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__() 

+

 

+

                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() 

+

 

+

    def get_name(self, view): 

+

        return get_view_name(view.__class__, getattr(view, 'suffix', None)) 

+

 

+

    def get_description(self, view): 

+

        return get_view_description(view.__class__, html=True) 

+

 

+

    def get_breadcrumbs(self, request): 

+

        return get_breadcrumbs(request.path) 

+

 

+

    def render(self, data, accepted_media_type=None, renderer_context=None): 

+

        """ 

+

        Render the HTML for the browsable API representation. 

+

        """ 

+

        accepted_media_type = accepted_media_type or '' 

+

        renderer_context = renderer_context or {} 

+

 

+

        view = renderer_context['view'] 

+

        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) 

+

 

+

        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) 

+

 

+

        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) 

+

        description = self.get_description(view) 

+

        breadcrumb_list = self.get_breadcrumbs(request) 

+

 

+

        template = loader.get_template(self.template) 

+

        context = RequestContext(request, { 

+

            'content': content, 

+

            'view': view, 

+

            'request': request, 

+

            'response': response, 

+

            'description': description, 

+

            'name': name, 

+

            'version': VERSION, 

+

            'breadcrumblist': breadcrumb_list, 

+

            '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, 

+

 

+

            'raw_data_put_form': raw_data_put_form, 

+

            '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, 

+

 

+

            'api_settings': api_settings 

+

        }) 

+

 

+

        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) 

+

        if response.status_code == status.HTTP_204_NO_CONTENT: 

+

            response.status_code = status.HTTP_200_OK 

+

 

+

        return ret 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_request.html b/htmlcov/rest_framework_request.html new file mode 100644 index 000000000..03f2c3e34 --- /dev/null +++ b/htmlcov/rest_framework_request.html @@ -0,0 +1,819 @@ + + + + + + + + Coverage for rest_framework/request: 95% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+ +
+

""" 

+

The Request class is used as a wrapper around the standard request object. 

+

 

+

The wrapped request then offers a richer API, in particular : 

+

 

+

    - content automatically parsed according to `Content-Type` header, 

+

      and available as `request.DATA` 

+

    - full support of PUT method, including support for file uploads 

+

    - form overloading of HTTP method, content type and content 

+

""" 

+

from __future__ import unicode_literals 

+

from django.conf import settings 

+

from django.http import QueryDict 

+

from django.http.multipartparser import parse_header 

+

from django.utils.datastructures import MultiValueDict 

+

from rest_framework import HTTP_HEADER_ENCODING 

+

from rest_framework import exceptions 

+

from rest_framework.compat import BytesIO 

+

from rest_framework.settings import api_settings 

+

 

+

 

+

def is_form_media_type(media_type): 

+

    """ 

+

    Return True if the media type is a valid form media type. 

+

    """ 

+

    base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) 

+

    return (base_media_type == 'application/x-www-form-urlencoded' or 

+

            base_media_type == 'multipart/form-data') 

+

 

+

 

+

class Empty(object): 

+

    """ 

+

    Placeholder for unset attributes. 

+

    Cannot use `None`, as that may be a valid value. 

+

    """ 

+

    pass 

+

 

+

 

+

def _hasattr(obj, name): 

+

    return not getattr(obj, name) is Empty 

+

 

+

 

+

def clone_request(request, method): 

+

    """ 

+

    Internal helper method to clone a request, replacing with a different 

+

    HTTP method.  Used for checking permissions against other methods. 

+

    """ 

+

    ret = Request(request=request._request, 

+

                  parsers=request.parsers, 

+

                  authenticators=request.authenticators, 

+

                  negotiator=request.negotiator, 

+

                  parser_context=request.parser_context) 

+

    ret._data = request._data 

+

    ret._files = request._files 

+

    ret._content_type = request._content_type 

+

    ret._stream = request._stream 

+

    ret._method = method 

+

    if hasattr(request, '_user'): 

+

        ret._user = request._user 

+

    if hasattr(request, '_auth'): 

+

        ret._auth = request._auth 

+

    if hasattr(request, '_authenticator'): 

+

        ret._authenticator = request._authenticator 

+

    return ret 

+

 

+

 

+

class Request(object): 

+

    """ 

+

    Wrapper allowing to enhance a standard `HttpRequest` instance. 

+

 

+

    Kwargs: 

+

        - request(HttpRequest). The original request instance. 

+

        - parsers_classes(list/tuple). The parsers to use for parsing the 

+

          request content. 

+

        - authentication_classes(list/tuple). The authentications used to try 

+

          authenticating the request's user. 

+

    """ 

+

 

+

    _METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE 

+

    _CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE 

+

    _CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE 

+

 

+

    def __init__(self, request, parsers=None, authenticators=None, 

+

                 negotiator=None, parser_context=None): 

+

        self._request = request 

+

        self.parsers = parsers or () 

+

        self.authenticators = authenticators or () 

+

        self.negotiator = negotiator or self._default_negotiator() 

+

        self.parser_context = parser_context 

+

        self._data = Empty 

+

        self._files = Empty 

+

        self._method = Empty 

+

        self._content_type = Empty 

+

        self._stream = Empty 

+

 

+

        if self.parser_context is None: 

+

            self.parser_context = {} 

+

        self.parser_context['request'] = self 

+

        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET 

+

 

+

    def _default_negotiator(self): 

+

        return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() 

+

 

+

    @property 

+

    def method(self): 

+

        """ 

+

        Returns the HTTP method. 

+

 

+

        This allows the `method` to be overridden by using a hidden `form` 

+

        field on a form POST request. 

+

        """ 

+

        if not _hasattr(self, '_method'): 

+

            self._load_method_and_content_type() 

+

        return self._method 

+

 

+

    @property 

+

    def content_type(self): 

+

        """ 

+

        Returns the content type header. 

+

 

+

        This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`, 

+

        as it allows the content type to be overridden by using a hidden form 

+

        field on a form POST request. 

+

        """ 

+

        if not _hasattr(self, '_content_type'): 

+

            self._load_method_and_content_type() 

+

        return self._content_type 

+

 

+

    @property 

+

    def stream(self): 

+

        """ 

+

        Returns an object that may be used to stream the request content. 

+

        """ 

+

        if not _hasattr(self, '_stream'): 

+

            self._load_stream() 

+

        return self._stream 

+

 

+

    @property 

+

    def QUERY_PARAMS(self): 

+

        """ 

+

        More semantically correct name for request.GET. 

+

        """ 

+

        return self._request.GET 

+

 

+

    @property 

+

    def DATA(self): 

+

        """ 

+

        Parses the request body and returns the data. 

+

 

+

        Similar to usual behaviour of `request.POST`, except that it handles 

+

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

+

        """ 

+

        if not _hasattr(self, '_data'): 

+

            self._load_data_and_files() 

+

        return self._data 

+

 

+

    @property 

+

    def FILES(self): 

+

        """ 

+

        Parses the request body and returns any files uploaded in the request. 

+

 

+

        Similar to usual behaviour of `request.FILES`, except that it handles 

+

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

+

        """ 

+

        if not _hasattr(self, '_files'): 

+

            self._load_data_and_files() 

+

        return self._files 

+

 

+

    @property 

+

    def user(self): 

+

        """ 

+

        Returns the user associated with the current request, as authenticated 

+

        by the authentication classes provided to the request. 

+

        """ 

+

        if not hasattr(self, '_user'): 

+

            self._authenticate() 

+

        return self._user 

+

 

+

    @user.setter 

+

    def user(self, value): 

+

        """ 

+

        Sets the user on the current request. This is necessary to maintain 

+

        compatilbility with django.contrib.auth where the user proprety is 

+

        set in the login and logout functions. 

+

        """ 

+

        self._user = value 

+

 

+

    @property 

+

    def auth(self): 

+

        """ 

+

        Returns any non-user authentication information associated with the 

+

        request, such as an authentication token. 

+

        """ 

+

        if not hasattr(self, '_auth'): 

+

            self._authenticate() 

+

        return self._auth 

+

 

+

    @auth.setter 

+

    def auth(self, value): 

+

        """ 

+

        Sets any non-user authentication information associated with the 

+

        request, such as an authentication token. 

+

        """ 

+

        self._auth = value 

+

 

+

    @property 

+

    def successful_authenticator(self): 

+

        """ 

+

        Return the instance of the authentication instance class that was used 

+

        to authenticate the request, or `None`. 

+

        """ 

+

        if not hasattr(self, '_authenticator'): 

+

            self._authenticate() 

+

        return self._authenticator 

+

 

+

    def _load_data_and_files(self): 

+

        """ 

+

        Parses the request content into self.DATA and self.FILES. 

+

        """ 

+

        if not _hasattr(self, '_content_type'): 

+

            self._load_method_and_content_type() 

+

 

+

        if not _hasattr(self, '_data'): 

+

            self._data, self._files = self._parse() 

+

 

+

    def _load_method_and_content_type(self): 

+

        """ 

+

        Sets the method and content_type, and then check if they've 

+

        been overridden. 

+

        """ 

+

        self._content_type = self.META.get('HTTP_CONTENT_TYPE', 

+

                                           self.META.get('CONTENT_TYPE', '')) 

+

 

+

        self._perform_form_overloading() 

+

 

+

        if not _hasattr(self, '_method'): 

+

            self._method = self._request.method 

+

 

+

            if self._method == 'POST': 

+

                # Allow X-HTTP-METHOD-OVERRIDE header 

+

                self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', 

+

                                             self._method) 

+

 

+

    def _load_stream(self): 

+

        """ 

+

        Return the content body of the request, as a stream. 

+

        """ 

+

        try: 

+

            content_length = int(self.META.get('CONTENT_LENGTH', 

+

                                    self.META.get('HTTP_CONTENT_LENGTH'))) 

+

        except (ValueError, TypeError): 

+

            content_length = 0 

+

 

+

        if content_length == 0: 

+

            self._stream = None 

+

        elif hasattr(self._request, 'read'): 

+

            self._stream = self._request 

+

        else: 

+

            self._stream = BytesIO(self.raw_post_data) 

+

 

+

    def _perform_form_overloading(self): 

+

        """ 

+

        If this is a form POST request, then we need to check if the method and 

+

        content/content_type have been overridden by setting them in hidden 

+

        form fields or not. 

+

        """ 

+

 

+

        USE_FORM_OVERLOADING = ( 

+

            self._METHOD_PARAM or 

+

            (self._CONTENT_PARAM and self._CONTENTTYPE_PARAM) 

+

        ) 

+

 

+

        # We only need to use form overloading on form POST requests. 

+

        if (not USE_FORM_OVERLOADING 

+

            or self._request.method != 'POST' 

+

            or not is_form_media_type(self._content_type)): 

+

            return 

+

 

+

        # At this point we're committed to parsing the request as form data. 

+

        self._data = self._request.POST 

+

        self._files = self._request.FILES 

+

 

+

        # Method overloading - change the method and remove the param from the content. 

+

        if (self._METHOD_PARAM and 

+

            self._METHOD_PARAM in self._data): 

+

            self._method = self._data[self._METHOD_PARAM].upper() 

+

 

+

        # Content overloading - modify the content type, and force re-parse. 

+

        if (self._CONTENT_PARAM and 

+

            self._CONTENTTYPE_PARAM and 

+

            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._data, self._files = (Empty, Empty) 

+

 

+

    def _parse(self): 

+

        """ 

+

        Parse the request content, returning a two-tuple of (data, files) 

+

 

+

        May raise an `UnsupportedMediaType`, or `ParseError` exception. 

+

        """ 

+

        stream = self.stream 

+

        media_type = self.content_type 

+

 

+

        if stream is None or media_type is None: 

+

            empty_data = QueryDict('', self._request._encoding) 

+

            empty_files = MultiValueDict() 

+

            return (empty_data, empty_files) 

+

 

+

        parser = self.negotiator.select_parser(self, self.parsers) 

+

 

+

        if not parser: 

+

            raise exceptions.UnsupportedMediaType(media_type) 

+

 

+

        parsed = parser.parse(stream, media_type, self.parser_context) 

+

 

+

        # Parser classes may return the raw data, or a 

+

        # DataAndFiles object.  Unpack the result as required. 

+

        try: 

+

            return (parsed.data, parsed.files) 

+

        except AttributeError: 

+

            empty_files = MultiValueDict() 

+

            return (parsed, empty_files) 

+

 

+

    def _authenticate(self): 

+

        """ 

+

        Attempt to authenticate the request using each authentication instance 

+

        in turn. 

+

        Returns a three-tuple of (authenticator, user, authtoken). 

+

        """ 

+

        for authenticator in self.authenticators: 

+

            try: 

+

                user_auth_tuple = authenticator.authenticate(self) 

+

            except exceptions.APIException: 

+

                self._not_authenticated() 

+

                raise 

+

 

+

            if not user_auth_tuple is None: 

+

                self._authenticator = authenticator 

+

                self._user, self._auth = user_auth_tuple 

+

                return 

+

 

+

        self._not_authenticated() 

+

 

+

    def _not_authenticated(self): 

+

        """ 

+

        Return a three-tuple of (authenticator, user, authtoken), representing 

+

        an unauthenticated request. 

+

 

+

        By default this will be (None, AnonymousUser, None). 

+

        """ 

+

        self._authenticator = None 

+

 

+

        if api_settings.UNAUTHENTICATED_USER: 

+

            self._user = api_settings.UNAUTHENTICATED_USER() 

+

        else: 

+

            self._user = None 

+

 

+

        if api_settings.UNAUTHENTICATED_TOKEN: 

+

            self._auth = api_settings.UNAUTHENTICATED_TOKEN() 

+

        else: 

+

            self._auth = None 

+

 

+

    def __getattr__(self, attr): 

+

        """ 

+

        Proxy other attributes to the underlying HttpRequest object. 

+

        """ 

+

        return getattr(self._request, attr) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_response.html b/htmlcov/rest_framework_response.html new file mode 100644 index 000000000..d297ecd0b --- /dev/null +++ b/htmlcov/rest_framework_response.html @@ -0,0 +1,249 @@ + + + + + + + + Coverage for rest_framework/response: 98% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+ +
+

""" 

+

The Response class in REST framework is similar to HTTPResponse, except that 

+

it is initialized with unrendered data, instead of a pre-rendered string. 

+

 

+

The appropriate renderer is called during Django's template response rendering. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.core.handlers.wsgi import STATUS_CODE_TEXT 

+

from django.template.response import SimpleTemplateResponse 

+

from rest_framework.compat import six 

+

 

+

 

+

class Response(SimpleTemplateResponse): 

+

    """ 

+

    An HttpResponse that allows its data to be rendered into 

+

    arbitrary media types. 

+

    """ 

+

 

+

    def __init__(self, data=None, status=200, 

+

                 template_name=None, headers=None, 

+

                 exception=False, content_type=None): 

+

        """ 

+

        Alters the init arguments slightly. 

+

        For example, drop 'template_name', and instead use 'data'. 

+

 

+

        Setting 'renderer' and 'media_type' will typically be deferred, 

+

        For example being set automatically by the `APIView`. 

+

        """ 

+

        super(Response, self).__init__(None, status=status) 

+

        self.data = data 

+

        self.template_name = template_name 

+

        self.exception = exception 

+

        self.content_type = content_type 

+

 

+

        if headers: 

+

            for name, value in six.iteritems(headers): 

+

                self[name] = value 

+

 

+

    @property 

+

    def rendered_content(self): 

+

        renderer = getattr(self, 'accepted_renderer', None) 

+

        media_type = getattr(self, 'accepted_media_type', None) 

+

        context = getattr(self, 'renderer_context', None) 

+

 

+

        assert renderer, ".accepted_renderer not set on Response" 

+

        assert media_type, ".accepted_media_type not set on Response" 

+

        assert context, ".renderer_context not set on Response" 

+

        context['response'] = self 

+

 

+

        charset = renderer.charset 

+

        content_type = self.content_type 

+

 

+

        if content_type is None and charset is not None: 

+

            content_type = "{0}; charset={1}".format(media_type, charset) 

+

        elif content_type is None: 

+

            content_type = media_type 

+

        self['Content-Type'] = content_type 

+

 

+

        ret = renderer.render(self.data, media_type, context) 

+

        if isinstance(ret, six.text_type): 

+

            assert charset, 'renderer returned unicode, and did not specify ' \ 

+

            'a charset value.' 

+

            return bytes(ret.encode(charset)) 

+

        return ret 

+

 

+

    @property 

+

    def status_text(self): 

+

        """ 

+

        Returns reason text corresponding to our HTTP response status code. 

+

        Provided for convenience. 

+

        """ 

+

        # TODO: Deprecate and use a template tag instead 

+

        # TODO: Status code text for RFC 6585 status codes 

+

        return STATUS_CODE_TEXT.get(self.status_code, '') 

+

 

+

    def __getstate__(self): 

+

        """ 

+

        Remove attributes from the response that shouldn't be cached 

+

        """ 

+

        state = super(Response, self).__getstate__() 

+

        for key in ('accepted_renderer', 'renderer_context', 'data'): 

+

            if key in state: 

+

                del state[key] 

+

        return state 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_reverse.html b/htmlcov/rest_framework_reverse.html new file mode 100644 index 000000000..4e7a8de23 --- /dev/null +++ b/htmlcov/rest_framework_reverse.html @@ -0,0 +1,127 @@ + + + + + + + + Coverage for rest_framework/reverse: 75% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+ +
+

""" 

+

Provide reverse functions that return fully qualified URLs 

+

""" 

+

from __future__ import unicode_literals 

+

from django.core.urlresolvers import reverse as django_reverse 

+

from django.utils.functional import lazy 

+

 

+

 

+

def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): 

+

    """ 

+

    Same as `django.core.urlresolvers.reverse`, but optionally takes a request 

+

    and returns a fully qualified URL, using the request to get the base URL. 

+

    """ 

+

    if format is not None: 

+

        kwargs = kwargs or {} 

+

        kwargs['format'] = format 

+

    url = django_reverse(viewname, args=args, kwargs=kwargs, **extra) 

+

    if request: 

+

        return request.build_absolute_uri(url) 

+

    return url 

+

 

+

 

+

reverse_lazy = lazy(reverse, str) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_routers.html b/htmlcov/rest_framework_routers.html new file mode 100644 index 000000000..f08d5007e --- /dev/null +++ b/htmlcov/rest_framework_routers.html @@ -0,0 +1,595 @@ + + + + + + + + Coverage for rest_framework/routers: 94% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+ +
+

""" 

+

Routers provide a convenient and consistent way of automatically 

+

determining the URL conf for your API. 

+

 

+

They are used by simply instantiating a Router class, and then registering 

+

all the required ViewSets with that router. 

+

 

+

For example, you might have a `urls.py` that looks something like this: 

+

 

+

    router = routers.DefaultRouter() 

+

    router.register('users', UserViewSet, 'user') 

+

    router.register('accounts', AccountViewSet, 'account') 

+

 

+

    urlpatterns = router.urls 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from collections import namedtuple 

+

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 

+

 

+

 

+

Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) 

+

 

+

 

+

def replace_methodname(format_string, methodname): 

+

    """ 

+

    Partially format a format_string, swapping out any 

+

    '{methodname}' or '{methodnamehyphen}' components. 

+

    """ 

+

    methodnamehyphen = methodname.replace('_', '-') 

+

    ret = format_string 

+

    ret = ret.replace('{methodname}', methodname) 

+

    ret = ret.replace('{methodnamehyphen}', methodnamehyphen) 

+

    return ret 

+

 

+

 

+

class BaseRouter(object): 

+

    def __init__(self): 

+

        self.registry = [] 

+

 

+

    def register(self, prefix, viewset, base_name=None): 

+

        if base_name is None: 

+

            base_name = self.get_default_base_name(viewset) 

+

        self.registry.append((prefix, viewset, base_name)) 

+

 

+

    def get_default_base_name(self, viewset): 

+

        """ 

+

        If `base_name` is not specified, attempt to automatically determine 

+

        it from the viewset. 

+

        """ 

+

        raise NotImplemented('get_default_base_name must be overridden') 

+

 

+

    def get_urls(self): 

+

        """ 

+

        Return a list of URL patterns, given the registered viewsets. 

+

        """ 

+

        raise NotImplemented('get_urls must be overridden') 

+

 

+

    @property 

+

    def urls(self): 

+

        if not hasattr(self, '_urls'): 

+

            self._urls = patterns('', *self.get_urls()) 

+

        return self._urls 

+

 

+

 

+

class SimpleRouter(BaseRouter): 

+

    routes = [ 

+

        # List route. 

+

        Route( 

+

            url=r'^{prefix}{trailing_slash}$', 

+

            mapping={ 

+

                'get': 'list', 

+

                'post': 'create' 

+

            }, 

+

            name='{basename}-list', 

+

            initkwargs={'suffix': 'List'} 

+

        ), 

+

        # Detail route. 

+

        Route( 

+

            url=r'^{prefix}/{lookup}{trailing_slash}$', 

+

            mapping={ 

+

                'get': 'retrieve', 

+

                'put': 'update', 

+

                'patch': 'partial_update', 

+

                'delete': 'destroy' 

+

            }, 

+

            name='{basename}-detail', 

+

            initkwargs={'suffix': 'Instance'} 

+

        ), 

+

        # Dynamically generated routes. 

+

        # Generated using @action or @link decorators on methods of the viewset. 

+

        Route( 

+

            url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', 

+

            mapping={ 

+

                '{httpmethod}': '{methodname}', 

+

            }, 

+

            name='{basename}-{methodnamehyphen}', 

+

            initkwargs={} 

+

        ), 

+

    ] 

+

 

+

    def __init__(self, trailing_slash=True): 

+

        self.trailing_slash = trailing_slash and '/' or '' 

+

        super(SimpleRouter, self).__init__() 

+

 

+

    def get_default_base_name(self, viewset): 

+

        """ 

+

        If `base_name` is not specified, attempt to automatically determine 

+

        it from the viewset. 

+

        """ 

+

        model_cls = getattr(viewset, 'model', None) 

+

        queryset = getattr(viewset, 'queryset', None) 

+

        if model_cls is None and queryset is not None: 

+

            model_cls = queryset.model 

+

 

+

        assert model_cls, '`name` not argument not specified, and could ' \ 

+

            'not automatically determine the name from the viewset, as ' \ 

+

            'it does not have a `.model` or `.queryset` attribute.' 

+

 

+

        return model_cls._meta.object_name.lower() 

+

 

+

    def get_routes(self, viewset): 

+

        """ 

+

        Augment `self.routes` with any dynamically generated routes. 

+

 

+

        Returns a list of the Route namedtuple. 

+

        """ 

+

 

+

        # Determine any `@action` or `@link` decorated methods on the viewset 

+

        dynamic_routes = [] 

+

        for methodname in dir(viewset): 

+

            attr = getattr(viewset, methodname) 

+

            httpmethods = getattr(attr, 'bind_to_methods', None) 

+

            if httpmethods: 

+

                dynamic_routes.append((httpmethods, methodname)) 

+

 

+

        ret = [] 

+

        for route in self.routes: 

+

            if route.mapping == {'{httpmethod}': '{methodname}'}: 

+

                # 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( 

+

                        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) 

+

 

+

        return ret 

+

 

+

    def get_method_map(self, viewset, method_map): 

+

        """ 

+

        Given a viewset, and a mapping of http methods to actions, 

+

        return a new mapping which only includes any mappings that 

+

        are actually implemented by the viewset. 

+

        """ 

+

        bound_methods = {} 

+

        for method, action in method_map.items(): 

+

            if hasattr(viewset, action): 

+

                bound_methods[method] = action 

+

        return bound_methods 

+

 

+

    def get_lookup_regex(self, viewset): 

+

        """ 

+

        Given a viewset, return the portion of URL regex that is used 

+

        to match against a single instance. 

+

        """ 

+

        base_regex = '(?P<{lookup_field}>[^/]+)' 

+

        lookup_field = getattr(viewset, 'lookup_field', 'pk') 

+

        return base_regex.format(lookup_field=lookup_field) 

+

 

+

    def get_urls(self): 

+

        """ 

+

        Use the registered viewsets to generate a list of URL patterns. 

+

        """ 

+

        ret = [] 

+

 

+

        for prefix, viewset, basename in self.registry: 

+

            lookup = self.get_lookup_regex(viewset) 

+

            routes = self.get_routes(viewset) 

+

 

+

            for route in routes: 

+

 

+

                # Only actions which actually exist on the viewset will be bound 

+

                mapping = self.get_method_map(viewset, route.mapping) 

+

                if not mapping: 

+

                    continue 

+

 

+

                # Build the url pattern 

+

                regex = route.url.format( 

+

                    prefix=prefix, 

+

                    lookup=lookup, 

+

                    trailing_slash=self.trailing_slash 

+

                ) 

+

                view = viewset.as_view(mapping, **route.initkwargs) 

+

                name = route.name.format(basename=basename) 

+

                ret.append(url(regex, view, name=name)) 

+

 

+

        return ret 

+

 

+

 

+

class DefaultRouter(SimpleRouter): 

+

    """ 

+

    The default router extends the SimpleRouter, but also adds in a default 

+

    API root view, and adds format suffix patterns to the URLs. 

+

    """ 

+

    include_root_view = True 

+

    include_format_suffixes = True 

+

    root_view_name = 'api-root' 

+

 

+

    def get_api_root_view(self): 

+

        """ 

+

        Return a view to use as the API root. 

+

        """ 

+

        api_root_dict = {} 

+

        list_name = self.routes[0].name 

+

        for prefix, viewset, basename in self.registry: 

+

            api_root_dict[prefix] = list_name.format(basename=basename) 

+

 

+

        class APIRoot(views.APIView): 

+

            _ignore_model_permissions = True 

+

 

+

            def get(self, request, format=None): 

+

                ret = {} 

+

                for key, url_name in api_root_dict.items(): 

+

                    ret[key] = reverse(url_name, request=request, format=format) 

+

                return Response(ret) 

+

 

+

        return APIRoot.as_view() 

+

 

+

    def get_urls(self): 

+

        """ 

+

        Generate the list of URL patterns, including a default root view 

+

        for the API, and appending `.json` style format suffixes. 

+

        """ 

+

        urls = [] 

+

 

+

        if self.include_root_view: 

+

            root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name) 

+

            urls.append(root_url) 

+

 

+

        default_urls = super(DefaultRouter, self).get_urls() 

+

        urls.extend(default_urls) 

+

 

+

        if self.include_format_suffixes: 

+

            urls = format_suffix_patterns(urls) 

+

 

+

        return urls 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_serializers.html b/htmlcov/rest_framework_serializers.html new file mode 100644 index 000000000..79dc56474 --- /dev/null +++ b/htmlcov/rest_framework_serializers.html @@ -0,0 +1,2011 @@ + + + + + + + + Coverage for rest_framework/serializers: 94% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+

357

+

358

+

359

+

360

+

361

+

362

+

363

+

364

+

365

+

366

+

367

+

368

+

369

+

370

+

371

+

372

+

373

+

374

+

375

+

376

+

377

+

378

+

379

+

380

+

381

+

382

+

383

+

384

+

385

+

386

+

387

+

388

+

389

+

390

+

391

+

392

+

393

+

394

+

395

+

396

+

397

+

398

+

399

+

400

+

401

+

402

+

403

+

404

+

405

+

406

+

407

+

408

+

409

+

410

+

411

+

412

+

413

+

414

+

415

+

416

+

417

+

418

+

419

+

420

+

421

+

422

+

423

+

424

+

425

+

426

+

427

+

428

+

429

+

430

+

431

+

432

+

433

+

434

+

435

+

436

+

437

+

438

+

439

+

440

+

441

+

442

+

443

+

444

+

445

+

446

+

447

+

448

+

449

+

450

+

451

+

452

+

453

+

454

+

455

+

456

+

457

+

458

+

459

+

460

+

461

+

462

+

463

+

464

+

465

+

466

+

467

+

468

+

469

+

470

+

471

+

472

+

473

+

474

+

475

+

476

+

477

+

478

+

479

+

480

+

481

+

482

+

483

+

484

+

485

+

486

+

487

+

488

+

489

+

490

+

491

+

492

+

493

+

494

+

495

+

496

+

497

+

498

+

499

+

500

+

501

+

502

+

503

+

504

+

505

+

506

+

507

+

508

+

509

+

510

+

511

+

512

+

513

+

514

+

515

+

516

+

517

+

518

+

519

+

520

+

521

+

522

+

523

+

524

+

525

+

526

+

527

+

528

+

529

+

530

+

531

+

532

+

533

+

534

+

535

+

536

+

537

+

538

+

539

+

540

+

541

+

542

+

543

+

544

+

545

+

546

+

547

+

548

+

549

+

550

+

551

+

552

+

553

+

554

+

555

+

556

+

557

+

558

+

559

+

560

+

561

+

562

+

563

+

564

+

565

+

566

+

567

+

568

+

569

+

570

+

571

+

572

+

573

+

574

+

575

+

576

+

577

+

578

+

579

+

580

+

581

+

582

+

583

+

584

+

585

+

586

+

587

+

588

+

589

+

590

+

591

+

592

+

593

+

594

+

595

+

596

+

597

+

598

+

599

+

600

+

601

+

602

+

603

+

604

+

605

+

606

+

607

+

608

+

609

+

610

+

611

+

612

+

613

+

614

+

615

+

616

+

617

+

618

+

619

+

620

+

621

+

622

+

623

+

624

+

625

+

626

+

627

+

628

+

629

+

630

+

631

+

632

+

633

+

634

+

635

+

636

+

637

+

638

+

639

+

640

+

641

+

642

+

643

+

644

+

645

+

646

+

647

+

648

+

649

+

650

+

651

+

652

+

653

+

654

+

655

+

656

+

657

+

658

+

659

+

660

+

661

+

662

+

663

+

664

+

665

+

666

+

667

+

668

+

669

+

670

+

671

+

672

+

673

+

674

+

675

+

676

+

677

+

678

+

679

+

680

+

681

+

682

+

683

+

684

+

685

+

686

+

687

+

688

+

689

+

690

+

691

+

692

+

693

+

694

+

695

+

696

+

697

+

698

+

699

+

700

+

701

+

702

+

703

+

704

+

705

+

706

+

707

+

708

+

709

+

710

+

711

+

712

+

713

+

714

+

715

+

716

+

717

+

718

+

719

+

720

+

721

+

722

+

723

+

724

+

725

+

726

+

727

+

728

+

729

+

730

+

731

+

732

+

733

+

734

+

735

+

736

+

737

+

738

+

739

+

740

+

741

+

742

+

743

+

744

+

745

+

746

+

747

+

748

+

749

+

750

+

751

+

752

+

753

+

754

+

755

+

756

+

757

+

758

+

759

+

760

+

761

+

762

+

763

+

764

+

765

+

766

+

767

+

768

+

769

+

770

+

771

+

772

+

773

+

774

+

775

+

776

+

777

+

778

+

779

+

780

+

781

+

782

+

783

+

784

+

785

+

786

+

787

+

788

+

789

+

790

+

791

+

792

+

793

+

794

+

795

+

796

+

797

+

798

+

799

+

800

+

801

+

802

+

803

+

804

+

805

+

806

+

807

+

808

+

809

+

810

+

811

+

812

+

813

+

814

+

815

+

816

+

817

+

818

+

819

+

820

+

821

+

822

+

823

+

824

+

825

+

826

+

827

+

828

+

829

+

830

+

831

+

832

+

833

+

834

+

835

+

836

+

837

+

838

+

839

+

840

+

841

+

842

+

843

+

844

+

845

+

846

+

847

+

848

+

849

+

850

+

851

+

852

+

853

+

854

+

855

+

856

+

857

+

858

+

859

+

860

+

861

+

862

+

863

+

864

+

865

+

866

+

867

+

868

+

869

+

870

+

871

+

872

+

873

+

874

+

875

+

876

+

877

+

878

+

879

+

880

+

881

+

882

+

883

+

884

+

885

+

886

+

887

+

888

+

889

+

890

+

891

+

892

+

893

+

894

+

895

+

896

+

897

+

898

+

899

+

900

+

901

+

902

+

903

+

904

+

905

+

906

+

907

+

908

+

909

+

910

+

911

+

912

+

913

+

914

+

915

+

916

+

917

+

918

+

919

+

920

+

921

+

922

+

923

+

924

+

925

+

926

+

927

+

928

+

929

+

930

+

931

+

932

+

933

+

934

+

935

+

936

+

937

+

938

+

939

+

940

+

941

+

942

+

943

+

944

+

945

+

946

+

947

+

948

+

949

+

950

+

951

+

952

+

953

+

954

+

955

+

956

+

957

+

958

+

959

+

960

+

961

+

962

+

963

+

964

+

965

+ +
+

""" 

+

Serializers and ModelSerializers are similar to Forms and ModelForms. 

+

Unlike forms, they are not constrained to dealing with HTML output, and 

+

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 

+

response content is handled by parsers and renderers. 

+

""" 

+

from __future__ import unicode_literals 

+

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 

+

from rest_framework.compat import get_concrete_model, six 

+

 

+

# Note: We do the following so that users of the framework can use this style: 

+

# 

+

#     example_field = serializers.CharField(...) 

+

# 

+

# This helps keep the separation between model fields, form fields, and 

+

# serializer fields more explicit. 

+

 

+

from rest_framework.relations import * 

+

from rest_framework.fields import * 

+

 

+

 

+

class NestedValidationError(ValidationError): 

+

    """ 

+

    The default ValidationError behavior is to stringify each item in the list 

+

    if the messages are a list of error messages. 

+

 

+

    In the case of nested serializers, where the parent has many children, 

+

    then the child's `serializer.errors` will be a list of dicts.  In the case 

+

    of a single child, the `serializer.errors` will be a dict. 

+

 

+

    We need to override the default behavior to get properly nested error dicts. 

+

    """ 

+

 

+

    def __init__(self, message): 

+

        if isinstance(message, dict): 

+

            self.messages = [message] 

+

        else: 

+

            self.messages = message 

+

 

+

 

+

class DictWithMetadata(dict): 

+

    """ 

+

    A dict-like object, that can have additional properties attached. 

+

    """ 

+

    def __getstate__(self): 

+

        """ 

+

        Used by pickle (e.g., caching). 

+

        Overridden to remove the metadata from the dict, since it shouldn't be 

+

        pickled and may in some instances be unpickleable. 

+

        """ 

+

        return dict(self) 

+

 

+

 

+

class SortedDictWithMetadata(SortedDict): 

+

    """ 

+

    A sorted dict-like object, that can have additional properties attached. 

+

    """ 

+

    def __getstate__(self): 

+

        """ 

+

        Used by pickle (e.g., caching). 

+

        Overriden to remove the metadata from the dict, since it shouldn't be 

+

        pickle and may in some instances be unpickleable. 

+

        """ 

+

        return SortedDict(self).__dict__ 

+

 

+

 

+

def _is_protected_type(obj): 

+

    """ 

+

    True if the object is a native datatype that does not need to 

+

    be serialized further. 

+

    """ 

+

    return isinstance(obj, ( 

+

        types.NoneType, 

+

        int, long, 

+

        datetime.datetime, datetime.date, datetime.time, 

+

        float, Decimal, 

+

        basestring) 

+

    ) 

+

 

+

 

+

def _get_declared_fields(bases, attrs): 

+

    """ 

+

    Create a list of serializer field instances from the passed in 'attrs', 

+

    plus any fields on the base classes (in 'bases'). 

+

 

+

    Note that all fields from the base classes are used. 

+

    """ 

+

    fields = [(field_name, attrs.pop(field_name)) 

+

              for field_name, obj in list(six.iteritems(attrs)) 

+

              if isinstance(obj, Field)] 

+

    fields.sort(key=lambda x: x[1].creation_counter) 

+

 

+

    # If this class is subclassing another Serializer, add that Serializer's 

+

    # fields.  Note that we loop over the bases in *reverse*. This is necessary 

+

    # in order to maintain the correct order of fields. 

+

    for base in bases[::-1]: 

+

        if hasattr(base, 'base_fields'): 

+

            fields = list(base.base_fields.items()) + fields 

+

 

+

    return SortedDict(fields) 

+

 

+

 

+

class SerializerMetaclass(type): 

+

    def __new__(cls, name, bases, attrs): 

+

        attrs['base_fields'] = _get_declared_fields(bases, attrs) 

+

        return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) 

+

 

+

 

+

class SerializerOptions(object): 

+

    """ 

+

    Meta class options for Serializer 

+

    """ 

+

    def __init__(self, meta): 

+

        self.depth = getattr(meta, 'depth', 0) 

+

        self.fields = getattr(meta, 'fields', ()) 

+

        self.exclude = getattr(meta, 'exclude', ()) 

+

 

+

 

+

class BaseSerializer(WritableField): 

+

    """ 

+

    This is the Serializer implementation. 

+

    We need to implement it as `BaseSerializer` due to metaclass magicks. 

+

    """ 

+

    class Meta(object): 

+

        pass 

+

 

+

    _options_class = SerializerOptions 

+

    _dict_class = SortedDictWithMetadata 

+

 

+

    def __init__(self, instance=None, data=None, files=None, 

+

                 context=None, partial=False, many=None, 

+

                 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_add_remove = allow_add_remove 

+

 

+

        self.context = context or {} 

+

 

+

        self.init_data = data 

+

        self.init_files = files 

+

        self.object = instance 

+

        self.fields = self.get_fields() 

+

 

+

        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') 

+

 

+

        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. 

+

 

+

    def get_default_fields(self): 

+

        """ 

+

        Return the complete set of default fields for the object, as a dict. 

+

        """ 

+

        return {} 

+

 

+

    def get_fields(self): 

+

        """ 

+

        Returns the complete set of fields for the object as a dict. 

+

 

+

        This will be the set of any explicitly declared fields, 

+

        plus the set of fields returned by get_default_fields(). 

+

        """ 

+

        ret = SortedDict() 

+

 

+

        # Get the explicitly declared fields 

+

        base_fields = copy.deepcopy(self.base_fields) 

+

        for key, field in base_fields.items(): 

+

            ret[key] = field 

+

 

+

        # Add in the default fields 

+

        default_fields = self.get_default_fields() 

+

        for key, val in default_fields.items(): 

+

            if key not in ret: 

+

                ret[key] = val 

+

 

+

        # If 'fields' is specified, use those fields, in that order. 

+

        if self.opts.fields: 

+

            assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple' 

+

            new = SortedDict() 

+

            for key in self.opts.fields: 

+

                new[key] = ret[key] 

+

            ret = new 

+

 

+

        # Remove anything in 'exclude' 

+

        if self.opts.exclude: 

+

            assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple' 

+

            for key in self.opts.exclude: 

+

                ret.pop(key, None) 

+

 

+

        for key, field in ret.items(): 

+

            field.initialize(parent=self, field_name=key) 

+

 

+

        return ret 

+

 

+

    ##### 

+

    # Methods to convert or revert from objects <--> primitive representations. 

+

 

+

    def get_field_key(self, field_name): 

+

        """ 

+

        Return the key that should be used for a given field. 

+

        """ 

+

        return field_name 

+

 

+

    def restore_fields(self, data, files): 

+

        """ 

+

        Core of deserialization, together with `restore_object`. 

+

        Converts a dictionary of data into a dictionary of deserialized fields. 

+

        """ 

+

        reverted_data = {} 

+

 

+

        if data is not None and not isinstance(data, dict): 

+

            self._errors['non_field_errors'] = ['Invalid data'] 

+

            return None 

+

 

+

        for field_name, field in self.fields.items(): 

+

            field.initialize(parent=self, field_name=field_name) 

+

            try: 

+

                field.field_from_native(data, files, field_name, reverted_data) 

+

            except ValidationError as err: 

+

                self._errors[field_name] = list(err.messages) 

+

 

+

        return reverted_data 

+

 

+

    def perform_validation(self, attrs): 

+

        """ 

+

        Run `validate_<fieldname>()` and `validate()` methods on the serializer 

+

        """ 

+

        for field_name, field in self.fields.items(): 

+

            if field_name in self._errors: 

+

                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) 

+

 

+

        # If there are already errors, we don't run .validate() because 

+

        # field-validation failed and thus `attrs` may not be complete. 

+

        # which in turn can cause inconsistent validation errors. 

+

        if not self._errors: 

+

            try: 

+

                attrs = self.validate(attrs) 

+

            except ValidationError as err: 

+

                if hasattr(err, 'message_dict'): 

+

                    for field_name, error_messages in err.message_dict.items(): 

+

                        self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) 

+

                elif hasattr(err, 'messages'): 

+

                    self._errors['non_field_errors'] = err.messages 

+

 

+

        return attrs 

+

 

+

    def validate(self, attrs): 

+

        """ 

+

        Stub method, to be overridden in Serializer subclasses 

+

        """ 

+

        return attrs 

+

 

+

    def restore_object(self, attrs, instance=None): 

+

        """ 

+

        Deserialize a dictionary of attributes into an object instance. 

+

        You should override this method to control how deserialized objects 

+

        are instantiated. 

+

        """ 

+

        if instance is not None: 

+

            instance.update(attrs) 

+

            return instance 

+

        return attrs 

+

 

+

    def to_native(self, obj): 

+

        """ 

+

        Serialize objects -> primitives. 

+

        """ 

+

        ret = self._dict_class() 

+

        ret.fields = {} 

+

 

+

        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) 

+

            ret[key] = value 

+

            ret.fields[key] = field 

+

        return ret 

+

 

+

    def from_native(self, data, files): 

+

        """ 

+

        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: 

+

                attrs = self.perform_validation(attrs) 

+

        else: 

+

            self._errors['non_field_errors'] = ['No input provided'] 

+

 

+

        if not self._errors: 

+

            return self.restore_object(attrs, instance=getattr(self, 'object', None)) 

+

 

+

    def field_to_native(self, obj, field_name): 

+

        """ 

+

        Override default so that the serializer can be used as a nested field 

+

        across relationships. 

+

        """ 

+

        if self.source == '*': 

+

            return self.to_native(obj) 

+

 

+

        try: 

+

            source = self.source or field_name 

+

            value = obj 

+

 

+

            for component in source.split('.'): 

+

                value = get_component(value, component) 

+

                if value is None: 

+

                    break 

+

        except ObjectDoesNotExist: 

+

            return None 

+

 

+

        if is_simple_callable(getattr(value, 'all', None)): 

+

            return [self.to_native(item) for item in value.all()] 

+

 

+

        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: 

+

            return [self.to_native(item) for item in value] 

+

        return self.to_native(value) 

+

 

+

    def field_from_native(self, data, files, field_name, into): 

+

        """ 

+

        Override default so that the serializer can be used as a writable 

+

        nested field across relationships. 

+

        """ 

+

        if self.read_only: 

+

            return 

+

 

+

        try: 

+

            value = data[field_name] 

+

        except KeyError: 

+

            if self.default is not None and not self.partial: 

+

                # Note: partial updates shouldn't set defaults 

+

                value = copy.deepcopy(self.default) 

+

            else: 

+

                if self.required: 

+

                    raise ValidationError(self.error_messages['required']) 

+

                return 

+

 

+

        # Set the serializer object if it exists 

+

        obj = getattr(self.parent.object, field_name) if self.parent.object else None 

+

 

+

        if self.source == '*': 

+

            if value: 

+

                into.update(value) 

+

        else: 

+

            if value in (None, ''): 

+

                into[(self.source or field_name)] = None 

+

            else: 

+

                kwargs = { 

+

                    'instance': obj, 

+

                    'data': value, 

+

                    'context': self.context, 

+

                    'partial': self.partial, 

+

                    'many': self.many 

+

                } 

+

                serializer = self.__class__(**kwargs) 

+

 

+

                if serializer.is_valid(): 

+

                    into[self.source or field_name] = serializer.object 

+

                else: 

+

                    # Propagate errors up to our parent 

+

                    raise NestedValidationError(serializer.errors) 

+

 

+

    def get_identity(self, data): 

+

        """ 

+

        This hook is required for bulk update. 

+

        It is used to determine the canonical identity of a given object. 

+

 

+

        Note that the data has not been validated at this point, so we need 

+

        to make sure that we catch any cases of incorrect datatypes being 

+

        passed to this method. 

+

        """ 

+

        try: 

+

            return data.get('id', None) 

+

        except AttributeError: 

+

            return None 

+

 

+

    @property 

+

    def errors(self): 

+

        """ 

+

        Run deserialization and return error data, 

+

        setting self.object if no errors occurred. 

+

        """ 

+

        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: 

+

                ret = [] 

+

                errors = [] 

+

                update = self.object is not None 

+

 

+

                if update: 

+

                    # If this is a bulk update we need to map all the objects 

+

                    # to a canonical identity so we can determine which 

+

                    # individual object is being updated for each item in the 

+

                    # incoming data 

+

                    objects = self.object 

+

                    identities = [self.get_identity(self.to_native(obj)) for obj in objects] 

+

                    identity_to_objects = dict(zip(identities, objects)) 

+

 

+

                if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): 

+

                    for item in data: 

+

                        if update: 

+

                            # Determine which object we're updating 

+

                            identity = self.get_identity(item) 

+

                            self.object = identity_to_objects.pop(identity, None) 

+

                            if self.object is None and not self.allow_add_remove: 

+

                                ret.append(None) 

+

                                errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}) 

+

                                continue 

+

 

+

                        ret.append(self.from_native(item, None)) 

+

                        errors.append(self._errors) 

+

 

+

                    if update: 

+

                        self._deleted = identity_to_objects.values() 

+

 

+

                    self._errors = any(errors) and errors or [] 

+

                else: 

+

                    self._errors = {'non_field_errors': ['Expected a list of items.']} 

+

            else: 

+

                ret = self.from_native(data, files) 

+

 

+

            if not self._errors: 

+

                self.object = ret 

+

 

+

        return self._errors 

+

 

+

    def is_valid(self): 

+

        return not self.errors 

+

 

+

    @property 

+

    def data(self): 

+

        """ 

+

        Returns the serialized data on the serializer. 

+

        """ 

+

        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: 

+

                self._data = [self.to_native(item) for item in obj] 

+

            else: 

+

                self._data = self.to_native(obj) 

+

 

+

        return self._data 

+

 

+

    def save_object(self, obj, **kwargs): 

+

        obj.save(**kwargs) 

+

 

+

    def delete_object(self, obj): 

+

        obj.delete() 

+

 

+

    def save(self, **kwargs): 

+

        """ 

+

        Save the deserialized object and return it. 

+

        """ 

+

        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] 

+

 

+

        return self.object 

+

 

+

    def metadata(self): 

+

        """ 

+

        Return a dictionary of metadata about the fields on the serializer. 

+

        Useful for things like responding to OPTIONS requests, or generating 

+

        API schemas for auto-documentation. 

+

        """ 

+

        return SortedDict( 

+

            [(field_name, field.metadata()) 

+

            for field_name, field in six.iteritems(self.fields)] 

+

        ) 

+

 

+

 

+

class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): 

+

    pass 

+

 

+

 

+

class ModelSerializerOptions(SerializerOptions): 

+

    """ 

+

    Meta class options for ModelSerializer 

+

    """ 

+

    def __init__(self, meta): 

+

        super(ModelSerializerOptions, self).__init__(meta) 

+

        self.model = getattr(meta, 'model', None) 

+

        self.read_only_fields = getattr(meta, 'read_only_fields', ()) 

+

 

+

 

+

class ModelSerializer(Serializer): 

+

    """ 

+

    A serializer that deals with model instances and querysets. 

+

    """ 

+

    _options_class = ModelSerializerOptions 

+

 

+

    field_mapping = { 

+

        models.AutoField: IntegerField, 

+

        models.FloatField: FloatField, 

+

        models.IntegerField: IntegerField, 

+

        models.PositiveIntegerField: IntegerField, 

+

        models.SmallIntegerField: IntegerField, 

+

        models.PositiveSmallIntegerField: IntegerField, 

+

        models.DateTimeField: DateTimeField, 

+

        models.DateField: DateField, 

+

        models.TimeField: TimeField, 

+

        models.DecimalField: DecimalField, 

+

        models.EmailField: EmailField, 

+

        models.CharField: CharField, 

+

        models.URLField: URLField, 

+

        models.SlugField: SlugField, 

+

        models.TextField: CharField, 

+

        models.CommaSeparatedIntegerField: CharField, 

+

        models.BooleanField: BooleanField, 

+

        models.FileField: FileField, 

+

        models.ImageField: ImageField, 

+

    } 

+

 

+

    def get_default_fields(self): 

+

        """ 

+

        Return all the fields that should be serialized for the model. 

+

        """ 

+

 

+

        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 

+

        ret = SortedDict() 

+

        nested = bool(self.opts.depth) 

+

 

+

        # Deal with adding the primary key field 

+

        pk_field = opts.pk 

+

        while pk_field.rel and pk_field.rel.parent_link: 

+

            # If model is a child via multitable inheritance, use parent's pk 

+

            pk_field = pk_field.rel.to._meta.pk 

+

 

+

        field = self.get_pk_field(pk_field) 

+

        if field: 

+

            ret[pk_field.name] = field 

+

 

+

        # Deal with forward relationships 

+

        forward_rels = [field for field in opts.fields if field.serialize] 

+

        forward_rels += [field for field in opts.many_to_many if field.serialize] 

+

 

+

        for model_field in forward_rels: 

+

            has_through_model = False 

+

 

+

            if model_field.rel: 

+

                to_many = isinstance(model_field, 

+

                                     models.fields.related.ManyToManyField) 

+

                related_model = model_field.rel.to 

+

 

+

                if to_many and not model_field.rel.through._meta.auto_created: 

+

                    has_through_model = True 

+

 

+

            if model_field.rel and nested: 

+

                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. ' 

+

                        'Use `get_nested_field(model_field, related_model, ' 

+

                        'to_many) instead', 

+

                        PendingDeprecationWarning 

+

                    ) 

+

                    field = self.get_nested_field(model_field) 

+

                else: 

+

                    field = self.get_nested_field(model_field, related_model, to_many) 

+

            elif model_field.rel: 

+

                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. ' 

+

                        'Use `get_related_field(model_field, related_model, ' 

+

                        'to_many) instead', 

+

                        PendingDeprecationWarning 

+

                    ) 

+

                    field = self.get_related_field(model_field, to_many=to_many) 

+

                else: 

+

                    field = self.get_related_field(model_field, related_model, to_many) 

+

            else: 

+

                field = self.get_field(model_field) 

+

 

+

            if field: 

+

                if has_through_model: 

+

                    field.read_only = True 

+

 

+

                ret[model_field.name] = field 

+

 

+

        # Deal with reverse relationships 

+

        if not self.opts.fields: 

+

            reverse_rels = [] 

+

        else: 

+

            # Reverse relationships are only included if they are explicitly 

+

            # present in the `fields` option on the serializer 

+

            reverse_rels = opts.get_all_related_objects() 

+

            reverse_rels += opts.get_all_related_many_to_many_objects() 

+

 

+

        for relation in reverse_rels: 

+

            accessor_name = relation.get_accessor_name() 

+

            if not self.opts.fields or accessor_name not in self.opts.fields: 

+

                continue 

+

            related_model = relation.model 

+

            to_many = relation.field.rel.multiple 

+

            has_through_model = False 

+

            is_m2m = isinstance(relation.field, 

+

                                models.fields.related.ManyToManyField) 

+

 

+

            if is_m2m and not relation.field.rel.through._meta.auto_created: 

+

                has_through_model = True 

+

 

+

            if nested: 

+

                field = self.get_nested_field(None, related_model, to_many) 

+

            else: 

+

                field = self.get_related_field(None, related_model, to_many) 

+

 

+

            if field: 

+

                if has_through_model: 

+

                    field.read_only = True 

+

 

+

                ret[accessor_name] = field 

+

 

+

        # Add the `read_only` flag to any fields that have bee specified 

+

        # 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 " \ 

+

                "`read_only_fields`, but also added " \ 

+

                "as an explict 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` " \ 

+

                "on serializer '%s'." % \ 

+

                (self.__class__.__name__, field_name) 

+

            ret[field_name].read_only = True 

+

 

+

        return ret 

+

 

+

    def get_pk_field(self, model_field): 

+

        """ 

+

        Returns a default instance of the pk field. 

+

        """ 

+

        return self.get_field(model_field) 

+

 

+

    def get_nested_field(self, model_field, related_model, to_many): 

+

        """ 

+

        Creates a default instance of a nested relational field. 

+

 

+

        Note that model_field will be `None` for reverse relationships. 

+

        """ 

+

        class NestedModelSerializer(ModelSerializer): 

+

            class Meta: 

+

                model = related_model 

+

                depth = self.opts.depth - 1 

+

 

+

        return NestedModelSerializer(many=to_many) 

+

 

+

    def get_related_field(self, model_field, related_model, to_many): 

+

        """ 

+

        Creates a default instance of a flat relational field. 

+

 

+

        Note that model_field will be `None` for reverse relationships. 

+

        """ 

+

        # TODO: filter queryset using: 

+

        # .using(db).complex_filter(self.rel.limit_choices_to) 

+

 

+

        kwargs = { 

+

            'queryset': related_model._default_manager, 

+

            'many': to_many 

+

        } 

+

 

+

        if model_field: 

+

            kwargs['required'] = not(model_field.null or model_field.blank) 

+

 

+

        return PrimaryKeyRelatedField(**kwargs) 

+

 

+

    def get_field(self, model_field): 

+

        """ 

+

        Creates a default instance of a basic non-relational field. 

+

        """ 

+

        kwargs = {} 

+

 

+

        if model_field.null or model_field.blank: 

+

            kwargs['required'] = False 

+

 

+

        if isinstance(model_field, models.AutoField) or not model_field.editable: 

+

            kwargs['read_only'] = True 

+

 

+

        if model_field.has_default(): 

+

            kwargs['default'] = model_field.get_default() 

+

 

+

        if issubclass(model_field.__class__, models.TextField): 

+

            kwargs['widget'] = widgets.Textarea 

+

 

+

        if model_field.verbose_name is not None: 

+

            kwargs['label'] = model_field.verbose_name 

+

 

+

        if model_field.help_text is not None: 

+

            kwargs['help_text'] = model_field.help_text 

+

 

+

        # TODO: TypedChoiceField? 

+

        if model_field.flatchoices:  # This ModelField contains choices 

+

            kwargs['choices'] = model_field.flatchoices 

+

            return ChoiceField(**kwargs) 

+

 

+

        # put this below the ChoiceField because min_value isn't a valid initializer 

+

        if issubclass(model_field.__class__, models.PositiveIntegerField) or\ 

+

                issubclass(model_field.__class__, models.PositiveSmallIntegerField): 

+

            kwargs['min_value'] = 0 

+

 

+

        attribute_dict = { 

+

            models.CharField: ['max_length'], 

+

            models.CommaSeparatedIntegerField: ['max_length'], 

+

            models.DecimalField: ['max_digits', 'decimal_places'], 

+

            models.EmailField: ['max_length'], 

+

            models.FileField: ['max_length'], 

+

            models.ImageField: ['max_length'], 

+

            models.SlugField: ['max_length'], 

+

            models.URLField: ['max_length'], 

+

        } 

+

 

+

        if model_field.__class__ in attribute_dict: 

+

            attributes = attribute_dict[model_field.__class__] 

+

            for attribute in attributes: 

+

                kwargs.update({attribute: getattr(model_field, attribute)}) 

+

 

+

        try: 

+

            return self.field_mapping[model_field.__class__](**kwargs) 

+

        except KeyError: 

+

            return ModelField(model_field=model_field, **kwargs) 

+

 

+

    def get_validation_exclusions(self): 

+

        """ 

+

        Return a list of field names to exclude from model validation. 

+

        """ 

+

        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: 

+

                exclusions.remove(field_name) 

+

        return exclusions 

+

 

+

    def full_clean(self, instance): 

+

        """ 

+

        Perform Django's full_clean, and populate the `errors` dictionary 

+

        if any validation errors occur. 

+

 

+

        Note that we don't perform this inside the `.restore_object()` method, 

+

        so that subclasses can override `.restore_object()`, and still get 

+

        the full_clean validation checking. 

+

        """ 

+

        try: 

+

            instance.full_clean(exclude=self.get_validation_exclusions()) 

+

        except ValidationError as err: 

+

            self._errors = err.message_dict 

+

            return None 

+

        return instance 

+

 

+

    def restore_object(self, attrs, instance=None): 

+

        """ 

+

        Restore the model instance. 

+

        """ 

+

        m2m_data = {} 

+

        related_data = {} 

+

        meta = self.opts.model._meta 

+

 

+

        # 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() 

+

            if field_name in attrs: 

+

                related_data[field_name] = attrs.pop(field_name) 

+

 

+

        # Reverse m2m relations 

+

        for (obj, model) in meta.get_all_related_m2m_objects_with_model(): 

+

            field_name = obj.field.related_query_name() 

+

            if field_name in attrs: 

+

                m2m_data[field_name] = attrs.pop(field_name) 

+

 

+

        # Forward m2m relations 

+

        for field in meta.many_to_many: 

+

            if field.name in attrs: 

+

                m2m_data[field.name] = attrs.pop(field.name) 

+

 

+

        # Update an existing instance... 

+

        if instance is not None: 

+

            for key, val in attrs.items(): 

+

                setattr(instance, key, val) 

+

 

+

        # ...or create a new instance 

+

        else: 

+

            instance = self.opts.model(**attrs) 

+

 

+

        # Any relations that cannot be set until we've 

+

        # saved the model get hidden away on these 

+

        # private attributes, so we can deal with them 

+

        # at the point of save. 

+

        instance._related_data = related_data 

+

        instance._m2m_data = m2m_data 

+

 

+

        return instance 

+

 

+

    def from_native(self, data, files): 

+

        """ 

+

        Override the default method to also include model field validation. 

+

        """ 

+

        instance = super(ModelSerializer, self).from_native(data, files) 

+

        if not self._errors: 

+

            return self.full_clean(instance) 

+

 

+

    def save_object(self, obj, **kwargs): 

+

        """ 

+

        Save the deserialized object and return it. 

+

        """ 

+

        obj.save(**kwargs) 

+

 

+

        if getattr(obj, '_m2m_data', None): 

+

            for accessor_name, object_list in obj._m2m_data.items(): 

+

                setattr(obj, accessor_name, object_list) 

+

            del(obj._m2m_data) 

+

 

+

        if getattr(obj, '_related_data', None): 

+

            for accessor_name, related in obj._related_data.items(): 

+

                setattr(obj, accessor_name, related) 

+

            del(obj._related_data) 

+

 

+

 

+

class HyperlinkedModelSerializerOptions(ModelSerializerOptions): 

+

    """ 

+

    Options for HyperlinkedModelSerializer 

+

    """ 

+

    def __init__(self, meta): 

+

        super(HyperlinkedModelSerializerOptions, self).__init__(meta) 

+

        self.view_name = getattr(meta, 'view_name', None) 

+

        self.lookup_field = getattr(meta, 'lookup_field', None) 

+

 

+

 

+

class HyperlinkedModelSerializer(ModelSerializer): 

+

    """ 

+

    A subclass of ModelSerializer that uses hyperlinked relationships, 

+

    instead of primary key relationships. 

+

    """ 

+

    _options_class = HyperlinkedModelSerializerOptions 

+

    _default_view_name = '%(model_name)s-detail' 

+

    _hyperlink_field_class = HyperlinkedRelatedField 

+

 

+

    def get_default_fields(self): 

+

        fields = super(HyperlinkedModelSerializer, self).get_default_fields() 

+

 

+

        if self.opts.view_name is None: 

+

            self.opts.view_name = self._get_default_view_name(self.opts.model) 

+

 

+

        if 'url' not in fields: 

+

            url_field = HyperlinkedIdentityField( 

+

                view_name=self.opts.view_name, 

+

                lookup_field=self.opts.lookup_field 

+

            ) 

+

            fields.insert(0, 'url', url_field) 

+

 

+

        return fields 

+

 

+

    def get_pk_field(self, model_field): 

+

        if self.opts.fields and model_field.name in self.opts.fields: 

+

            return self.get_field(model_field) 

+

 

+

    def get_related_field(self, model_field, related_model, to_many): 

+

        """ 

+

        Creates a default instance of a flat relational field. 

+

        """ 

+

        # TODO: filter queryset using: 

+

        # .using(db).complex_filter(self.rel.limit_choices_to) 

+

        kwargs = { 

+

            'queryset': related_model._default_manager, 

+

            'view_name': self._get_default_view_name(related_model), 

+

            'many': to_many 

+

        } 

+

 

+

        if model_field: 

+

            kwargs['required'] = not(model_field.null or model_field.blank) 

+

 

+

        if self.opts.lookup_field: 

+

            kwargs['lookup_field'] = self.opts.lookup_field 

+

 

+

        return self._hyperlink_field_class(**kwargs) 

+

 

+

    def get_identity(self, data): 

+

        """ 

+

        This hook is required for bulk update. 

+

        We need to override the default, to use the url as the identity. 

+

        """ 

+

        try: 

+

            return data.get('url', None) 

+

        except AttributeError: 

+

            return None 

+

 

+

    def _get_default_view_name(self, model): 

+

        """ 

+

        Return the view name to use if 'view_name' is not specified in 'Meta' 

+

        """ 

+

        model_meta = model._meta 

+

        format_kwargs = { 

+

            'app_label': model_meta.app_label, 

+

            'model_name': model_meta.object_name.lower() 

+

        } 

+

        return self._default_view_name % format_kwargs 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_settings.html b/htmlcov/rest_framework_settings.html new file mode 100644 index 000000000..ae47b5bc8 --- /dev/null +++ b/htmlcov/rest_framework_settings.html @@ -0,0 +1,465 @@ + + + + + + + + Coverage for rest_framework/settings: 95% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+ +
+

""" 

+

Settings for REST framework are all namespaced in the REST_FRAMEWORK setting. 

+

For example your project's `settings.py` file might look like this: 

+

 

+

REST_FRAMEWORK = { 

+

    'DEFAULT_RENDERER_CLASSES': ( 

+

        'rest_framework.renderers.JSONRenderer', 

+

        'rest_framework.renderers.YAMLRenderer', 

+

    ) 

+

    'DEFAULT_PARSER_CLASSES': ( 

+

        'rest_framework.parsers.JSONParser', 

+

        'rest_framework.parsers.YAMLParser', 

+

    ) 

+

} 

+

 

+

This module provides the `api_setting` object, that is used to access 

+

REST framework settings, checking for user settings first, then falling 

+

back to the defaults. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from django.conf import settings 

+

from django.utils import importlib 

+

 

+

from rest_framework import ISO_8601 

+

from rest_framework.compat import six 

+

 

+

 

+

USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) 

+

 

+

DEFAULTS = { 

+

    # Base API policies 

+

    'DEFAULT_RENDERER_CLASSES': ( 

+

        'rest_framework.renderers.JSONRenderer', 

+

        'rest_framework.renderers.BrowsableAPIRenderer', 

+

    ), 

+

    'DEFAULT_PARSER_CLASSES': ( 

+

        'rest_framework.parsers.JSONParser', 

+

        'rest_framework.parsers.FormParser', 

+

        'rest_framework.parsers.MultiPartParser' 

+

    ), 

+

    'DEFAULT_AUTHENTICATION_CLASSES': ( 

+

        'rest_framework.authentication.SessionAuthentication', 

+

        'rest_framework.authentication.BasicAuthentication' 

+

    ), 

+

    'DEFAULT_PERMISSION_CLASSES': ( 

+

        'rest_framework.permissions.AllowAny', 

+

    ), 

+

    'DEFAULT_THROTTLE_CLASSES': ( 

+

    ), 

+

 

+

    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 

+

        'rest_framework.negotiation.DefaultContentNegotiation', 

+

 

+

    # Genric view behavior 

+

    'DEFAULT_MODEL_SERIALIZER_CLASS': 

+

        'rest_framework.serializers.ModelSerializer', 

+

    'DEFAULT_PAGINATION_SERIALIZER_CLASS': 

+

        'rest_framework.pagination.PaginationSerializer', 

+

    'DEFAULT_FILTER_BACKENDS': (), 

+

 

+

    # Throttling 

+

    'DEFAULT_THROTTLE_RATES': { 

+

        'user': None, 

+

        'anon': None, 

+

    }, 

+

 

+

    # Pagination 

+

    'PAGINATE_BY': None, 

+

    'PAGINATE_BY_PARAM': None, 

+

 

+

    # Authentication 

+

    'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 

+

    'UNAUTHENTICATED_TOKEN': None, 

+

 

+

    # Browser enhancements 

+

    'FORM_METHOD_OVERRIDE': '_method', 

+

    'FORM_CONTENT_OVERRIDE': '_content', 

+

    'FORM_CONTENTTYPE_OVERRIDE': '_content_type', 

+

    'URL_ACCEPT_OVERRIDE': 'accept', 

+

    'URL_FORMAT_OVERRIDE': 'format', 

+

 

+

    'FORMAT_SUFFIX_KWARG': 'format', 

+

 

+

    # Input and output formats 

+

    'DATE_INPUT_FORMATS': ( 

+

        ISO_8601, 

+

    ), 

+

    'DATE_FORMAT': None, 

+

 

+

    'DATETIME_INPUT_FORMATS': ( 

+

        ISO_8601, 

+

    ), 

+

    'DATETIME_FORMAT': None, 

+

 

+

    'TIME_INPUT_FORMATS': ( 

+

        ISO_8601, 

+

    ), 

+

    'TIME_FORMAT': None, 

+

 

+

    # Pending deprecation 

+

    'FILTER_BACKEND': None, 

+

} 

+

 

+

 

+

# List of settings that may be in string import notation. 

+

IMPORT_STRINGS = ( 

+

    'DEFAULT_RENDERER_CLASSES', 

+

    'DEFAULT_PARSER_CLASSES', 

+

    'DEFAULT_AUTHENTICATION_CLASSES', 

+

    'DEFAULT_PERMISSION_CLASSES', 

+

    'DEFAULT_THROTTLE_CLASSES', 

+

    'DEFAULT_CONTENT_NEGOTIATION_CLASS', 

+

    'DEFAULT_MODEL_SERIALIZER_CLASS', 

+

    'DEFAULT_PAGINATION_SERIALIZER_CLASS', 

+

    'DEFAULT_FILTER_BACKENDS', 

+

    'FILTER_BACKEND', 

+

    'UNAUTHENTICATED_USER', 

+

    'UNAUTHENTICATED_TOKEN', 

+

) 

+

 

+

 

+

def perform_import(val, setting_name): 

+

    """ 

+

    If the given setting is a string import notation, 

+

    then perform the necessary import or imports. 

+

    """ 

+

    if isinstance(val, six.string_types): 

+

        return import_from_string(val, setting_name) 

+

    elif isinstance(val, (list, tuple)): 

+

        return [import_from_string(item, setting_name) for item in val] 

+

    return val 

+

 

+

 

+

def import_from_string(val, setting_name): 

+

    """ 

+

    Attempt to import a class from a string representation. 

+

    """ 

+

    try: 

+

        # Nod to tastypie's use of importlib. 

+

        parts = val.split('.') 

+

        module_path, class_name = '.'.join(parts[:-1]), parts[-1] 

+

        module = importlib.import_module(module_path) 

+

        return getattr(module, class_name) 

+

    except ImportError as e: 

+

        msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) 

+

        raise ImportError(msg) 

+

 

+

 

+

class APISettings(object): 

+

    """ 

+

    A settings object, that allows API settings to be accessed as properties. 

+

    For example: 

+

 

+

        from rest_framework.settings import api_settings 

+

        print api_settings.DEFAULT_RENDERER_CLASSES 

+

 

+

    Any setting with string import paths will be automatically resolved 

+

    and return the class, rather than the string literal. 

+

    """ 

+

    def __init__(self, user_settings=None, defaults=None, import_strings=None): 

+

        self.user_settings = user_settings or {} 

+

        self.defaults = defaults or {} 

+

        self.import_strings = import_strings or () 

+

 

+

    def __getattr__(self, attr): 

+

        if attr not in self.defaults.keys(): 

+

            raise AttributeError("Invalid API setting: '%s'" % attr) 

+

 

+

        try: 

+

            # Check if present in user settings 

+

            val = self.user_settings[attr] 

+

        except KeyError: 

+

            # Fall back to defaults 

+

            val = self.defaults[attr] 

+

 

+

        # Coerce import strings into classes 

+

        if val and attr in self.import_strings: 

+

            val = perform_import(val, attr) 

+

 

+

        self.validate_setting(attr, val) 

+

 

+

        # Cache the result 

+

        setattr(self, attr, val) 

+

        return val 

+

 

+

    def validate_setting(self, attr, val): 

+

        if attr == 'FILTER_BACKEND' and val is not None: 

+

            # Make sure we can initialize the class 

+

            val() 

+

 

+

api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_status.html b/htmlcov/rest_framework_status.html new file mode 100644 index 000000000..85f919f6b --- /dev/null +++ b/htmlcov/rest_framework_status.html @@ -0,0 +1,187 @@ + + + + + + + + Coverage for rest_framework/status: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+ +
+

""" 

+

Descriptive HTTP status codes, for code readability. 

+

 

+

See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 

+

And RFC 6585 - http://tools.ietf.org/html/rfc6585 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

HTTP_100_CONTINUE = 100 

+

HTTP_101_SWITCHING_PROTOCOLS = 101 

+

HTTP_200_OK = 200 

+

HTTP_201_CREATED = 201 

+

HTTP_202_ACCEPTED = 202 

+

HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 

+

HTTP_204_NO_CONTENT = 204 

+

HTTP_205_RESET_CONTENT = 205 

+

HTTP_206_PARTIAL_CONTENT = 206 

+

HTTP_300_MULTIPLE_CHOICES = 300 

+

HTTP_301_MOVED_PERMANENTLY = 301 

+

HTTP_302_FOUND = 302 

+

HTTP_303_SEE_OTHER = 303 

+

HTTP_304_NOT_MODIFIED = 304 

+

HTTP_305_USE_PROXY = 305 

+

HTTP_306_RESERVED = 306 

+

HTTP_307_TEMPORARY_REDIRECT = 307 

+

HTTP_400_BAD_REQUEST = 400 

+

HTTP_401_UNAUTHORIZED = 401 

+

HTTP_402_PAYMENT_REQUIRED = 402 

+

HTTP_403_FORBIDDEN = 403 

+

HTTP_404_NOT_FOUND = 404 

+

HTTP_405_METHOD_NOT_ALLOWED = 405 

+

HTTP_406_NOT_ACCEPTABLE = 406 

+

HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 

+

HTTP_408_REQUEST_TIMEOUT = 408 

+

HTTP_409_CONFLICT = 409 

+

HTTP_410_GONE = 410 

+

HTTP_411_LENGTH_REQUIRED = 411 

+

HTTP_412_PRECONDITION_FAILED = 412 

+

HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 

+

HTTP_414_REQUEST_URI_TOO_LONG = 414 

+

HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 

+

HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 

+

HTTP_417_EXPECTATION_FAILED = 417 

+

HTTP_428_PRECONDITION_REQUIRED = 428 

+

HTTP_429_TOO_MANY_REQUESTS = 429 

+

HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 

+

HTTP_500_INTERNAL_SERVER_ERROR = 500 

+

HTTP_501_NOT_IMPLEMENTED = 501 

+

HTTP_502_BAD_GATEWAY = 502 

+

HTTP_503_SERVICE_UNAVAILABLE = 503 

+

HTTP_504_GATEWAY_TIMEOUT = 504 

+

HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 

+

HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_throttling.html b/htmlcov/rest_framework_throttling.html new file mode 100644 index 000000000..778b0293b --- /dev/null +++ b/htmlcov/rest_framework_throttling.html @@ -0,0 +1,533 @@ + + + + + + + + Coverage for rest_framework/throttling: 81% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+ +
+

""" 

+

Provides various throttling policies. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.core.cache import cache 

+

from django.core.exceptions import ImproperlyConfigured 

+

from rest_framework.settings import api_settings 

+

import time 

+

 

+

 

+

class BaseThrottle(object): 

+

    """ 

+

    Rate throttling of requests. 

+

    """ 

+

    def allow_request(self, request, view): 

+

        """ 

+

        Return `True` if the request should be allowed, `False` otherwise. 

+

        """ 

+

        raise NotImplementedError('.allow_request() must be overridden') 

+

 

+

    def wait(self): 

+

        """ 

+

        Optionally, return a recommended number of seconds to wait before 

+

        the next request. 

+

        """ 

+

        return None 

+

 

+

 

+

class SimpleRateThrottle(BaseThrottle): 

+

    """ 

+

    A simple cache implementation, that only requires `.get_cache_key()` 

+

    to be overridden. 

+

 

+

    The rate (requests / seconds) is set by a `throttle` attribute on the View 

+

    class.  The attribute is a string of the form 'number_of_requests/period'. 

+

 

+

    Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') 

+

 

+

    Previous request information used for throttling is stored in the cache. 

+

    """ 

+

 

+

    timer = time.time 

+

    cache_format = 'throtte_%(scope)s_%(ident)s' 

+

    scope = None 

+

    THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES 

+

 

+

    def __init__(self): 

+

        if not getattr(self, 'rate', None): 

+

            self.rate = self.get_rate() 

+

        self.num_requests, self.duration = self.parse_rate(self.rate) 

+

 

+

    def get_cache_key(self, request, view): 

+

        """ 

+

        Should return a unique cache-key which can be used for throttling. 

+

        Must be overridden. 

+

 

+

        May return `None` if the request should not be throttled. 

+

        """ 

+

        raise NotImplementedError('.get_cache_key() must be overridden') 

+

 

+

    def get_rate(self): 

+

        """ 

+

        Determine the string representation of the allowed request rate. 

+

        """ 

+

        if not getattr(self, 'scope', None): 

+

            msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % 

+

                   self.__class__.__name__) 

+

            raise ImproperlyConfigured(msg) 

+

 

+

        try: 

+

            return self.THROTTLE_RATES[self.scope] 

+

        except KeyError: 

+

            msg = "No default throttle rate set for '%s' scope" % self.scope 

+

            raise ImproperlyConfigured(msg) 

+

 

+

    def parse_rate(self, rate): 

+

        """ 

+

        Given the request rate string, return a two tuple of: 

+

        <allowed number of requests>, <period of time in seconds> 

+

        """ 

+

        if rate is None: 

+

            return (None, None) 

+

        num, period = rate.split('/') 

+

        num_requests = int(num) 

+

        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] 

+

        return (num_requests, duration) 

+

 

+

    def allow_request(self, request, view): 

+

        """ 

+

        Implement the check to see if the request should be throttled. 

+

 

+

        On success calls `throttle_success`. 

+

        On failure calls `throttle_failure`. 

+

        """ 

+

        if self.rate is None: 

+

            return True 

+

 

+

        self.key = self.get_cache_key(request, view) 

+

        self.history = cache.get(self.key, []) 

+

        self.now = self.timer() 

+

 

+

        # Drop any requests from the history which have now passed the 

+

        # throttle duration 

+

        while self.history and self.history[-1] <= self.now - self.duration: 

+

            self.history.pop() 

+

        if len(self.history) >= self.num_requests: 

+

            return self.throttle_failure() 

+

        return self.throttle_success() 

+

 

+

    def throttle_success(self): 

+

        """ 

+

        Inserts the current request's timestamp along with the key 

+

        into the cache. 

+

        """ 

+

        self.history.insert(0, self.now) 

+

        cache.set(self.key, self.history, self.duration) 

+

        return True 

+

 

+

    def throttle_failure(self): 

+

        """ 

+

        Called when a request to the API has failed due to throttling. 

+

        """ 

+

        return False 

+

 

+

    def wait(self): 

+

        """ 

+

        Returns the recommended next request time in seconds. 

+

        """ 

+

        if self.history: 

+

            remaining_duration = self.duration - (self.now - self.history[-1]) 

+

        else: 

+

            remaining_duration = self.duration 

+

 

+

        available_requests = self.num_requests - len(self.history) + 1 

+

 

+

        return remaining_duration / float(available_requests) 

+

 

+

 

+

class AnonRateThrottle(SimpleRateThrottle): 

+

    """ 

+

    Limits the rate of API calls that may be made by a anonymous users. 

+

 

+

    The IP address of the request will be used as the unique cache key. 

+

    """ 

+

    scope = 'anon' 

+

 

+

    def get_cache_key(self, request, view): 

+

        if request.user.is_authenticated(): 

+

            return None  # Only throttle unauthenticated requests. 

+

 

+

        ident = request.META.get('REMOTE_ADDR', None) 

+

 

+

        return self.cache_format % { 

+

            'scope': self.scope, 

+

            'ident': ident 

+

        } 

+

 

+

 

+

class UserRateThrottle(SimpleRateThrottle): 

+

    """ 

+

    Limits the rate of API calls that may be made by a given user. 

+

 

+

    The user id will be used as a unique cache key if the user is 

+

    authenticated.  For anonymous requests, the IP address of the request will 

+

    be used. 

+

    """ 

+

    scope = 'user' 

+

 

+

    def get_cache_key(self, request, view): 

+

        if request.user.is_authenticated(): 

+

            ident = request.user.id 

+

        else: 

+

            ident = request.META.get('REMOTE_ADDR', None) 

+

 

+

        return self.cache_format % { 

+

            'scope': self.scope, 

+

            'ident': ident 

+

        } 

+

 

+

 

+

class ScopedRateThrottle(SimpleRateThrottle): 

+

    """ 

+

    Limits the rate of API calls by different amounts for various parts of 

+

    the API.  Any view that has the `throttle_scope` property set will be 

+

    throttled.  The unique cache key will be generated by concatenating the 

+

    user id of the request, and the scope of the view being accessed. 

+

    """ 

+

    scope_attr = 'throttle_scope' 

+

 

+

    def __init__(self): 

+

        # Override the usual SimpleRateThrottle, because we can't determine 

+

        # the rate until called by the view. 

+

        pass 

+

 

+

    def allow_request(self, request, view): 

+

        # We can only determine the scope once we're called by the view. 

+

        self.scope = getattr(view, self.scope_attr, None) 

+

 

+

        # If a view does not have a `throttle_scope` always allow the request 

+

        if not self.scope: 

+

            return True 

+

 

+

        # Determine the allowed request rate as we normally would during 

+

        # the `__init__` call. 

+

        self.rate = self.get_rate() 

+

        self.num_requests, self.duration = self.parse_rate(self.rate) 

+

 

+

        # We can now proceed as normal. 

+

        return super(ScopedRateThrottle, self).allow_request(request, view) 

+

 

+

    def get_cache_key(self, request, view): 

+

        """ 

+

        If `view.throttle_scope` is not set, don't apply this throttle. 

+

 

+

        Otherwise generate the unique cache key by concatenating the user id 

+

        with the '.throttle_scope` property of the view. 

+

        """ 

+

        if request.user.is_authenticated(): 

+

            ident = request.user.id 

+

        else: 

+

            ident = request.META.get('REMOTE_ADDR', None) 

+

 

+

        return self.cache_format % { 

+

            'scope': self.scope, 

+

            'ident': ident 

+

        } 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_urlpatterns.html b/htmlcov/rest_framework_urlpatterns.html new file mode 100644 index 000000000..4c824a770 --- /dev/null +++ b/htmlcov/rest_framework_urlpatterns.html @@ -0,0 +1,205 @@ + + + + + + + + Coverage for rest_framework/urlpatterns: 87% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+ +
+

from __future__ import unicode_literals 

+

from django.core.urlresolvers import RegexURLResolver 

+

from rest_framework.compat import url, include 

+

from rest_framework.settings import api_settings 

+

 

+

 

+

def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): 

+

    ret = [] 

+

    for urlpattern in urlpatterns: 

+

        if isinstance(urlpattern, RegexURLResolver): 

+

            # Set of included URL patterns 

+

            regex = urlpattern.regex.pattern 

+

            namespace = urlpattern.namespace 

+

            app_name = urlpattern.app_name 

+

            kwargs = urlpattern.default_kwargs 

+

            # Add in the included patterns, after applying the suffixes 

+

            patterns = apply_suffix_patterns(urlpattern.url_patterns, 

+

                                             suffix_pattern, 

+

                                             suffix_required) 

+

            ret.append(url(regex, include(patterns, namespace, app_name), kwargs)) 

+

 

+

        else: 

+

            # Regular URL pattern 

+

            regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern 

+

            view = urlpattern._callback or urlpattern._callback_str 

+

            kwargs = urlpattern.default_args 

+

            name = urlpattern.name 

+

            # Add in both the existing and the new urlpattern 

+

            if not suffix_required: 

+

                ret.append(urlpattern) 

+

            ret.append(url(regex, view, kwargs, name)) 

+

 

+

    return ret 

+

 

+

 

+

def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): 

+

    """ 

+

    Supplement existing urlpatterns with corresponding patterns that also 

+

    include a '.format' suffix.  Retains urlpattern ordering. 

+

 

+

    urlpatterns: 

+

        A list of URL patterns. 

+

 

+

    suffix_required: 

+

        If `True`, only suffixed URLs will be generated, and non-suffixed 

+

        URLs will not be used.  Defaults to `False`. 

+

 

+

    allowed: 

+

        An optional tuple/list of allowed suffixes.  eg ['json', 'api'] 

+

        Defaults to `None`, which allows any suffix. 

+

    """ 

+

    suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG 

+

    if allowed: 

+

        if len(allowed) == 1: 

+

            allowed_pattern = allowed[0] 

+

        else: 

+

            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 

+

 

+

    return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_urls.html b/htmlcov/rest_framework_urls.html new file mode 100644 index 000000000..7720a6d40 --- /dev/null +++ b/htmlcov/rest_framework_urls.html @@ -0,0 +1,129 @@ + + + + + + + + Coverage for rest_framework/urls: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+ +
+

""" 

+

Login and logout views for the browsable API. 

+

 

+

Add these to your root URLconf if you're using the browsable API and 

+

your API requires authentication. 

+

 

+

The urls must be namespaced as 'rest_framework', and you should make sure 

+

your authentication settings include `SessionAuthentication`. 

+

 

+

    urlpatterns = patterns('', 

+

        ... 

+

        url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) 

+

    ) 

+

""" 

+

from __future__ import unicode_literals 

+

from rest_framework.compat import patterns, url 

+

 

+

 

+

template_name = {'template_name': 'rest_framework/login.html'} 

+

 

+

urlpatterns = patterns('django.contrib.auth.views', 

+

    url(r'^login/$', 'login', template_name, name='login'), 

+

    url(r'^logout/$', 'logout', template_name, name='logout'), 

+

) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_utils___init__.html b/htmlcov/rest_framework_utils___init__.html new file mode 100644 index 000000000..99eb18c4f --- /dev/null +++ b/htmlcov/rest_framework_utils___init__.html @@ -0,0 +1,81 @@ + + + + + + + + Coverage for rest_framework/utils/__init__: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+ + + +
+
+ + + + + diff --git a/htmlcov/rest_framework_utils_breadcrumbs.html b/htmlcov/rest_framework_utils_breadcrumbs.html new file mode 100644 index 000000000..14fb8955d --- /dev/null +++ b/htmlcov/rest_framework_utils_breadcrumbs.html @@ -0,0 +1,189 @@ + + + + + + + + Coverage for rest_framework/utils/breadcrumbs: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+ +
+

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): 

+

    """ 

+

    Given a url returns a list of breadcrumbs, which are each a 

+

    tuple of (name, url). 

+

    """ 

+

 

+

    from rest_framework.views import APIView 

+

 

+

    def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): 

+

        """ 

+

        Add tuples of (name, url) to the breadcrumbs list, 

+

        progressively chomping off parts of the url. 

+

        """ 

+

 

+

        try: 

+

            (view, unused_args, unused_kwargs) = resolve(url) 

+

        except Exception: 

+

            pass 

+

        else: 

+

            # Check if this is a REST framework view, 

+

            # and if so add it to the breadcrumbs 

+

            cls = getattr(view, 'cls', None) 

+

            if cls is not None and issubclass(cls, APIView): 

+

                # 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) 

+

                    breadcrumbs_list.insert(0, (name, prefix + url)) 

+

                    seen.append(view) 

+

 

+

        if url == '': 

+

            # All done 

+

            return breadcrumbs_list 

+

 

+

        elif url.endswith('/'): 

+

            # Drop trailing slash off the end and continue to try to 

+

            # resolve more breadcrumbs 

+

            url = url.rstrip('/') 

+

            return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen) 

+

 

+

        # Drop trailing non-slash off the end and continue to try to 

+

        # resolve more breadcrumbs 

+

        url = url[:url.rfind('/') + 1] 

+

        return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen) 

+

 

+

    prefix = get_script_prefix().rstrip('/') 

+

    url = url[len(prefix):] 

+

    return breadcrumbs_recursive(url, [], prefix, []) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_utils_encoders.html b/htmlcov/rest_framework_utils_encoders.html new file mode 100644 index 000000000..9f0ca343a --- /dev/null +++ b/htmlcov/rest_framework_utils_encoders.html @@ -0,0 +1,275 @@ + + + + + + + + Coverage for rest_framework/utils/encoders: 73% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+ +
+

""" 

+

Helper classes for parsers. 

+

""" 

+

from __future__ import unicode_literals 

+

from django.utils.datastructures import SortedDict 

+

from django.utils.functional import Promise 

+

from rest_framework.compat import timezone, force_text 

+

from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata 

+

import datetime 

+

import decimal 

+

import types 

+

import json 

+

 

+

 

+

class JSONEncoder(json.JSONEncoder): 

+

    """ 

+

    JSONEncoder subclass that knows how to encode date/time/timedelta, 

+

    decimal types, and generators. 

+

    """ 

+

    def default(self, o): 

+

        # For Date Time string spec, see ECMA 262 

+

        # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 

+

        if isinstance(o, Promise): 

+

            return force_text(o) 

+

        elif isinstance(o, datetime.datetime): 

+

            r = o.isoformat() 

+

            if o.microsecond: 

+

                r = r[:23] + r[26:] 

+

            if r.endswith('+00:00'): 

+

                r = r[:-6] + 'Z' 

+

            return r 

+

        elif isinstance(o, datetime.date): 

+

            return o.isoformat() 

+

        elif isinstance(o, datetime.time): 

+

            if timezone and timezone.is_aware(o): 

+

                raise ValueError("JSON can't represent timezone-aware times.") 

+

            r = o.isoformat() 

+

            if o.microsecond: 

+

                r = r[:12] 

+

            return r 

+

        elif isinstance(o, datetime.timedelta): 

+

            return str(o.total_seconds()) 

+

        elif isinstance(o, decimal.Decimal): 

+

            return str(o) 

+

        elif hasattr(o, '__iter__'): 

+

            return [i for i in o] 

+

        return super(JSONEncoder, self).default(o) 

+

 

+

 

+

try: 

+

    import yaml 

+

except ImportError: 

+

    SafeDumper = None 

+

else: 

+

    # Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py 

+

    class SafeDumper(yaml.SafeDumper): 

+

        """ 

+

        Handles decimals as strings. 

+

        Handles SortedDicts as usual dicts, but preserves field order, rather 

+

        than the usual behaviour of sorting the keys. 

+

        """ 

+

        def represent_decimal(self, data): 

+

            return self.represent_scalar('tag:yaml.org,2002:str', str(data)) 

+

 

+

        def represent_mapping(self, tag, mapping, flow_style=None): 

+

            value = [] 

+

            node = yaml.MappingNode(tag, value, flow_style=flow_style) 

+

            if self.alias_key is not None: 

+

                self.represented_objects[self.alias_key] = node 

+

            best_style = True 

+

            if hasattr(mapping, 'items'): 

+

                mapping = list(mapping.items()) 

+

                if not isinstance(mapping, SortedDict): 

+

                    mapping.sort() 

+

            for item_key, item_value in mapping: 

+

                node_key = self.represent_data(item_key) 

+

                node_value = self.represent_data(item_value) 

+

                if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): 

+

                    best_style = False 

+

                if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): 

+

                    best_style = False 

+

                value.append((node_key, node_value)) 

+

            if flow_style is None: 

+

                if self.default_flow_style is not None: 

+

                    node.flow_style = self.default_flow_style 

+

                else: 

+

                    node.flow_style = best_style 

+

            return node 

+

 

+

    SafeDumper.add_representer(SortedDict, 

+

            yaml.representer.SafeRepresenter.represent_dict) 

+

    SafeDumper.add_representer(DictWithMetadata, 

+

            yaml.representer.SafeRepresenter.represent_dict) 

+

    SafeDumper.add_representer(SortedDictWithMetadata, 

+

            yaml.representer.SafeRepresenter.represent_dict) 

+

    SafeDumper.add_representer(types.GeneratorType, 

+

            yaml.representer.SafeRepresenter.represent_list) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_utils_formatting.html b/htmlcov/rest_framework_utils_formatting.html new file mode 100644 index 000000000..54e1570f7 --- /dev/null +++ b/htmlcov/rest_framework_utils_formatting.html @@ -0,0 +1,241 @@ + + + + + + + + Coverage for rest_framework/utils/formatting: 97% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+ +
+

""" 

+

Utility functions to return a formatted name and description for a given view. 

+

""" 

+

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 

+

import re 

+

 

+

 

+

def _remove_trailing_string(content, trailing): 

+

    """ 

+

    Strip trailing component `trailing` from `content` if it exists. 

+

    Used when generating names from view classes. 

+

    """ 

+

    if content.endswith(trailing) and content != trailing: 

+

        return content[:-len(trailing)] 

+

    return content 

+

 

+

 

+

def _remove_leading_indent(content): 

+

    """ 

+

    Remove leading indent from a block of text. 

+

    Used when generating descriptions from docstrings. 

+

    """ 

+

    whitespace_counts = [len(line) - len(line.lstrip(' ')) 

+

                         for line in content.splitlines()[1:] if line.lstrip()] 

+

 

+

    # unindent the content if needed 

+

    if whitespace_counts: 

+

        whitespace_pattern = '^' + (' ' * min(whitespace_counts)) 

+

        content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) 

+

    content = content.strip('\n') 

+

    return content 

+

 

+

 

+

def _camelcase_to_spaces(content): 

+

    """ 

+

    Translate 'CamelCaseNames' to 'Camel Case Names'. 

+

    Used when generating names from view classes. 

+

    """ 

+

    camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' 

+

    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. 

+

    """ 

+

    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 get_view_description(cls, html=False): 

+

    """ 

+

    Return a description for an `APIView` class or `@api_view` function. 

+

    """ 

+

    description = cls.__doc__ or '' 

+

    description = _remove_leading_indent(description) 

+

    if html: 

+

        return markup_description(description) 

+

    return description 

+

 

+

 

+

def markup_description(description): 

+

    """ 

+

    Apply HTML markup to the given description. 

+

    """ 

+

    if apply_markdown: 

+

        description = apply_markdown(description) 

+

    else: 

+

        description = escape(description).replace('\n', '<br />') 

+

    return mark_safe(description) 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_utils_mediatypes.html b/htmlcov/rest_framework_utils_mediatypes.html new file mode 100644 index 000000000..2ce44ab59 --- /dev/null +++ b/htmlcov/rest_framework_utils_mediatypes.html @@ -0,0 +1,257 @@ + + + + + + + + Coverage for rest_framework/utils/mediatypes: 77% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+ +
+

""" 

+

Handling of media types, as found in HTTP Content-Type and Accept headers. 

+

 

+

See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 

+

""" 

+

from __future__ import unicode_literals 

+

from django.http.multipartparser import parse_header 

+

from rest_framework import HTTP_HEADER_ENCODING 

+

 

+

 

+

def media_type_matches(lhs, rhs): 

+

    """ 

+

    Returns ``True`` if the media type in the first argument <= the 

+

    media type in the second argument.  The media types are strings 

+

    as described by the HTTP spec. 

+

 

+

    Valid media type strings include: 

+

 

+

    'application/json; indent=4' 

+

    'application/json' 

+

    'text/*' 

+

    '*/*' 

+

    """ 

+

    lhs = _MediaType(lhs) 

+

    rhs = _MediaType(rhs) 

+

    return lhs.match(rhs) 

+

 

+

 

+

def order_by_precedence(media_type_lst): 

+

    """ 

+

    Returns a list of sets of media type strings, ordered by precedence. 

+

    Precedence is determined by how specific a media type is: 

+

 

+

    3. 'type/subtype; param=val' 

+

    2. 'type/subtype' 

+

    1. 'type/*' 

+

    0. '*/*' 

+

    """ 

+

    ret = [set(), set(), set(), set()] 

+

    for media_type in media_type_lst: 

+

        precedence = _MediaType(media_type).precedence 

+

        ret[3 - precedence].add(media_type) 

+

    return [media_types for media_types in ret if media_types] 

+

 

+

 

+

class _MediaType(object): 

+

    def __init__(self, media_type_str): 

+

        if media_type_str is None: 

+

            media_type_str = '' 

+

        self.orig = media_type_str 

+

        self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING)) 

+

        self.main_type, sep, self.sub_type = self.full_type.partition('/') 

+

 

+

    def match(self, other): 

+

        """Return true if this MediaType satisfies the given MediaType.""" 

+

        for key in self.params.keys(): 

+

            if key != 'q' and other.params.get(key, None) != self.params.get(key, None): 

+

                return False 

+

 

+

        if self.sub_type != '*' and other.sub_type != '*'  and other.sub_type != self.sub_type: 

+

            return False 

+

 

+

        if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: 

+

            return False 

+

 

+

        return True 

+

 

+

    @property 

+

    def precedence(self): 

+

        """ 

+

        Return a precedence level from 0-3 for the media type given how specific it is. 

+

        """ 

+

        if self.main_type == '*': 

+

            return 0 

+

        elif self.sub_type == '*': 

+

            return 1 

+

        elif not self.params or self.params.keys() == ['q']: 

+

            return 2 

+

        return 3 

+

 

+

    def __str__(self): 

+

        return unicode(self).encode('utf-8') 

+

 

+

    def __unicode__(self): 

+

        ret = "%s/%s" % (self.main_type, self.sub_type) 

+

        for key, val in self.params.items(): 

+

            ret += "; %s=%s" % (key, val) 

+

        return ret 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_views.html b/htmlcov/rest_framework_views.html new file mode 100644 index 000000000..f836e71fb --- /dev/null +++ b/htmlcov/rest_framework_views.html @@ -0,0 +1,793 @@ + + + + + + + + Coverage for rest_framework/views: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+

140

+

141

+

142

+

143

+

144

+

145

+

146

+

147

+

148

+

149

+

150

+

151

+

152

+

153

+

154

+

155

+

156

+

157

+

158

+

159

+

160

+

161

+

162

+

163

+

164

+

165

+

166

+

167

+

168

+

169

+

170

+

171

+

172

+

173

+

174

+

175

+

176

+

177

+

178

+

179

+

180

+

181

+

182

+

183

+

184

+

185

+

186

+

187

+

188

+

189

+

190

+

191

+

192

+

193

+

194

+

195

+

196

+

197

+

198

+

199

+

200

+

201

+

202

+

203

+

204

+

205

+

206

+

207

+

208

+

209

+

210

+

211

+

212

+

213

+

214

+

215

+

216

+

217

+

218

+

219

+

220

+

221

+

222

+

223

+

224

+

225

+

226

+

227

+

228

+

229

+

230

+

231

+

232

+

233

+

234

+

235

+

236

+

237

+

238

+

239

+

240

+

241

+

242

+

243

+

244

+

245

+

246

+

247

+

248

+

249

+

250

+

251

+

252

+

253

+

254

+

255

+

256

+

257

+

258

+

259

+

260

+

261

+

262

+

263

+

264

+

265

+

266

+

267

+

268

+

269

+

270

+

271

+

272

+

273

+

274

+

275

+

276

+

277

+

278

+

279

+

280

+

281

+

282

+

283

+

284

+

285

+

286

+

287

+

288

+

289

+

290

+

291

+

292

+

293

+

294

+

295

+

296

+

297

+

298

+

299

+

300

+

301

+

302

+

303

+

304

+

305

+

306

+

307

+

308

+

309

+

310

+

311

+

312

+

313

+

314

+

315

+

316

+

317

+

318

+

319

+

320

+

321

+

322

+

323

+

324

+

325

+

326

+

327

+

328

+

329

+

330

+

331

+

332

+

333

+

334

+

335

+

336

+

337

+

338

+

339

+

340

+

341

+

342

+

343

+

344

+

345

+

346

+

347

+

348

+

349

+

350

+

351

+

352

+

353

+

354

+

355

+

356

+ +
+

""" 

+

Provides an APIView class that is the base of all views in REST framework. 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from django.core.exceptions import PermissionDenied 

+

from django.http import Http404, HttpResponse 

+

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 

+

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): 

+

    settings = api_settings 

+

 

+

    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES 

+

    parser_classes = api_settings.DEFAULT_PARSER_CLASSES 

+

    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 

+

    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES 

+

    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES 

+

    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS 

+

 

+

    @classmethod 

+

    def as_view(cls, **initkwargs): 

+

        """ 

+

        Store the original class on the view function. 

+

 

+

        This allows us to discover information about the view when we do URL 

+

        reverse lookups.  Used for breadcrumb generation. 

+

        """ 

+

        view = super(APIView, cls).as_view(**initkwargs) 

+

        view.cls = cls 

+

        return view 

+

 

+

    @property 

+

    def allowed_methods(self): 

+

        """ 

+

        Wrap Django's private `_allowed_methods` interface in a public property. 

+

        """ 

+

        return self._allowed_methods() 

+

 

+

    @property 

+

    def default_response_headers(self): 

+

        # TODO: deprecate? 

+

        # TODO: Only vary by accept if multiple renderers 

+

        return { 

+

            'Allow': ', '.join(self.allowed_methods), 

+

            'Vary': 'Accept' 

+

        } 

+

 

+

    def http_method_not_allowed(self, request, *args, **kwargs): 

+

        """ 

+

        If `request.method` does not correspond to a handler method, 

+

        determine what kind of exception to raise. 

+

        """ 

+

        raise exceptions.MethodNotAllowed(request.method) 

+

 

+

    def permission_denied(self, request): 

+

        """ 

+

        If request is not permitted, determine what kind of exception to raise. 

+

        """ 

+

        if not self.request.successful_authenticator: 

+

            raise exceptions.NotAuthenticated() 

+

        raise exceptions.PermissionDenied() 

+

 

+

    def throttled(self, request, wait): 

+

        """ 

+

        If request is throttled, determine what kind of exception to raise. 

+

        """ 

+

        raise exceptions.Throttled(wait) 

+

 

+

    def get_authenticate_header(self, request): 

+

        """ 

+

        If a request is unauthenticated, determine the WWW-Authenticate 

+

        header to use for 401 responses, if any. 

+

        """ 

+

        authenticators = self.get_authenticators() 

+

        if authenticators: 

+

            return authenticators[0].authenticate_header(request) 

+

 

+

    def get_parser_context(self, http_request): 

+

        """ 

+

        Returns a dict that is passed through to Parser.parse(), 

+

        as the `parser_context` keyword argument. 

+

        """ 

+

        # Note: Additionally `request` will also be added to the context 

+

        #       by the Request object. 

+

        return { 

+

            'view': self, 

+

            'args': getattr(self, 'args', ()), 

+

            'kwargs': getattr(self, 'kwargs', {}) 

+

        } 

+

 

+

    def get_renderer_context(self): 

+

        """ 

+

        Returns a dict that is passed through to Renderer.render(), 

+

        as the `renderer_context` keyword argument. 

+

        """ 

+

        # Note: Additionally 'response' will also be added to the context, 

+

        #       by the Response object. 

+

        return { 

+

            'view': self, 

+

            'args': getattr(self, 'args', ()), 

+

            'kwargs': getattr(self, 'kwargs', {}), 

+

            'request': getattr(self, 'request', None) 

+

        } 

+

 

+

    # API policy instantiation methods 

+

 

+

    def get_format_suffix(self, **kwargs): 

+

        """ 

+

        Determine if the request includes a '.json' style format suffix 

+

        """ 

+

        if self.settings.FORMAT_SUFFIX_KWARG: 

+

            return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) 

+

 

+

    def get_renderers(self): 

+

        """ 

+

        Instantiates and returns the list of renderers that this view can use. 

+

        """ 

+

        return [renderer() for renderer in self.renderer_classes] 

+

 

+

    def get_parsers(self): 

+

        """ 

+

        Instantiates and returns the list of parsers that this view can use. 

+

        """ 

+

        return [parser() for parser in self.parser_classes] 

+

 

+

    def get_authenticators(self): 

+

        """ 

+

        Instantiates and returns the list of authenticators that this view can use. 

+

        """ 

+

        return [auth() for auth in self.authentication_classes] 

+

 

+

    def get_permissions(self): 

+

        """ 

+

        Instantiates and returns the list of permissions that this view requires. 

+

        """ 

+

        return [permission() for permission in self.permission_classes] 

+

 

+

    def get_throttles(self): 

+

        """ 

+

        Instantiates and returns the list of throttles that this view uses. 

+

        """ 

+

        return [throttle() for throttle in self.throttle_classes] 

+

 

+

    def get_content_negotiator(self): 

+

        """ 

+

        Instantiate and return the content negotiation class to use. 

+

        """ 

+

        if not getattr(self, '_negotiator', None): 

+

            self._negotiator = self.content_negotiation_class() 

+

        return self._negotiator 

+

 

+

    # API policy implementation methods 

+

 

+

    def perform_content_negotiation(self, request, force=False): 

+

        """ 

+

        Determine which renderer and media type to use render the response. 

+

        """ 

+

        renderers = self.get_renderers() 

+

        conneg = self.get_content_negotiator() 

+

 

+

        try: 

+

            return conneg.select_renderer(request, renderers, self.format_kwarg) 

+

        except Exception: 

+

            if force: 

+

                return (renderers[0], renderers[0].media_type) 

+

            raise 

+

 

+

    def perform_authentication(self, request): 

+

        """ 

+

        Perform authentication on the incoming request. 

+

 

+

        Note that if you override this and simply 'pass', then authentication 

+

        will instead be performed lazily, the first time either 

+

        `request.user` or `request.auth` is accessed. 

+

        """ 

+

        request.user 

+

 

+

    def check_permissions(self, request): 

+

        """ 

+

        Check if the request should be permitted. 

+

        Raises an appropriate exception if the request is not permitted. 

+

        """ 

+

        for permission in self.get_permissions(): 

+

            if not permission.has_permission(request, self): 

+

                self.permission_denied(request) 

+

 

+

    def check_object_permissions(self, request, obj): 

+

        """ 

+

        Check if the request should be permitted for a given object. 

+

        Raises an appropriate exception if the request is not permitted. 

+

        """ 

+

        for permission in self.get_permissions(): 

+

            if not permission.has_object_permission(request, self, obj): 

+

                self.permission_denied(request) 

+

 

+

    def check_throttles(self, request): 

+

        """ 

+

        Check if request should be throttled. 

+

        Raises an appropriate exception if the request is throttled. 

+

        """ 

+

        for throttle in self.get_throttles(): 

+

            if not throttle.allow_request(request, self): 

+

                self.throttled(request, throttle.wait()) 

+

 

+

    # Dispatch methods 

+

 

+

    def initialize_request(self, request, *args, **kargs): 

+

        """ 

+

        Returns the initial request object. 

+

        """ 

+

        parser_context = self.get_parser_context(request) 

+

 

+

        return Request(request, 

+

                       parsers=self.get_parsers(), 

+

                       authenticators=self.get_authenticators(), 

+

                       negotiator=self.get_content_negotiator(), 

+

                       parser_context=parser_context) 

+

 

+

    def initial(self, request, *args, **kwargs): 

+

        """ 

+

        Runs anything that needs to occur prior to calling the method handler. 

+

        """ 

+

        self.format_kwarg = self.get_format_suffix(**kwargs) 

+

 

+

        # Ensure that the incoming request is permitted 

+

        self.perform_authentication(request) 

+

        self.check_permissions(request) 

+

        self.check_throttles(request) 

+

 

+

        # Perform content negotiation and store the accepted info on the request 

+

        neg = self.perform_content_negotiation(request) 

+

        request.accepted_renderer, request.accepted_media_type = neg 

+

 

+

    def finalize_response(self, request, response, *args, **kwargs): 

+

        """ 

+

        Returns the final response object. 

+

        """ 

+

        # Make the error obvious if a proper response is not returned 

+

        assert isinstance(response, HttpResponse), ( 

+

            'Expected a `Response` to be returned from the view, ' 

+

            'but received a `%s`' % type(response) 

+

        ) 

+

 

+

        if isinstance(response, Response): 

+

            if not getattr(request, 'accepted_renderer', None): 

+

                neg = self.perform_content_negotiation(request, force=True) 

+

                request.accepted_renderer, request.accepted_media_type = neg 

+

 

+

            response.accepted_renderer = request.accepted_renderer 

+

            response.accepted_media_type = request.accepted_media_type 

+

            response.renderer_context = self.get_renderer_context() 

+

 

+

        for key, value in self.headers.items(): 

+

            response[key] = value 

+

 

+

        return response 

+

 

+

    def handle_exception(self, exc): 

+

        """ 

+

        Handle any exception that occurs, by returning an appropriate response, 

+

        or re-raising the error. 

+

        """ 

+

        if isinstance(exc, exceptions.Throttled): 

+

            # 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 

+

            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 

+

 

+

    # Note: session based authentication is explicitly CSRF validated, 

+

    # all other authentication is CSRF exempt. 

+

    @csrf_exempt 

+

    def dispatch(self, request, *args, **kwargs): 

+

        """ 

+

        `.dispatch()` is pretty much the same as Django's regular dispatch, 

+

        but with extra hooks for startup, finalize, and exception handling. 

+

        """ 

+

        self.args = args 

+

        self.kwargs = kwargs 

+

        request = self.initialize_request(request, *args, **kwargs) 

+

        self.request = request 

+

        self.headers = self.default_response_headers  # deprecate? 

+

 

+

        try: 

+

            self.initial(request, *args, **kwargs) 

+

 

+

            # Get the appropriate handler method 

+

            if request.method.lower() in self.http_method_names: 

+

                handler = getattr(self, request.method.lower(), 

+

                                  self.http_method_not_allowed) 

+

            else: 

+

                handler = self.http_method_not_allowed 

+

 

+

            response = handler(request, *args, **kwargs) 

+

 

+

        except Exception as exc: 

+

            response = self.handle_exception(exc) 

+

 

+

        self.response = self.finalize_response(request, response, *args, **kwargs) 

+

        return self.response 

+

 

+

    def options(self, request, *args, **kwargs): 

+

        """ 

+

        Handler method for HTTP 'OPTIONS' request. 

+

        We may as well implement this as Django will otherwise provide 

+

        a less useful default implementation. 

+

        """ 

+

        return Response(self.metadata(request), status=status.HTTP_200_OK) 

+

 

+

    def metadata(self, request): 

+

        """ 

+

        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['renders'] = [renderer.media_type for renderer in self.renderer_classes] 

+

        ret['parses'] = [parser.media_type for parser in self.parser_classes] 

+

        return ret 

+ +
+
+ + + + + diff --git a/htmlcov/rest_framework_viewsets.html b/htmlcov/rest_framework_viewsets.html new file mode 100644 index 000000000..8264ddc0c --- /dev/null +++ b/htmlcov/rest_framework_viewsets.html @@ -0,0 +1,359 @@ + + + + + + + + Coverage for rest_framework/viewsets: 95% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

+

28

+

29

+

30

+

31

+

32

+

33

+

34

+

35

+

36

+

37

+

38

+

39

+

40

+

41

+

42

+

43

+

44

+

45

+

46

+

47

+

48

+

49

+

50

+

51

+

52

+

53

+

54

+

55

+

56

+

57

+

58

+

59

+

60

+

61

+

62

+

63

+

64

+

65

+

66

+

67

+

68

+

69

+

70

+

71

+

72

+

73

+

74

+

75

+

76

+

77

+

78

+

79

+

80

+

81

+

82

+

83

+

84

+

85

+

86

+

87

+

88

+

89

+

90

+

91

+

92

+

93

+

94

+

95

+

96

+

97

+

98

+

99

+

100

+

101

+

102

+

103

+

104

+

105

+

106

+

107

+

108

+

109

+

110

+

111

+

112

+

113

+

114

+

115

+

116

+

117

+

118

+

119

+

120

+

121

+

122

+

123

+

124

+

125

+

126

+

127

+

128

+

129

+

130

+

131

+

132

+

133

+

134

+

135

+

136

+

137

+

138

+

139

+ +
+

""" 

+

ViewSets are essentially just a type of class based view, that doesn't provide 

+

any method handlers, such as `get()`, `post()`, etc... but instead has actions, 

+

such as `list()`, `retrieve()`, `create()`, etc... 

+

 

+

Actions are only bound to methods at the point of instantiating the views. 

+

 

+

    user_list = UserViewSet.as_view({'get': 'list'}) 

+

    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 

+

automatically. 

+

 

+

    router = DefaultRouter() 

+

    router.register(r'users', UserViewSet, 'user') 

+

    urlpatterns = router.urls 

+

""" 

+

from __future__ import unicode_literals 

+

 

+

from functools import update_wrapper 

+

from django.utils.decorators import classonlymethod 

+

from rest_framework import views, generics, mixins 

+

 

+

 

+

class ViewSetMixin(object): 

+

    """ 

+

    This is the magic. 

+

 

+

    Overrides `.as_view()` so that it takes an `actions` keyword that performs 

+

    the binding of HTTP methods to actions on the Resource. 

+

 

+

    For example, to create a concrete view binding the 'GET' and 'POST' methods 

+

    to the 'list' and 'create' actions... 

+

 

+

    view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) 

+

    """ 

+

 

+

    @classonlymethod 

+

    def as_view(cls, actions=None, **initkwargs): 

+

        """ 

+

        Because of the way class based views create a closure around the 

+

        instantiated view, we need to totally reimplement `.as_view`, 

+

        and slightly modify the view function that is created and returned. 

+

        """ 

+

        # The suffix initkwarg is reserved for identifing the viewset type 

+

        # eg. 'List' or 'Instance'. 

+

        cls.suffix = None 

+

 

+

        # 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) 

+

            # We also store the mapping of request methods to actions, 

+

            # so that we can later set the action attribute. 

+

            # eg. `self.action = 'list'` on an incoming GET request. 

+

            self.action_map = actions 

+

 

+

            # Bind methods to actions 

+

            # This is the bit that's different to a standard view 

+

            for method, action in actions.items(): 

+

                handler = getattr(self, action) 

+

                setattr(self, method, handler) 

+

 

+

            # Patch this in as it's otherwise only present from 1.5 onwards 

+

            if hasattr(self, 'get') and not hasattr(self, 'head'): 

+

                self.head = self.get 

+

 

+

            # And continue as usual 

+

            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=()) 

+

 

+

        # We need to set these on the view function, so that breadcrumb 

+

        # generation can pick out these bits of information from a 

+

        # resolved URL. 

+

        view.cls = cls 

+

        view.suffix = initkwargs.get('suffix', None) 

+

        return view 

+

 

+

    def initialize_request(self, request, *args, **kargs): 

+

        """ 

+

        Set the `.action` attribute on the view, 

+

        depending on the request method. 

+

        """ 

+

        request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs) 

+

        self.action = self.action_map.get(request.method.lower()) 

+

        return request 

+

 

+

 

+

class ViewSet(ViewSetMixin, views.APIView): 

+

    """ 

+

    The base ViewSet class does not provide any actions by default. 

+

    """ 

+

    pass 

+

 

+

 

+

class GenericViewSet(ViewSetMixin, generics.GenericAPIView): 

+

    """ 

+

    The GenericViewSet class does not provide any actions by default, 

+

    but does include the base set of generic view behavior, such as 

+

    the `get_object` and `get_queryset` methods. 

+

    """ 

+

    pass 

+

 

+

 

+

class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, 

+

                           mixins.ListModelMixin, 

+

                           GenericViewSet): 

+

    """ 

+

    A viewset that provides default `list()` and `retrieve()` actions. 

+

    """ 

+

    pass 

+

 

+

 

+

class ModelViewSet(mixins.CreateModelMixin, 

+

                    mixins.RetrieveModelMixin, 

+

                    mixins.UpdateModelMixin, 

+

                    mixins.DestroyModelMixin, 

+

                    mixins.ListModelMixin, 

+

                    GenericViewSet): 

+

    """ 

+

    A viewset that provides default `create()`, `retrieve()`, `update()`, 

+

    `partial_update()`, `destroy()` and `list()` actions. 

+

    """ 

+

    pass 

+ +
+
+ + + + + diff --git a/htmlcov/status.dat b/htmlcov/status.dat new file mode 100644 index 000000000..9e448e377 --- /dev/null +++ b/htmlcov/status.dat @@ -0,0 +1,1258 @@ +(dp1 +S'files' +p2 +(dp3 +S'rest_framework_utils_encoders' +p4 +(dp5 +S'index' +p6 +(dp7 +S'par' +p8 +I0 +sS'html_filename' +p9 +S'rest_framework_utils_encoders.html' +p10 +sS'name' +p11 +S'rest_framework/utils/encoders' +p12 +sS'nums' +p13 +ccopy_reg +_reconstructor +p14 +(ccoverage.results +Numbers +p15 +c__builtin__ +object +p16 +NtRp17 +(dp18 +S'n_files' +p19 +I1 +sS'n_branches' +p20 +I0 +sS'n_statements' +p21 +I70 +sS'n_excluded' +p22 +I0 +sS'n_missing' +p23 +I19 +sS'n_missing_branches' +p24 +I0 +sbssS'hash' +p25 +S'\x9d|\xea|-p\x0c#\xef\x82\x8c\x91\xf1\xcd}f' +p26 +ssS'rest_framework___init__' +p27 +(dp28 +g6 +(dp29 +g8 +I0 +sg9 +S'rest_framework___init__.html' +p30 +sg11 +S'rest_framework/__init__' +p31 +sg13 +g14 +(g15 +g16 +NtRp32 +(dp33 +g19 +I1 +sg20 +I0 +sg21 +I4 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\xbc\xf1f\xd9[\xd4\xcf\x9cQ\x94H\xfd3)\xea[' +p34 +ssS'rest_framework_urlpatterns' +p35 +(dp36 +g6 +(dp37 +g8 +I0 +sg9 +S'rest_framework_urlpatterns.html' +p38 +sg11 +S'rest_framework/urlpatterns' +p39 +sg13 +g14 +(g15 +g16 +NtRp40 +(dp41 +g19 +I1 +sg20 +I0 +sg21 +I31 +sg22 +I0 +sg23 +I4 +sg24 +I0 +sbssg25 +S"\x84\xb0-\xc6Y\x7f\xebA'\x8c5+\xcf\xf6\xcf\xda" +p42 +ssS'rest_framework_permissions' +p43 +(dp44 +g6 +(dp45 +g8 +I0 +sg9 +S'rest_framework_permissions.html' +p46 +sg11 +S'rest_framework/permissions' +p47 +sg13 +g14 +(g15 +g16 +NtRp48 +(dp49 +g19 +I1 +sg20 +I0 +sg21 +I63 +sg22 +I0 +sg23 +I12 +sg24 +I0 +sbssg25 +S",\xc4,\xda\x05\x86\x17\xe8u2~ls*'\xc1" +p50 +ssS'rest_framework_fields' +p51 +(dp52 +g6 +(dp53 +g8 +I0 +sg9 +S'rest_framework_fields.html' +p54 +sg11 +S'rest_framework/fields' +p55 +sg13 +g14 +(g15 +g16 +NtRp56 +(dp57 +g19 +I1 +sg20 +I0 +sg21 +I594 +sg22 +I0 +sg23 +I80 +sg24 +I0 +sbssg25 +S'\x08\x1b\xd2m\x91l\x14e\x97CDA\x1c&k\xf9' +p58 +ssS'rest_framework_models' +p59 +(dp60 +g6 +(dp61 +g8 +I0 +sg9 +S'rest_framework_models.html' +p62 +sg11 +S'rest_framework/models' +p63 +sg13 +g14 +(g15 +g16 +NtRp64 +(dp65 +g19 +I1 +sg20 +I0 +sg21 +I0 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S' E\xaf\xdd\xe7\xbb\xc4\x11z\xf8\x80\x18v.\xec\xf6' +p66 +ssS'rest_framework_utils_breadcrumbs' +p67 +(dp68 +g6 +(dp69 +g8 +I0 +sg9 +S'rest_framework_utils_breadcrumbs.html' +p70 +sg11 +S'rest_framework/utils/breadcrumbs' +p71 +sg13 +g14 +(g15 +g16 +NtRp72 +(dp73 +g19 +I1 +sg20 +I0 +sg21 +I27 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'V"\xf6\xbc\\m)\x12R4>c\xff\xea\xde\x8b' +p74 +ssS'rest_framework_urls' +p75 +(dp76 +g6 +(dp77 +g8 +I0 +sg9 +S'rest_framework_urls.html' +p78 +sg11 +S'rest_framework/urls' +p79 +sg13 +g14 +(g15 +g16 +NtRp80 +(dp81 +g19 +I1 +sg20 +I0 +sg21 +I4 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\xba\x9b\xdaeu\x17\x8b\xe0e\xc6-\xc5R\xba\xa2\xd5' +p82 +ssS'rest_framework_serializers' +p83 +(dp84 +g6 +(dp85 +g8 +I0 +sg9 +S'rest_framework_serializers.html' +p86 +sg11 +S'rest_framework/serializers' +p87 +sg13 +g14 +(g15 +g16 +NtRp88 +(dp89 +g19 +I1 +sg20 +I0 +sg21 +I464 +sg22 +I0 +sg23 +I27 +sg24 +I0 +sbssg25 +S'O\\\xf6\x81y\x95\xae\x9a)\xe9~\xb8\xab\t\x88#' +p90 +ssS'rest_framework_exceptions' +p91 +(dp92 +g6 +(dp93 +g8 +I0 +sg9 +S'rest_framework_exceptions.html' +p94 +sg11 +S'rest_framework/exceptions' +p95 +sg13 +g14 +(g15 +g16 +NtRp96 +(dp97 +g19 +I1 +sg20 +I0 +sg21 +I51 +sg22 +I0 +sg23 +I2 +sg24 +I0 +sbssg25 +S'\xdd\xcaE\x12\x1f4V\xe6\x91\x11\xef:T\xe1r\xca' +p98 +ssS'rest_framework_status' +p99 +(dp100 +g6 +(dp101 +g8 +I0 +sg9 +S'rest_framework_status.html' +p102 +sg11 +S'rest_framework/status' +p103 +sg13 +g14 +(g15 +g16 +NtRp104 +(dp105 +g19 +I1 +sg20 +I0 +sg21 +I46 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\x97z\xcd\xfd\xdc\x0c\xe3\xa9j\x04\xab\x13]\x98\xbf\x80' +p106 +ssS'rest_framework_relations' +p107 +(dp108 +g6 +(dp109 +g8 +I0 +sg9 +S'rest_framework_relations.html' +p110 +sg11 +S'rest_framework/relations' +p111 +sg13 +g14 +(g15 +g16 +NtRp112 +(dp113 +g19 +I1 +sg20 +I0 +sg21 +I365 +sg22 +I0 +sg23 +I88 +sg24 +I0 +sbssg25 +S'\xdb"\xfe\xc2\xb3\x8a\xe2(\xbeoNk\x1b\xd3H9' +p114 +ssS'rest_framework_authtoken_views' +p115 +(dp116 +g6 +(dp117 +g8 +I0 +sg9 +S'rest_framework_authtoken_views.html' +p118 +sg11 +S'rest_framework/authtoken/views' +p119 +sg13 +g14 +(g15 +g16 +NtRp120 +(dp121 +g19 +I1 +sg20 +I0 +sg21 +I21 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\xb8A\x13\xee\xfc9\x8b\x1eY-\xad\x00\xa7\x9dH]' +p122 +ssS'rest_framework_mixins' +p123 +(dp124 +g6 +(dp125 +g8 +I0 +sg9 +S'rest_framework_mixins.html' +p126 +sg11 +S'rest_framework/mixins' +p127 +sg13 +g14 +(g15 +g16 +NtRp128 +(dp129 +g19 +I1 +sg20 +I0 +sg21 +I97 +sg22 +I0 +sg23 +I7 +sg24 +I0 +sbssg25 +S'\xcd\xe5\x9f\xc2\xbb\xd9\xcb\x14*\x88\x99\xe8\xdf\xd2\xa8\xd6' +p130 +ssS'rest_framework_views' +p131 +(dp132 +g6 +(dp133 +g8 +I0 +sg9 +S'rest_framework_views.html' +p134 +sg11 +S'rest_framework/views' +p135 +sg13 +g14 +(g15 +g16 +NtRp136 +(dp137 +g19 +I1 +sg20 +I0 +sg21 +I146 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'ZBo\x84oh^\x1f\x8c\x94Mp$\xf3\xd2\xa1' +p138 +ssS'rest_framework_generics' +p139 +(dp140 +g6 +(dp141 +g8 +I0 +sg9 +S'rest_framework_generics.html' +p142 +sg11 +S'rest_framework/generics' +p143 +sg13 +g14 +(g15 +g16 +NtRp144 +(dp145 +g19 +I1 +sg20 +I0 +sg21 +I196 +sg22 +I0 +sg23 +I34 +sg24 +I0 +sbssg25 +S'@\x1c\x97\x176\x18\x9c\xfc"| |\xb8^\xbb\x83' +p146 +ssS'rest_framework_utils___init__' +p147 +(dp148 +g6 +(dp149 +g8 +I0 +sg9 +S'rest_framework_utils___init__.html' +p150 +sg11 +S'rest_framework/utils/__init__' +p151 +sg13 +g14 +(g15 +g16 +NtRp152 +(dp153 +g19 +I1 +sg20 +I0 +sg21 +I0 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\xb0\xc8pN\xaf>\xa0\xbaz\x144\xe0A9\xb8?' +p154 +ssS'rest_framework_renderers' +p155 +(dp156 +g6 +(dp157 +g8 +I0 +sg9 +S'rest_framework_renderers.html' +p158 +sg11 +S'rest_framework/renderers' +p159 +sg13 +g14 +(g15 +g16 +NtRp160 +(dp161 +g19 +I1 +sg20 +I0 +sg21 +I282 +sg22 +I0 +sg23 +I23 +sg24 +I0 +sbssg25 +S'\t\x11\xd4\xafO\xae\\*\x8d\xaf\xa4f\xde\x86\xe8N' +p162 +ssS'rest_framework_negotiation' +p163 +(dp164 +g6 +(dp165 +g8 +I0 +sg9 +S'rest_framework_negotiation.html' +p166 +sg11 +S'rest_framework/negotiation' +p167 +sg13 +g14 +(g15 +g16 +NtRp168 +(dp169 +g19 +I1 +sg20 +I0 +sg21 +I41 +sg22 +I0 +sg23 +I4 +sg24 +I0 +sbssg25 +S'\xd2\xa2\x94\xc8}y\xba\x9eZE\xe5M\xa5>\x9f\x8d' +p170 +ssS'rest_framework_throttling' +p171 +(dp172 +g6 +(dp173 +g8 +I0 +sg9 +S'rest_framework_throttling.html' +p174 +sg11 +S'rest_framework/throttling' +p175 +sg13 +g14 +(g15 +g16 +NtRp176 +(dp177 +g19 +I1 +sg20 +I0 +sg21 +I90 +sg22 +I0 +sg23 +I17 +sg24 +I0 +sbssg25 +S'a\xbcT\xe7\xff\x1an\xb5\x886\xa3\xa2e\x90PZ' +p178 +ssS'rest_framework_reverse' +p179 +(dp180 +g6 +(dp181 +g8 +I0 +sg9 +S'rest_framework_reverse.html' +p182 +sg11 +S'rest_framework/reverse' +p183 +sg13 +g14 +(g15 +g16 +NtRp184 +(dp185 +g19 +I1 +sg20 +I0 +sg21 +I12 +sg22 +I0 +sg23 +I3 +sg24 +I0 +sbssg25 +S"#\xe7D\x01\x10\xe8'1\x9c\xc9yX4\xb4\xef\x19" +p186 +ssS'rest_framework_request' +p187 +(dp188 +g6 +(dp189 +g8 +I0 +sg9 +S'rest_framework_request.html' +p190 +sg11 +S'rest_framework/request' +p191 +sg13 +g14 +(g15 +g16 +NtRp192 +(dp193 +g19 +I1 +sg20 +I0 +sg21 +I161 +sg22 +I0 +sg23 +I8 +sg24 +I0 +sbssg25 +S'C\xd4v\x9b\xf2Z\xe47\xe8\xc8\x03\xf4\xf8\xac\xefs' +p194 +ssS'rest_framework_parsers' +p195 +(dp196 +g6 +(dp197 +g8 +I0 +sg9 +S'rest_framework_parsers.html' +p198 +sg11 +S'rest_framework/parsers' +p199 +sg13 +g14 +(g15 +g16 +NtRp200 +(dp201 +g19 +I1 +sg20 +I0 +sg21 +I153 +sg22 +I0 +sg23 +I13 +sg24 +I0 +sbssg25 +S'\x11o\x05[\x99{\x9c\x8bj\xa8\xd0t\xe8\x16\\\xae' +p202 +ssS'rest_framework_settings' +p203 +(dp204 +g6 +(dp205 +g8 +I0 +sg9 +S'rest_framework_settings.html' +p206 +sg11 +S'rest_framework/settings' +p207 +sg13 +g14 +(g15 +g16 +NtRp208 +(dp209 +g19 +I1 +sg20 +I0 +sg21 +I44 +sg22 +I0 +sg23 +I2 +sg24 +I0 +sbssg25 +S'\n\xb8|\x03\xa7d|\xfc9\xda\xb5\xb9\x1a\x00@\xc3' +p210 +ssS'rest_framework_authtoken_models' +p211 +(dp212 +g6 +(dp213 +g8 +I0 +sg9 +S'rest_framework_authtoken_models.html' +p214 +sg11 +S'rest_framework/authtoken/models' +p215 +sg13 +g14 +(g15 +g16 +NtRp216 +(dp217 +g19 +I1 +sg20 +I0 +sg21 +I21 +sg22 +I0 +sg23 +I1 +sg24 +I0 +sbssg25 +S'U;\xc7\xf5{\xf6r\xc7]\x95\xffF\xde\x8caE' +p218 +ssS'rest_framework_decorators' +p219 +(dp220 +g6 +(dp221 +g8 +I0 +sg9 +S'rest_framework_decorators.html' +p222 +sg11 +S'rest_framework/decorators' +p223 +sg13 +g14 +(g15 +g16 +NtRp224 +(dp225 +g19 +I1 +sg20 +I0 +sg21 +I60 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S"\xd4\x88\xa2\x16\xf4#X\xb4X\xe97Lj\xeb\x16'" +p226 +ssS'rest_framework_authentication' +p227 +(dp228 +g6 +(dp229 +g8 +I0 +sg9 +S'rest_framework_authentication.html' +p230 +sg11 +S'rest_framework/authentication' +p231 +sg13 +g14 +(g15 +g16 +NtRp232 +(dp233 +g19 +I1 +sg20 +I0 +sg21 +I169 +sg22 +I0 +sg23 +I33 +sg24 +I0 +sbssg25 +S'^\x80:,\x1cL\xde\t\xc1\x93\xe0\x8b\x11\xf4\xb8\x06' +p234 +ssS'rest_framework_utils_formatting' +p235 +(dp236 +g6 +(dp237 +g8 +I0 +sg9 +S'rest_framework_utils_formatting.html' +p238 +sg11 +S'rest_framework/utils/formatting' +p239 +sg13 +g14 +(g15 +g16 +NtRp240 +(dp241 +g19 +I1 +sg20 +I0 +sg21 +I39 +sg22 +I0 +sg23 +I1 +sg24 +I0 +sbssg25 +S'\xdd\x05M\xeb\xfe\tl\xe6\xdd\xc5k\xae\xa8\xf9um' +p242 +ssS'rest_framework_pagination' +p243 +(dp244 +g6 +(dp245 +g8 +I0 +sg9 +S'rest_framework_pagination.html' +p246 +sg11 +S'rest_framework/pagination' +p247 +sg13 +g14 +(g15 +g16 +NtRp248 +(dp249 +g19 +I1 +sg20 +I0 +sg21 +I43 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'y\xa8f\rv\x8c\x9b\x9a:9\xdc\x89\t>\x0c' +p282 +ssS'rest_framework_viewsets' +p283 +(dp284 +g6 +(dp285 +g8 +I0 +sg9 +S'rest_framework_viewsets.html' +p286 +sg11 +S'rest_framework/viewsets' +p287 +sg13 +g14 +(g15 +g16 +NtRp288 +(dp289 +g19 +I1 +sg20 +I0 +sg21 +I39 +sg22 +I0 +sg23 +I2 +sg24 +I0 +sbssg25 +S'ic\x82\xc6e\x93$\x1b\x0c\x8bK\x10\x0f9\xe8\n' +p290 +ssS'rest_framework_authtoken___init__' +p291 +(dp292 +g6 +(dp293 +g8 +I0 +sg9 +S'rest_framework_authtoken___init__.html' +p294 +sg11 +S'rest_framework/authtoken/__init__' +p295 +sg13 +g14 +(g15 +g16 +NtRp296 +(dp297 +g19 +I1 +sg20 +I0 +sg21 +I0 +sg22 +I0 +sg23 +I0 +sg24 +I0 +sbssg25 +S'\xb0\xc8pN\xaf>\xa0\xbaz\x144\xe0A9\xb8?' +p298 +ssS'rest_framework_routers' +p299 +(dp300 +g6 +(dp301 +g8 +I0 +sg9 +S'rest_framework_routers.html' +p302 +sg11 +S'rest_framework/routers' +p303 +sg13 +g14 +(g15 +g16 +NtRp304 +(dp305 +g19 +I1 +sg20 +I0 +sg21 +I108 +sg22 +I0 +sg23 +I7 +sg24 +I0 +sbssg25 +S'i\xa8[\x1f\x0f|\xd6\xa0R\x98\xa9\xecs\xe53\xb3' +p306 +sssS'version' +p307 +S'3.5.1' +p308 +sS'settings' +p309 +S'\xfe\xa4\x01e\x06\x8a\x97H\x97\xaf\xbf\xcd\xfez\xe4\xbf' +p310 +sS'format' +p311 +I1 +s. \ No newline at end of file diff --git a/htmlcov/style.css b/htmlcov/style.css new file mode 100644 index 000000000..c40357b8b --- /dev/null +++ b/htmlcov/style.css @@ -0,0 +1,275 @@ +/* CSS styles for Coverage. */ +/* Page-wide styles */ +html, body, h1, h2, h3, p, td, th { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; + } + +/* Set baseline grid to 16 pt. */ +body { + font-family: georgia, serif; + font-size: 1em; + } + +html>body { + font-size: 16px; + } + +/* Set base font size to 12/16 */ +p { + font-size: .75em; /* 12/16 */ + line-height: 1.3333em; /* 16/12 */ + } + +table { + border-collapse: collapse; + } + +a.nav { + text-decoration: none; + color: inherit; + } +a.nav:hover { + text-decoration: underline; + color: inherit; + } + +/* Page structure */ +#header { + background: #f8f8f8; + width: 100%; + border-bottom: 1px solid #eee; + } + +#source { + padding: 1em; + font-family: "courier new", monospace; + } + +#indexfile #footer { + margin: 1em 3em; + } + +#pyfile #footer { + margin: 1em 1em; + } + +#footer .content { + padding: 0; + font-size: 85%; + font-family: verdana, sans-serif; + color: #666666; + font-style: italic; + } + +#index { + margin: 1em 0 0 3em; + } + +/* Header styles */ +#header .content { + padding: 1em 3em; + } + +h1 { + font-size: 1.25em; +} + +h2.stats { + margin-top: .5em; + font-size: 1em; +} +.stats span { + border: 1px solid; + padding: .1em .25em; + margin: 0 .1em; + cursor: pointer; + border-color: #999 #ccc #ccc #999; +} +.stats span.hide_run, .stats span.hide_exc, +.stats span.hide_mis, .stats span.hide_par, +.stats span.par.hide_run.hide_par { + border-color: #ccc #999 #999 #ccc; +} +.stats span.par.hide_run { + border-color: #999 #ccc #ccc #999; +} + +/* Help panel */ +#keyboard_icon { + float: right; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffc; + padding: .5em; + border: 1px solid #883; + display: none; +} + +#indexfile .help_panel { + width: 20em; height: 4em; +} + +#pyfile .help_panel { + width: 16em; height: 8em; +} + +.help_panel .legend { + font-style: italic; + margin-bottom: 1em; +} + +#panel_icon { + float: right; + cursor: pointer; +} + +.keyhelp { + margin: .75em; +} + +.keyhelp .key { + border: 1px solid black; + border-color: #888 #333 #333 #888; + padding: .1em .35em; + font-family: monospace; + font-weight: bold; + background: #eee; +} + +/* Source file styles */ +.linenos p { + text-align: right; + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + font-size: .625em; /* 10/16 */ + line-height: 1.6em; /* 16/10 */ + } +.linenos p.highlight { + background: #ffdd00; + } +.linenos p a { + text-decoration: none; + color: #999999; + } +.linenos p a:hover { + text-decoration: underline; + color: #999999; + } + +td.text { + width: 100%; + } +.text p { + margin: 0; + padding: 0 0 0 .5em; + border-left: 2px solid #ffffff; + white-space: nowrap; + } + +.text p.mis { + background: #ffdddd; + border-left: 2px solid #ff0000; + } +.text p.run, .text p.run.hide_par { + background: #ddffdd; + border-left: 2px solid #00ff00; + } +.text p.exc { + background: #eeeeee; + border-left: 2px solid #808080; + } +.text p.par, .text p.par.hide_run { + background: #ffffaa; + border-left: 2px solid #eeee99; + } +.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, +.text p.hide_run.hide_par { + background: inherit; + } + +.text span.annotate { + font-family: georgia; + font-style: italic; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } + +/* Syntax coloring */ +.text .com { + color: green; + font-style: italic; + line-height: 1px; + } +.text .key { + font-weight: bold; + line-height: 1px; + } +.text .str { + color: #000080; + } + +/* index styles */ +#index td, #index th { + text-align: right; + width: 5em; + padding: .25em .5em; + border-bottom: 1px solid #eee; + } +#index th { + font-style: italic; + color: #333; + border-bottom: 1px solid #ccc; + cursor: pointer; + } +#index th:hover { + background: #eee; + border-bottom: 1px solid #999; + } +#index td.left, #index th.left { + padding-left: 0; + } +#index td.right, #index th.right { + padding-right: 0; + } +#index th.headerSortDown, #index th.headerSortUp { + border-bottom: 1px solid #000; + } +#index td.name, #index th.name { + text-align: left; + width: auto; + } +#index td.name a { + text-decoration: none; + color: #000; + } +#index td.name a:hover { + text-decoration: underline; + color: #000; + } +#index tr.total { + } +#index tr.total td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index 291142cf9..fe0711fa2 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.db import models from django.test import TestCase from django.test.client import RequestFactory -from rest_framework import serializers, viewsets +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.response import Response @@ -120,7 +120,7 @@ class TestCustomLookupFields(TestCase): ) -class TestTrailingSlash(TestCase): +class TestTrailingSlashIncluded(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): model = RouterTestModel @@ -135,7 +135,7 @@ class TestTrailingSlash(TestCase): self.assertEqual(expected[idx], self.urls[idx].regex.pattern) -class TestTrailingSlash(TestCase): +class TestTrailingSlashRemoved(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): model = RouterTestModel @@ -149,6 +149,7 @@ class TestTrailingSlash(TestCase): for idx in range(len(expected)): self.assertEqual(expected[idx], self.urls[idx].regex.pattern) + class TestNameableRoot(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): @@ -162,3 +163,31 @@ class TestNameableRoot(TestCase): expected = 'nameable-root' self.assertEqual(expected, self.urls[0].name) + +class TestActionKeywordArgs(TestCase): + """ + Ensure keyword arguments passed in the `@action` decorator + are properly handled. Refs #940. + """ + + def setUp(self): + class TestViewSet(viewsets.ModelViewSet): + permission_classes = [] + + @action(permission_classes=[permissions.AllowAny]) + def custom(self, request, *args, **kwargs): + return Response({ + 'permission_classes': self.permission_classes + }) + + self.router = SimpleRouter() + self.router.register(r'test', TestViewSet, base_name='test') + self.view = self.router.urls[-1].callback + + def test_action_kwargs(self): + request = factory.post('/test/0/custom/') + response = self.view(request) + self.assertEqual( + response.data, + {'permission_classes': [permissions.AllowAny]} + ) From f2e6af89755c34083acb1a5fcd843a480037293f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:04:38 +0100 Subject: [PATCH 011/206] Remove erronous htmlcov files --- htmlcov/coverage_html.js | 372 --- htmlcov/index.html | 404 ---- htmlcov/jquery-1.4.3.min.js | 166 -- htmlcov/jquery.hotkeys.js | 99 - htmlcov/jquery.isonscreen.js | 53 - htmlcov/jquery.tablesorter.min.js | 2 - htmlcov/keybd_closed.png | Bin 264 -> 0 bytes htmlcov/keybd_open.png | Bin 267 -> 0 bytes htmlcov/rest_framework___init__.html | 99 - htmlcov/rest_framework_authentication.html | 767 ------- .../rest_framework_authtoken___init__.html | 81 - htmlcov/rest_framework_authtoken_models.html | 151 -- .../rest_framework_authtoken_serializers.html | 129 -- htmlcov/rest_framework_authtoken_views.html | 133 -- htmlcov/rest_framework_decorators.html | 339 --- htmlcov/rest_framework_exceptions.html | 257 --- htmlcov/rest_framework_fields.html | 1991 ---------------- htmlcov/rest_framework_filters.html | 367 --- htmlcov/rest_framework_generics.html | 1079 --------- htmlcov/rest_framework_mixins.html | 449 ---- htmlcov/rest_framework_models.html | 83 - htmlcov/rest_framework_negotiation.html | 259 --- htmlcov/rest_framework_pagination.html | 269 --- htmlcov/rest_framework_parsers.html | 671 ------ htmlcov/rest_framework_permissions.html | 429 ---- htmlcov/rest_framework_relations.html | 1347 ----------- htmlcov/rest_framework_renderers.html | 1227 ---------- htmlcov/rest_framework_request.html | 819 ------- htmlcov/rest_framework_response.html | 249 -- htmlcov/rest_framework_reverse.html | 127 -- htmlcov/rest_framework_routers.html | 595 ----- htmlcov/rest_framework_serializers.html | 2011 ----------------- htmlcov/rest_framework_settings.html | 465 ---- htmlcov/rest_framework_status.html | 187 -- htmlcov/rest_framework_throttling.html | 533 ----- htmlcov/rest_framework_urlpatterns.html | 205 -- htmlcov/rest_framework_urls.html | 129 -- htmlcov/rest_framework_utils___init__.html | 81 - htmlcov/rest_framework_utils_breadcrumbs.html | 189 -- htmlcov/rest_framework_utils_encoders.html | 275 --- htmlcov/rest_framework_utils_formatting.html | 241 -- htmlcov/rest_framework_utils_mediatypes.html | 257 --- htmlcov/rest_framework_views.html | 793 ------- htmlcov/rest_framework_viewsets.html | 359 --- htmlcov/status.dat | 1258 ----------- htmlcov/style.css | 275 --- 46 files changed, 20271 deletions(-) delete mode 100644 htmlcov/coverage_html.js delete mode 100644 htmlcov/index.html delete mode 100644 htmlcov/jquery-1.4.3.min.js delete mode 100644 htmlcov/jquery.hotkeys.js delete mode 100644 htmlcov/jquery.isonscreen.js delete mode 100644 htmlcov/jquery.tablesorter.min.js delete mode 100644 htmlcov/keybd_closed.png delete mode 100644 htmlcov/keybd_open.png delete mode 100644 htmlcov/rest_framework___init__.html delete mode 100644 htmlcov/rest_framework_authentication.html delete mode 100644 htmlcov/rest_framework_authtoken___init__.html delete mode 100644 htmlcov/rest_framework_authtoken_models.html delete mode 100644 htmlcov/rest_framework_authtoken_serializers.html delete mode 100644 htmlcov/rest_framework_authtoken_views.html delete mode 100644 htmlcov/rest_framework_decorators.html delete mode 100644 htmlcov/rest_framework_exceptions.html delete mode 100644 htmlcov/rest_framework_fields.html delete mode 100644 htmlcov/rest_framework_filters.html delete mode 100644 htmlcov/rest_framework_generics.html delete mode 100644 htmlcov/rest_framework_mixins.html delete mode 100644 htmlcov/rest_framework_models.html delete mode 100644 htmlcov/rest_framework_negotiation.html delete mode 100644 htmlcov/rest_framework_pagination.html delete mode 100644 htmlcov/rest_framework_parsers.html delete mode 100644 htmlcov/rest_framework_permissions.html delete mode 100644 htmlcov/rest_framework_relations.html delete mode 100644 htmlcov/rest_framework_renderers.html delete mode 100644 htmlcov/rest_framework_request.html delete mode 100644 htmlcov/rest_framework_response.html delete mode 100644 htmlcov/rest_framework_reverse.html delete mode 100644 htmlcov/rest_framework_routers.html delete mode 100644 htmlcov/rest_framework_serializers.html delete mode 100644 htmlcov/rest_framework_settings.html delete mode 100644 htmlcov/rest_framework_status.html delete mode 100644 htmlcov/rest_framework_throttling.html delete mode 100644 htmlcov/rest_framework_urlpatterns.html delete mode 100644 htmlcov/rest_framework_urls.html delete mode 100644 htmlcov/rest_framework_utils___init__.html delete mode 100644 htmlcov/rest_framework_utils_breadcrumbs.html delete mode 100644 htmlcov/rest_framework_utils_encoders.html delete mode 100644 htmlcov/rest_framework_utils_formatting.html delete mode 100644 htmlcov/rest_framework_utils_mediatypes.html delete mode 100644 htmlcov/rest_framework_views.html delete mode 100644 htmlcov/rest_framework_viewsets.html delete mode 100644 htmlcov/status.dat delete mode 100644 htmlcov/style.css diff --git a/htmlcov/coverage_html.js b/htmlcov/coverage_html.js deleted file mode 100644 index da3e22c81..000000000 --- a/htmlcov/coverage_html.js +++ /dev/null @@ -1,372 +0,0 @@ -// Coverage.py HTML report browser code. -/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ -/*global coverage: true, document, window, $ */ - -coverage = {}; - -// Find all the elements with shortkey_* class, and use them to assign a shotrtcut key. -coverage.assign_shortkeys = function () { - $("*[class*='shortkey_']").each(function (i, e) { - $.each($(e).attr("class").split(" "), function (i, c) { - if (/^shortkey_/.test(c)) { - $(document).bind('keydown', c.substr(9), function () { - $(e).click(); - }); - } - }); - }); -}; - -// Create the events for the help panel. -coverage.wire_up_help_panel = function () { - $("#keyboard_icon").click(function () { - // Show the help panel, and position it so the keyboard icon in the - // panel is in the same place as the keyboard icon in the header. - $(".help_panel").show(); - var koff = $("#keyboard_icon").offset(); - var poff = $("#panel_icon").position(); - $(".help_panel").offset({ - top: koff.top-poff.top, - left: koff.left-poff.left - }); - }); - $("#panel_icon").click(function () { - $(".help_panel").hide(); - }); -}; - -// Loaded on index.html -coverage.index_ready = function ($) { - // Look for a cookie containing previous sort settings: - var sort_list = []; - var cookie_name = "COVERAGE_INDEX_SORT"; - var i; - - // This almost makes it worth installing the jQuery cookie plugin: - if (document.cookie.indexOf(cookie_name) > -1) { - var cookies = document.cookie.split(";"); - for (i = 0; i < cookies.length; i++) { - var parts = cookies[i].split("="); - - if ($.trim(parts[0]) === cookie_name && parts[1]) { - sort_list = eval("[[" + parts[1] + "]]"); - break; - } - } - } - - // Create a new widget which exists only to save and restore - // the sort order: - $.tablesorter.addWidget({ - id: "persistentSort", - - // Format is called by the widget before displaying: - format: function (table) { - if (table.config.sortList.length === 0 && sort_list.length > 0) { - // This table hasn't been sorted before - we'll use - // our stored settings: - $(table).trigger('sorton', [sort_list]); - } - else { - // This is not the first load - something has - // already defined sorting so we'll just update - // our stored value to match: - sort_list = table.config.sortList; - } - } - }); - - // Configure our tablesorter to handle the variable number of - // columns produced depending on report options: - var headers = []; - var col_count = $("table.index > thead > tr > th").length; - - headers[0] = { sorter: 'text' }; - for (i = 1; i < col_count-1; i++) { - headers[i] = { sorter: 'digit' }; - } - headers[col_count-1] = { sorter: 'percent' }; - - // Enable the table sorter: - $("table.index").tablesorter({ - widgets: ['persistentSort'], - headers: headers - }); - - coverage.assign_shortkeys(); - coverage.wire_up_help_panel(); - - // Watch for page unload events so we can save the final sort settings: - $(window).unload(function () { - document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; - }); -}; - -// -- pyfile stuff -- - -coverage.pyfile_ready = function ($) { - // If we're directed to a particular line number, highlight the line. - var frag = location.hash; - if (frag.length > 2 && frag[1] === 'n') { - $(frag).addClass('highlight'); - coverage.set_sel(parseInt(frag.substr(2), 10)); - } - else { - coverage.set_sel(0); - } - - $(document) - .bind('keydown', 'j', coverage.to_next_chunk_nicely) - .bind('keydown', 'k', coverage.to_prev_chunk_nicely) - .bind('keydown', '0', coverage.to_top) - .bind('keydown', '1', coverage.to_first_chunk) - ; - - coverage.assign_shortkeys(); - coverage.wire_up_help_panel(); -}; - -coverage.toggle_lines = function (btn, cls) { - btn = $(btn); - var hide = "hide_"+cls; - if (btn.hasClass(hide)) { - $("#source ."+cls).removeClass(hide); - btn.removeClass(hide); - } - else { - $("#source ."+cls).addClass(hide); - btn.addClass(hide); - } -}; - -// Return the nth line div. -coverage.line_elt = function (n) { - return $("#t" + n); -}; - -// Return the nth line number div. -coverage.num_elt = function (n) { - return $("#n" + n); -}; - -// Return the container of all the code. -coverage.code_container = function () { - return $(".linenos"); -}; - -// Set the selection. b and e are line numbers. -coverage.set_sel = function (b, e) { - // The first line selected. - coverage.sel_begin = b; - // The next line not selected. - coverage.sel_end = (e === undefined) ? b+1 : e; -}; - -coverage.to_top = function () { - coverage.set_sel(0, 1); - coverage.scroll_window(0); -}; - -coverage.to_first_chunk = function () { - coverage.set_sel(0, 1); - coverage.to_next_chunk(); -}; - -coverage.is_transparent = function (color) { - // Different browsers return different colors for "none". - return color === "transparent" || color === "rgba(0, 0, 0, 0)"; -}; - -coverage.to_next_chunk = function () { - var c = coverage; - - // Find the start of the next colored chunk. - var probe = c.sel_end; - while (true) { - var probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - var color = probe_line.css("background-color"); - if (!c.is_transparent(color)) { - break; - } - probe++; - } - - // There's a next chunk, `probe` points to it. - var begin = probe; - - // Find the end of this chunk. - var next_color = color; - while (next_color === color) { - probe++; - probe_line = c.line_elt(probe); - next_color = probe_line.css("background-color"); - } - c.set_sel(begin, probe); - c.show_selection(); -}; - -coverage.to_prev_chunk = function () { - var c = coverage; - - // Find the end of the prev colored chunk. - var probe = c.sel_begin-1; - var probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - var color = probe_line.css("background-color"); - while (probe > 0 && c.is_transparent(color)) { - probe--; - probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - return; - } - color = probe_line.css("background-color"); - } - - // There's a prev chunk, `probe` points to its last line. - var end = probe+1; - - // Find the beginning of this chunk. - var prev_color = color; - while (prev_color === color) { - probe--; - probe_line = c.line_elt(probe); - prev_color = probe_line.css("background-color"); - } - c.set_sel(probe+1, end); - c.show_selection(); -}; - -// Return the line number of the line nearest pixel position pos -coverage.line_at_pos = function (pos) { - var l1 = coverage.line_elt(1), - l2 = coverage.line_elt(2), - result; - if (l1.length && l2.length) { - var l1_top = l1.offset().top, - line_height = l2.offset().top - l1_top, - nlines = (pos - l1_top) / line_height; - if (nlines < 1) { - result = 1; - } - else { - result = Math.ceil(nlines); - } - } - else { - result = 1; - } - return result; -}; - -// Returns 0, 1, or 2: how many of the two ends of the selection are on -// the screen right now? -coverage.selection_ends_on_screen = function () { - if (coverage.sel_begin === 0) { - return 0; - } - - var top = coverage.line_elt(coverage.sel_begin); - var next = coverage.line_elt(coverage.sel_end-1); - - return ( - (top.isOnScreen() ? 1 : 0) + - (next.isOnScreen() ? 1 : 0) - ); -}; - -coverage.to_next_chunk_nicely = function () { - coverage.finish_scrolling(); - if (coverage.selection_ends_on_screen() === 0) { - // The selection is entirely off the screen: select the top line on - // the screen. - var win = $(window); - coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); - } - coverage.to_next_chunk(); -}; - -coverage.to_prev_chunk_nicely = function () { - coverage.finish_scrolling(); - if (coverage.selection_ends_on_screen() === 0) { - var win = $(window); - coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); - } - coverage.to_prev_chunk(); -}; - -// Select line number lineno, or if it is in a colored chunk, select the -// entire chunk -coverage.select_line_or_chunk = function (lineno) { - var c = coverage; - var probe_line = c.line_elt(lineno); - if (probe_line.length === 0) { - return; - } - var the_color = probe_line.css("background-color"); - if (!c.is_transparent(the_color)) { - // The line is in a highlighted chunk. - // Search backward for the first line. - var probe = lineno; - var color = the_color; - while (probe > 0 && color === the_color) { - probe--; - probe_line = c.line_elt(probe); - if (probe_line.length === 0) { - break; - } - color = probe_line.css("background-color"); - } - var begin = probe + 1; - - // Search forward for the last line. - probe = lineno; - color = the_color; - while (color === the_color) { - probe++; - probe_line = c.line_elt(probe); - color = probe_line.css("background-color"); - } - - coverage.set_sel(begin, probe); - } - else { - coverage.set_sel(lineno); - } -}; - -coverage.show_selection = function () { - var c = coverage; - - // Highlight the lines in the chunk - c.code_container().find(".highlight").removeClass("highlight"); - for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { - c.num_elt(probe).addClass("highlight"); - } - - c.scroll_to_selection(); -}; - -coverage.scroll_to_selection = function () { - // Scroll the page if the chunk isn't fully visible. - if (coverage.selection_ends_on_screen() < 2) { - // Need to move the page. The html,body trick makes it scroll in all - // browsers, got it from http://stackoverflow.com/questions/3042651 - var top = coverage.line_elt(coverage.sel_begin); - var top_pos = parseInt(top.offset().top, 10); - coverage.scroll_window(top_pos - 30); - } -}; - -coverage.scroll_window = function (to_pos) { - $("html,body").animate({scrollTop: to_pos}, 200); -}; - -coverage.finish_scrolling = function () { - $("html,body").stop(true, true); -}; - diff --git a/htmlcov/index.html b/htmlcov/index.html deleted file mode 100644 index 983451658..000000000 --- a/htmlcov/index.html +++ /dev/null @@ -1,404 +0,0 @@ - - - - - Coverage report - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- n - s - m - x - - c   change column sorting -

-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Modulestatementsmissingexcludedcoverage
Total3621406089%
rest_framework/__init__400100%
rest_framework/authentication16933080%
rest_framework/authtoken/__init__000100%
rest_framework/authtoken/models211095%
rest_framework/authtoken/serializers172088%
rest_framework/authtoken/views2100100%
rest_framework/decorators6000100%
rest_framework/exceptions512096%
rest_framework/fields59480087%
rest_framework/filters776092%
rest_framework/generics19634083%
rest_framework/mixins977093%
rest_framework/models000100%
rest_framework/negotiation414090%
rest_framework/pagination4300100%
rest_framework/parsers15313092%
rest_framework/permissions6312081%
rest_framework/relations36588076%
rest_framework/renderers28223092%
rest_framework/request1618095%
rest_framework/response421098%
rest_framework/reverse123075%
rest_framework/routers1087094%
rest_framework/serializers46427094%
rest_framework/settings442095%
rest_framework/status4600100%
rest_framework/throttling9017081%
rest_framework/urlpatterns314087%
rest_framework/urls400100%
rest_framework/utils/__init__000100%
rest_framework/utils/breadcrumbs2700100%
rest_framework/utils/encoders7019073%
rest_framework/utils/formatting391097%
rest_framework/utils/mediatypes4410077%
rest_framework/views14600100%
rest_framework/viewsets392095%
-
- - - - - diff --git a/htmlcov/jquery-1.4.3.min.js b/htmlcov/jquery-1.4.3.min.js deleted file mode 100644 index c941a5f7a..000000000 --- a/htmlcov/jquery-1.4.3.min.js +++ /dev/null @@ -1,166 +0,0 @@ -/*! - * jQuery JavaScript Library v1.4.3 - * http://jquery.com/ - * - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Oct 14 23:10:06 2010 -0400 - */ -(function(E,A){function U(){return false}function ba(){return true}function ja(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ga(a){var b,d,e=[],f=[],h,k,l,n,s,v,B,D;k=c.data(this,this.nodeType?"events":"__events__");if(typeof k==="function")k=k.events;if(!(a.liveFired===this||!k||!k.live||a.button&&a.type==="click")){if(a.namespace)D=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var H=k.live.slice(0);for(n=0;nd)break;a.currentTarget=f.elem;a.data=f.handleObj.data; -a.handleObj=f.handleObj;D=f.handleObj.origHandler.apply(f.elem,arguments);if(D===false||a.isPropagationStopped()){d=f.level;if(D===false)b=false}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(Ha,"`").replace(Ia,"&")}function ka(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Ja.test(b))return c.filter(b, -e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function la(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this,e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var k in e[h])c.event.add(this,h,e[h][k],e[h][k].data)}}})}function Ka(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)} -function ma(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?La:Ma,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a,"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function ca(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Na.test(a)?e(a,h):ca(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)? -e(a,""):c.each(b,function(f,h){ca(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(na.concat.apply([],na.slice(0,b)),function(){d[this]=a});return d}function oa(a){if(!da[a]){var b=c("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";da[a]=d}return da[a]}function ea(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var u=E.document,c=function(){function a(){if(!b.isReady){try{u.documentElement.doScroll("left")}catch(i){setTimeout(a, -1);return}b.ready()}}var b=function(i,r){return new b.fn.init(i,r)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,k=/\S/,l=/^\s+/,n=/\s+$/,s=/\W/,v=/\d/,B=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,D=/^[\],:{}\s]*$/,H=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,G=/(?:^|:|,)(?:\s*\[)+/g,M=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,j=/(msie) ([\w.]+)/,o=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false, -q=[],t,x=Object.prototype.toString,C=Object.prototype.hasOwnProperty,P=Array.prototype.push,N=Array.prototype.slice,R=String.prototype.trim,Q=Array.prototype.indexOf,L={};b.fn=b.prototype={init:function(i,r){var y,z,F;if(!i)return this;if(i.nodeType){this.context=this[0]=i;this.length=1;return this}if(i==="body"&&!r&&u.body){this.context=u;this[0]=u.body;this.selector="body";this.length=1;return this}if(typeof i==="string")if((y=h.exec(i))&&(y[1]||!r))if(y[1]){F=r?r.ownerDocument||r:u;if(z=B.exec(i))if(b.isPlainObject(r)){i= -[u.createElement(z[1])];b.fn.attr.call(i,r,true)}else i=[F.createElement(z[1])];else{z=b.buildFragment([y[1]],[F]);i=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this,i)}else{if((z=u.getElementById(y[2]))&&z.parentNode){if(z.id!==y[2])return f.find(i);this.length=1;this[0]=z}this.context=u;this.selector=i;return this}else if(!r&&!s.test(i)){this.selector=i;this.context=u;i=u.getElementsByTagName(i);return b.merge(this,i)}else return!r||r.jquery?(r||f).find(i):b(r).find(i); -else if(b.isFunction(i))return f.ready(i);if(i.selector!==A){this.selector=i.selector;this.context=i.context}return b.makeArray(i,this)},selector:"",jquery:"1.4.3",length:0,size:function(){return this.length},toArray:function(){return N.call(this,0)},get:function(i){return i==null?this.toArray():i<0?this.slice(i)[0]:this[i]},pushStack:function(i,r,y){var z=b();b.isArray(i)?P.apply(z,i):b.merge(z,i);z.prevObject=this;z.context=this.context;if(r==="find")z.selector=this.selector+(this.selector?" ": -"")+y;else if(r)z.selector=this.selector+"."+r+"("+y+")";return z},each:function(i,r){return b.each(this,i,r)},ready:function(i){b.bindReady();if(b.isReady)i.call(u,b);else q&&q.push(i);return this},eq:function(i){return i===-1?this.slice(i):this.slice(i,+i+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(i){return this.pushStack(b.map(this,function(r,y){return i.call(r, -y,r)}))},end:function(){return this.prevObject||b(null)},push:P,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var i=arguments[0]||{},r=1,y=arguments.length,z=false,F,I,K,J,fa;if(typeof i==="boolean"){z=i;i=arguments[1]||{};r=2}if(typeof i!=="object"&&!b.isFunction(i))i={};if(y===r){i=this;--r}for(;r0)){if(q){for(var r=0;i=q[r++];)i.call(u,b);q=null}b.fn.triggerHandler&&b(u).triggerHandler("ready")}}},bindReady:function(){if(!p){p=true;if(u.readyState==="complete")return setTimeout(b.ready, -1);if(u.addEventListener){u.addEventListener("DOMContentLoaded",t,false);E.addEventListener("load",b.ready,false)}else if(u.attachEvent){u.attachEvent("onreadystatechange",t);E.attachEvent("onload",b.ready);var i=false;try{i=E.frameElement==null}catch(r){}u.documentElement.doScroll&&i&&a()}}},isFunction:function(i){return b.type(i)==="function"},isArray:Array.isArray||function(i){return b.type(i)==="array"},isWindow:function(i){return i&&typeof i==="object"&&"setInterval"in i},isNaN:function(i){return i== -null||!v.test(i)||isNaN(i)},type:function(i){return i==null?String(i):L[x.call(i)]||"object"},isPlainObject:function(i){if(!i||b.type(i)!=="object"||i.nodeType||b.isWindow(i))return false;if(i.constructor&&!C.call(i,"constructor")&&!C.call(i.constructor.prototype,"isPrototypeOf"))return false;for(var r in i);return r===A||C.call(i,r)},isEmptyObject:function(i){for(var r in i)return false;return true},error:function(i){throw i;},parseJSON:function(i){if(typeof i!=="string"||!i)return null;i=b.trim(i); -if(D.test(i.replace(H,"@").replace(w,"]").replace(G,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(i):(new Function("return "+i))();else b.error("Invalid JSON: "+i)},noop:function(){},globalEval:function(i){if(i&&k.test(i)){var r=u.getElementsByTagName("head")[0]||u.documentElement,y=u.createElement("script");y.type="text/javascript";if(b.support.scriptEval)y.appendChild(u.createTextNode(i));else y.text=i;r.insertBefore(y,r.firstChild);r.removeChild(y)}},nodeName:function(i,r){return i.nodeName&&i.nodeName.toUpperCase()=== -r.toUpperCase()},each:function(i,r,y){var z,F=0,I=i.length,K=I===A||b.isFunction(i);if(y)if(K)for(z in i){if(r.apply(i[z],y)===false)break}else for(;F";a=u.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var s=u.createElement("div"); -s.style.width=s.style.paddingLeft="1px";u.body.appendChild(s);c.boxModel=c.support.boxModel=s.offsetWidth===2;if("zoom"in s.style){s.style.display="inline";s.style.zoom=1;c.support.inlineBlockNeedsLayout=s.offsetWidth===2;s.style.display="";s.innerHTML="
";c.support.shrinkWrapBlocks=s.offsetWidth!==2}s.innerHTML="
t
";var v=s.getElementsByTagName("td");c.support.reliableHiddenOffsets=v[0].offsetHeight=== -0;v[0].style.display="";v[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&v[0].offsetHeight===0;s.innerHTML="";u.body.removeChild(s).style.display="none"});a=function(s){var v=u.createElement("div");s="on"+s;var B=s in v;if(!B){v.setAttribute(s,"return;");B=typeof v[s]==="function"}return B};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength", -cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var pa={},Oa=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?pa:a;var e=a.nodeType,f=e?a[c.expando]:null,h=c.cache;if(!(e&&!f&&typeof b==="string"&&d===A)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]= -c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==A)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?pa:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando);else if(d)delete f[e];else for(var k in a)delete a[k]}},acceptData:function(a){if(a.nodeName){var b= -c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){if(typeof a==="undefined")return this.length?c.data(this[0]):null;else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===A){var e=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(e===A&&this.length){e=c.data(this[0],a);if(e===A&&this[0].nodeType===1){e=this[0].getAttribute("data-"+a);if(typeof e=== -"string")try{e=e==="true"?true:e==="false"?false:e==="null"?null:!c.isNaN(e)?parseFloat(e):Oa.test(e)?c.parseJSON(e):e}catch(f){}else e=A}}return e===A&&d[1]?this.data(d[0]):e}else return this.each(function(){var h=c(this),k=[d[0],b];h.triggerHandler("setData"+d[1]+"!",k);c.data(this,a,b);h.triggerHandler("changeData"+d[1]+"!",k)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var e=c.data(a,b);if(!d)return e|| -[];if(!e||c.isArray(d))e=c.data(a,b,c.makeArray(d));else e.push(d);return e}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),e=d.shift();if(e==="inprogress")e=d.shift();if(e){b==="fx"&&d.unshift("inprogress");e.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===A)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this, -a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var qa=/[\n\t]/g,ga=/\s+/,Pa=/\r/g,Qa=/^(?:href|src|style)$/,Ra=/^(?:button|input)$/i,Sa=/^(?:button|input|object|select|textarea)$/i,Ta=/^a(?:rea)?$/i,ra=/^(?:radio|checkbox)$/i;c.fn.extend({attr:function(a,b){return c.access(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this, -a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(s){var v=c(this);v.addClass(a.call(this,s,v.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ga),d=0,e=this.length;d-1)return true;return false}, -val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one";if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var B=c.makeArray(v);c("option",this).each(function(){this.selected= -c.inArray(c(this).val(),B)>=0});if(!B.length)this.selectedIndex=-1}else this.value=v}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return A;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==A;b=e&&c.props[b]||b;if(a.nodeType===1){var h=Qa.test(b);if((b in a||a[b]!==A)&&e&&!h){if(f){b==="type"&&Ra.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); -if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Sa.test(a.nodeName)||Ta.test(a.nodeName)&&a.href?0:A;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return A;a=!c.support.hrefNormalized&&e&& -h?a.getAttribute(b,2):a.getAttribute(b);return a===null?A:a}}});var X=/\.(.*)$/,ha=/^(?:textarea|input|select)$/i,Ha=/\./g,Ia=/ /g,Ua=/[^\w\s.|`]/g,Va=function(a){return a.replace(Ua,"\\$&")},sa={focusin:0,focusout:0};c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var k=a.nodeType?"events":"__events__",l=h[k],n=h.handle;if(typeof l=== -"function"){n=l.handle;l=l.events}else if(!l){a.nodeType||(h[k]=h=function(){});h.events=l={}}if(!n)h.handle=n=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(n.elem,arguments):A};n.elem=a;b=b.split(" ");for(var s=0,v;k=b[s++];){h=f?c.extend({},f):{handler:d,data:e};if(k.indexOf(".")>-1){v=k.split(".");k=v.shift();h.namespace=v.slice(0).sort().join(".")}else{v=[];h.namespace=""}h.type=k;if(!h.guid)h.guid=d.guid;var B=l[k],D=c.event.special[k]||{};if(!B){B=l[k]=[]; -if(!D.setup||D.setup.call(a,e,v,n)===false)if(a.addEventListener)a.addEventListener(k,n,false);else a.attachEvent&&a.attachEvent("on"+k,n)}if(D.add){D.add.call(a,h);if(!h.handler.guid)h.handler.guid=d.guid}B.push(h);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,k=0,l,n,s,v,B,D,H=a.nodeType?"events":"__events__",w=c.data(a),G=w&&w[H];if(w&&G){if(typeof G==="function"){w=G;G=G.events}if(b&&b.type){d=b.handler;b=b.type}if(!b|| -typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in G)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[k++];){v=f;l=f.indexOf(".")<0;n=[];if(!l){n=f.split(".");f=n.shift();s=RegExp("(^|\\.)"+c.map(n.slice(0).sort(),Va).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(B=G[f])if(d){v=c.event.special[f]||{};for(h=e||0;h=0){a.type= -f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return A;a.result=A;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)=== -false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){e=a.target;var k,l=f.replace(X,""),n=c.nodeName(e,"a")&&l==="click",s=c.event.special[l]||{};if((!s._default||s._default.call(d,a)===false)&&!n&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[l]){if(k=e["on"+l])e["on"+l]=null;c.event.triggered=true;e[l]()}}catch(v){}if(k)e["on"+l]=k;c.event.triggered=false}}},handle:function(a){var b,d,e; -d=[];var f,h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var k=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ha.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=va(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===A||f===e))if(e!=null||f){a.type="change";a.liveFired= -A;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",va(a))}},setup:function(){if(this.type=== -"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ha.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ha.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}u.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){sa[b]++===0&&u.addEventListener(a,d,true)},teardown:function(){--sa[b]=== -0&&u.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=A}var k=b==="one"?c.proxy(f,function(n){c(this).unbind(n,k);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var l=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); -(function(){function a(g,j,o,m,p,q){p=0;for(var t=m.length;p0){C=x;break}}x=x[g]}m[p]=C}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,k=true;[0,0].sort(function(){k=false;return 0});var l=function(g,j,o,m){o=o||[];var p=j=j||u;if(j.nodeType!==1&&j.nodeType!==9)return[];if(!g||typeof g!=="string")return o;var q=[],t,x,C,P,N=true,R=l.isXML(j),Q=g,L;do{d.exec("");if(t=d.exec(Q)){Q=t[3];q.push(t[1]);if(t[2]){P=t[3]; -break}}}while(t);if(q.length>1&&s.exec(g))if(q.length===2&&n.relative[q[0]])x=M(q[0]+q[1],j);else for(x=n.relative[q[0]]?[j]:l(q.shift(),j);q.length;){g=q.shift();if(n.relative[g])g+=q.shift();x=M(g,x)}else{if(!m&&q.length>1&&j.nodeType===9&&!R&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){t=l.find(q.shift(),j,R);j=t.expr?l.filter(t.expr,t.set)[0]:t.set[0]}if(j){t=m?{expr:q.pop(),set:D(m)}:l.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&j.parentNode?j.parentNode:j,R);x=t.expr?l.filter(t.expr, -t.set):t.set;if(q.length>0)C=D(x);else N=false;for(;q.length;){t=L=q.pop();if(n.relative[L])t=q.pop();else L="";if(t==null)t=j;n.relative[L](C,t,R)}}else C=[]}C||(C=x);C||l.error(L||g);if(f.call(C)==="[object Array]")if(N)if(j&&j.nodeType===1)for(g=0;C[g]!=null;g++){if(C[g]&&(C[g]===true||C[g].nodeType===1&&l.contains(j,C[g])))o.push(x[g])}else for(g=0;C[g]!=null;g++)C[g]&&C[g].nodeType===1&&o.push(x[g]);else o.push.apply(o,C);else D(C,o);if(P){l(P,p,o,m);l.uniqueSort(o)}return o};l.uniqueSort=function(g){if(w){h= -k;g.sort(w);if(h)for(var j=1;j0};l.find=function(g,j,o){var m;if(!g)return[];for(var p=0,q=n.order.length;p":function(g,j){var o=typeof j==="string",m,p=0,q=g.length;if(o&&!/\W/.test(j))for(j=j.toLowerCase();p=0))o||m.push(t);else if(o)j[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var j=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=j[1]+(j[2]||1)-0;g[3]=j[3]-0}g[0]=e++;return g},ATTR:function(g,j,o, -m,p,q){j=g[1].replace(/\\/g,"");if(!q&&n.attrMap[j])g[1]=n.attrMap[j];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,j,o,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=l(g[3],null,null,j);else{g=l.filter(g[3],j,o,true^p);o||m.push.apply(m,g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== -true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,j,o){return!!l(o[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== -g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,j){return j===0},last:function(g,j,o,m){return j===m.length-1},even:function(g,j){return j%2===0},odd:function(g,j){return j%2===1},lt:function(g,j,o){return jo[3]-0},nth:function(g,j,o){return o[3]- -0===j},eq:function(g,j,o){return o[3]-0===j}},filter:{PSEUDO:function(g,j,o,m){var p=j[1],q=n.filters[p];if(q)return q(g,o,j,m);else if(p==="contains")return(g.textContent||g.innerText||l.getText([g])||"").indexOf(j[3])>=0;else if(p==="not"){j=j[3];o=0;for(m=j.length;o=0}},ID:function(g,j){return g.nodeType===1&&g.getAttribute("id")===j},TAG:function(g,j){return j==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== -j},CLASS:function(g,j){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(j)>-1},ATTR:function(g,j){var o=j[1];o=n.attrHandle[o]?n.attrHandle[o](g):g[o]!=null?g[o]:g.getAttribute(o);var m=o+"",p=j[2],q=j[4];return o==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&o!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,j,o,m){var p=n.setFilters[j[2]]; -if(p)return p(g,o,j,m)}}},s=n.match.POS,v=function(g,j){return"\\"+(j-0+1)},B;for(B in n.match){n.match[B]=RegExp(n.match[B].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[B]=RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[B].source.replace(/\\(\d+)/g,v))}var D=function(g,j){g=Array.prototype.slice.call(g,0);if(j){j.push.apply(j,g);return j}return g};try{Array.prototype.slice.call(u.documentElement.childNodes,0)}catch(H){D=function(g,j){var o=j||[],m=0;if(f.call(g)==="[object Array]")Array.prototype.push.apply(o, -g);else if(typeof g.length==="number")for(var p=g.length;m";var o=u.documentElement;o.insertBefore(g,o.firstChild);if(u.getElementById(j)){n.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:A:[]};n.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}o.removeChild(g); -o=g=null})();(function(){var g=u.createElement("div");g.appendChild(u.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(j,o){var m=o.getElementsByTagName(j[1]);if(j[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(j){return j.getAttribute("href",2)};g=null})();u.querySelectorAll&& -function(){var g=l,j=u.createElement("div");j.innerHTML="

";if(!(j.querySelectorAll&&j.querySelectorAll(".TEST").length===0)){l=function(m,p,q,t){p=p||u;if(!t&&!l.isXML(p))if(p.nodeType===9)try{return D(p.querySelectorAll(m),q)}catch(x){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var C=p.id,P=p.id="__sizzle__";try{return D(p.querySelectorAll("#"+P+" "+m),q)}catch(N){}finally{if(C)p.id=C;else p.removeAttribute("id")}}return g(m,p,q,t)};for(var o in g)l[o]=g[o]; -j=null}}();(function(){var g=u.documentElement,j=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,o=false;try{j.call(u.documentElement,":sizzle")}catch(m){o=true}if(j)l.matchesSelector=function(p,q){try{if(o||!n.match.PSEUDO.test(q))return j.call(p,q)}catch(t){}return l(q,null,null,[p]).length>0}})();(function(){var g=u.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== -0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(j,o,m){if(typeof o.getElementsByClassName!=="undefined"&&!m)return o.getElementsByClassName(j[1])};g=null}}})();l.contains=u.documentElement.contains?function(g,j){return g!==j&&(g.contains?g.contains(j):true)}:function(g,j){return!!(g.compareDocumentPosition(j)&16)};l.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var M=function(g, -j){for(var o=[],m="",p,q=j.nodeType?[j]:j;p=n.match.PSEUDO.exec(g);){m+=p[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;p=0;for(var t=q.length;p0)for(var h=d;h0},closest:function(a, -b){var d=[],e,f,h=this[0];if(c.isArray(a)){var k={},l,n=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:n})}h=h.parentNode;n++}}return d}k=$a.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h|| -!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context):c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}}); -c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling", -d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Wa.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||Ya.test(e))&&Xa.test(a))f=f.reverse();return this.pushStack(f,a,Za.call(arguments).join(","))}}); -c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===A||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var xa=/ jQuery\d+="(?:\d+|null)"/g, -$=/^\s+/,ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,za=/<([\w:]+)/,ab=/\s]+\/)>/g,O={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"], -area:[1,"",""],_default:[0,"",""]};O.optgroup=O.option;O.tbody=O.tfoot=O.colgroup=O.caption=O.thead;O.th=O.td;if(!c.support.htmlSerialize)O._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==A)return this.empty().append((this[0]&&this[0].ownerDocument||u).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this, -d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})}, -unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a= -c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*")); -c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(xa,"").replace(cb,'="$1">').replace($, -"")],e)[0]}else return this.cloneNode(true)});if(a===true){la(this,b);la(this.find("*"),b.find("*"))}return b},html:function(a){if(a===A)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(xa,""):null;else if(typeof a==="string"&&!Aa.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!O[(za.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ya,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?l.cloneNode(true):l)}k.length&&c.each(k,Ka)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:u;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===u&&!Aa.test(a[0])&&(c.support.checkClone|| -!Ba.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h= -d.length;f0?this.clone(true):this).get();c(d[f])[b](k);e=e.concat(k)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||u;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||u;for(var f=[],h=0,k;(k=a[h])!=null;h++){if(typeof k==="number")k+="";if(k){if(typeof k==="string"&&!bb.test(k))k=b.createTextNode(k);else if(typeof k==="string"){k=k.replace(ya,"<$1>");var l=(za.exec(k)||["",""])[1].toLowerCase(),n=O[l]||O._default, -s=n[0],v=b.createElement("div");for(v.innerHTML=n[1]+k+n[2];s--;)v=v.lastChild;if(!c.support.tbody){s=ab.test(k);l=l==="table"&&!s?v.firstChild&&v.firstChild.childNodes:n[1]===""&&!s?v.childNodes:[];for(n=l.length-1;n>=0;--n)c.nodeName(l[n],"tbody")&&!l[n].childNodes.length&&l[n].parentNode.removeChild(l[n])}!c.support.leadingWhitespace&&$.test(k)&&v.insertBefore(b.createTextNode($.exec(k)[0]),v.firstChild);k=v.childNodes}if(k.nodeType)f.push(k);else f=c.merge(f,k)}}if(d)for(h=0;f[h];h++)if(e&& -c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script"))));d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,k=0,l;(l=a[k])!=null;k++)if(!(l.nodeName&&c.noData[l.nodeName.toLowerCase()]))if(d=l[c.expando]){if((b=e[d])&&b.events)for(var n in b.events)f[n]? -c.event.remove(l,n):c.removeEvent(l,n,b.handle);if(h)delete l[c.expando];else l.removeAttribute&&l.removeAttribute(c.expando);delete e[d]}}});var Ca=/alpha\([^)]*\)/i,db=/opacity=([^)]*)/,eb=/-([a-z])/ig,fb=/([A-Z])/g,Da=/^-?\d+(?:px)?$/i,gb=/^-?\d/,hb={position:"absolute",visibility:"hidden",display:"block"},La=["Left","Right"],Ma=["Top","Bottom"],W,ib=u.defaultView&&u.defaultView.getComputedStyle,jb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===A)return this; -return c.access(this,a,b,true,function(d,e,f){return f!==A?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true,zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),k=a.style,l=c.cssHooks[h];b=c.cssProps[h]|| -h;if(d!==A){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!l||!("set"in l)||(d=l.set(a,d))!==A)try{k[b]=d}catch(n){}}}else{if(l&&"get"in l&&(f=l.get(a,false,e))!==A)return f;return k[b]}}},css:function(a,b,d){var e,f=c.camelCase(b),h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==A)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]= -e[f]},camelCase:function(a){return a.replace(eb,jb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=ma(d,b,f);else c.swap(d,hb,function(){h=ma(d,b,f)});return h+"px"}},set:function(d,e){if(Da.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return db.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"": -b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f=d.filter||"";d.filter=Ca.test(f)?f.replace(Ca,e):d.filter+" "+e}};if(ib)W=function(a,b,d){var e;d=d.replace(fb,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return A;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};else if(u.documentElement.currentStyle)W=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b], -h=a.style;if(!Da.test(f)&&gb.test(f)){d=h.left;e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f};if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var kb=c.now(),lb=/)<[^<]*)*<\/script>/gi, -mb=/^(?:select|textarea)/i,nb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ob=/^(?:GET|HEAD|DELETE)$/,Na=/\[\]$/,T=/\=\?(&|$)/,ia=/\?/,pb=/([?&])_=[^&]*/,qb=/^(\w+:)?\/\/([^\/?#]+)/,rb=/%20/g,sb=/#.*$/,Ea=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ea)return Ea.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d= -b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(k,l){if(l==="success"||l==="notmodified")h.html(f?c("
").append(k.responseText.replace(lb,"")).find(f):k.responseText);d&&h.each(d,[k.responseText,l,k])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& -!this.disabled&&(this.checked||mb.test(this.nodeName)||nb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, -getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", -script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),k=ob.test(h);b.url=b.url.replace(sb,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ia.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| -!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+kb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var l=E[d];E[d]=function(m){f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);if(c.isFunction(l))l(m);else{E[d]=A;try{delete E[d]}catch(p){}}v&&v.removeChild(B)}}if(b.dataType==="script"&&b.cache===null)b.cache= -false;if(b.cache===false&&h==="GET"){var n=c.now(),s=b.url.replace(pb,"$1_="+n);b.url=s+(s===b.url?(ia.test(b.url)?"&":"?")+"_="+n:"")}if(b.data&&h==="GET")b.url+=(ia.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");n=(n=qb.exec(b.url))&&(n[1]&&n[1]!==location.protocol||n[2]!==location.host);if(b.dataType==="script"&&h==="GET"&&n){var v=u.getElementsByTagName("head")[0]||u.documentElement,B=u.createElement("script");if(b.scriptCharset)B.charset=b.scriptCharset;B.src= -b.url;if(!d){var D=false;B.onload=B.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);B.onload=B.onreadystatechange=null;v&&B.parentNode&&v.removeChild(B)}}}v.insertBefore(B,v.firstChild);return A}var H=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!k||a&&a.contentType)w.setRequestHeader("Content-Type", -b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}n||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(G){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& -c.triggerGlobal(b,"ajaxSend",[w,b]);var M=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){H||c.handleComplete(b,w,e,f);H=true;if(w)w.onreadystatechange=c.noop}else if(!H&&w&&(w.readyState===4||m==="timeout")){H=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| -c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&g.call&&g.call(w);M("abort")}}catch(j){}b.async&&b.timeout>0&&setTimeout(function(){w&&!H&&M("timeout")},b.timeout);try{w.send(k||b.data==null?null:b.data)}catch(o){c.handleError(b,w,null,o);c.handleComplete(b,w,e,f)}b.async||M();return w}},param:function(a,b){var d=[],e=function(h,k){k=c.isFunction(k)?k():k;d[d.length]=encodeURIComponent(h)+ -"="+encodeURIComponent(k)};if(b===A)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)ca(f,a[f],b,e);return d.join("&").replace(rb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess",[b,a])},handleComplete:function(a, -b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),e=a.getResponseHeader("Etag"); -if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}});if(E.ActiveXObject)c.ajaxSettings.xhr= -function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var da={},tb=/^(?:toggle|show|hide)$/,ub=/^([+\-]=)?([\d+.\-]+)(.*)$/,aa,na=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show",3),a,b,d);else{a= -0;for(b=this.length;a=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, -d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* -Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(h){return f.step(h)} -this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var f=this;a=c.fx;e.elem=this.elem;if(e()&&c.timers.push(e)&&!aa)aa=setInterval(a.tick,a.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; -this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(l,n){f.style["overflow"+n]=h.overflow[l]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| -this.options.show)for(var k in this.options.curAnim)c.style(this.elem,k,this.options.orig[k]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= -c.timers,b=0;b-1;e={};var s={};if(n)s=f.position();k=n?s.top:parseInt(k,10)||0;l=n?s.left:parseInt(l,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+k;if(b.left!=null)e.left=b.left-h.left+l;"using"in b?b.using.call(a, -e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Fa.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||u.body;a&&!Fa.test(a.nodeName)&& -c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==A)return this.each(function(){if(h=ea(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=ea(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); -c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(h){var k=c(this);k[d](e.call(this,h,k[d]()))});return c.isWindow(f)?f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b]:f.nodeType===9?Math.max(f.documentElement["client"+ -b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]):e===A?parseFloat(c.css(f,d)):this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/htmlcov/jquery.hotkeys.js b/htmlcov/jquery.hotkeys.js deleted file mode 100644 index 09b21e03c..000000000 --- a/htmlcov/jquery.hotkeys.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * jQuery Hotkeys Plugin - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Based upon the plugin by Tzury Bar Yochay: - * http://github.com/tzuryby/hotkeys - * - * Original idea by: - * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ -*/ - -(function(jQuery){ - - jQuery.hotkeys = { - version: "0.8", - - specialKeys: { - 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" - }, - - shiftNums: { - "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", - "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", - ".": ">", "/": "?", "\\": "|" - } - }; - - function keyHandler( handleObj ) { - // Only care when a possible input has been specified - if ( typeof handleObj.data !== "string" ) { - return; - } - - var origHandler = handleObj.handler, - keys = handleObj.data.toLowerCase().split(" "); - - handleObj.handler = function( event ) { - // Don't fire in text-accepting inputs that we didn't directly bind to - if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || - event.target.type === "text") ) { - return; - } - - // Keypress represents characters, not special keys - var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], - character = String.fromCharCode( event.which ).toLowerCase(), - key, modif = "", possible = {}; - - // check combinations (alt|ctrl|shift+anything) - if ( event.altKey && special !== "alt" ) { - modif += "alt+"; - } - - if ( event.ctrlKey && special !== "ctrl" ) { - modif += "ctrl+"; - } - - // TODO: Need to make sure this works consistently across platforms - if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { - modif += "meta+"; - } - - if ( event.shiftKey && special !== "shift" ) { - modif += "shift+"; - } - - if ( special ) { - possible[ modif + special ] = true; - - } else { - possible[ modif + character ] = true; - possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; - - // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" - if ( modif === "shift+" ) { - possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; - } - } - - for ( var i = 0, l = keys.length; i < l; i++ ) { - if ( possible[ keys[i] ] ) { - return origHandler.apply( this, arguments ); - } - } - }; - } - - jQuery.each([ "keydown", "keyup", "keypress" ], function() { - jQuery.event.special[ this ] = { add: keyHandler }; - }); - -})( jQuery ); diff --git a/htmlcov/jquery.isonscreen.js b/htmlcov/jquery.isonscreen.js deleted file mode 100644 index 0182ebd21..000000000 --- a/htmlcov/jquery.isonscreen.js +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright (c) 2010 - * @author Laurence Wheway - * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) - * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. - * - * @version 1.2.0 - */ -(function($) { - jQuery.extend({ - isOnScreen: function(box, container) { - //ensure numbers come in as intgers (not strings) and remove 'px' is it's there - for(var i in box){box[i] = parseFloat(box[i])}; - for(var i in container){container[i] = parseFloat(container[i])}; - - if(!container){ - container = { - left: $(window).scrollLeft(), - top: $(window).scrollTop(), - width: $(window).width(), - height: $(window).height() - } - } - - if( box.left+box.width-container.left > 0 && - box.left < container.width+container.left && - box.top+box.height-container.top > 0 && - box.top < container.height+container.top - ) return true; - return false; - } - }) - - - jQuery.fn.isOnScreen = function (container) { - for(var i in container){container[i] = parseFloat(container[i])}; - - if(!container){ - container = { - left: $(window).scrollLeft(), - top: $(window).scrollTop(), - width: $(window).width(), - height: $(window).height() - } - } - - if( $(this).offset().left+$(this).width()-container.left > 0 && - $(this).offset().left < container.width+container.left && - $(this).offset().top+$(this).height()-container.top > 0 && - $(this).offset().top < container.height+container.top - ) return true; - return false; - } -})(jQuery); diff --git a/htmlcov/jquery.tablesorter.min.js b/htmlcov/jquery.tablesorter.min.js deleted file mode 100644 index 64c700712..000000000 --- a/htmlcov/jquery.tablesorter.min.js +++ /dev/null @@ -1,2 +0,0 @@ - -(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('
').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;ib)?1:0));};function sortTextDesc(a,b){return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;iD6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9@bl*u7(4JIy_w*Lo808 zo$Afkpupp@{Fv_bobxQ#pD>iB3oNa1d9=pM`D99*FvsH{pKJfpB1-4UD;=6}F=+gKX>Gx9b=!>PY1_pdfo@{(boFyt=akR{ E04sl8JOBUy diff --git a/htmlcov/keybd_open.png b/htmlcov/keybd_open.png deleted file mode 100644 index a77961db5424cfff43a63d399972ee85fc0dfdb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^%0SG+!3HE>D6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9%kc-1xY}mZci7-5R21$ zCp+>TR^VYdE*ieC^FGV{Cyeh_21=Rotz3KNq=!VmdK II;Vst00jnQH~;_u diff --git a/htmlcov/rest_framework___init__.html b/htmlcov/rest_framework___init__.html deleted file mode 100644 index 9cb0c53ac..000000000 --- a/htmlcov/rest_framework___init__.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - Coverage for rest_framework/__init__: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
-
- - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

- -
-

__version__ = '2.3.5' 

-

 

-

VERSION = __version__  # synonym 

-

 

-

# Header encoding (see RFC5987) 

-

HTTP_HEADER_ENCODING = 'iso-8859-1' 

-

 

-

# Default datetime input and output formats 

-

ISO_8601 = 'iso-8601' 

- -
- - - - - - diff --git a/htmlcov/rest_framework_authentication.html b/htmlcov/rest_framework_authentication.html deleted file mode 100644 index 899d06777..000000000 --- a/htmlcov/rest_framework_authentication.html +++ /dev/null @@ -1,767 +0,0 @@ - - - - - - - - Coverage for rest_framework/authentication: 80% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

- -
-

""" 

-

Provides various authentication policies. 

-

""" 

-

from __future__ import unicode_literals 

-

import base64 

-

from datetime import datetime 

-

 

-

from django.contrib.auth import authenticate 

-

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 

-

from rest_framework.authtoken.models import Token 

-

 

-

 

-

def get_authorization_header(request): 

-

    """ 

-

    Return request's 'Authorization:' header, as a bytestring. 

-

 

-

    Hide some test client ickyness where the header can be unicode. 

-

    """ 

-

    auth = request.META.get('HTTP_AUTHORIZATION', b'') 

-

    if type(auth) == type(''): 

-

        # Work around django test client oddness 

-

        auth = auth.encode(HTTP_HEADER_ENCODING) 

-

    return auth 

-

 

-

 

-

class BaseAuthentication(object): 

-

    """ 

-

    All authentication classes should extend BaseAuthentication. 

-

    """ 

-

 

-

    def authenticate(self, request): 

-

        """ 

-

        Authenticate the request and return a two-tuple of (user, token). 

-

        """ 

-

        raise NotImplementedError(".authenticate() must be overridden.") 

-

 

-

    def authenticate_header(self, request): 

-

        """ 

-

        Return a string to be used as the value of the `WWW-Authenticate` 

-

        header in a `401 Unauthenticated` response, or `None` if the 

-

        authentication scheme should return `403 Permission Denied` responses. 

-

        """ 

-

        pass 

-

 

-

 

-

class BasicAuthentication(BaseAuthentication): 

-

    """ 

-

    HTTP Basic authentication against username/password. 

-

    """ 

-

    www_authenticate_realm = 'api' 

-

 

-

    def authenticate(self, request): 

-

        """ 

-

        Returns a `User` if a correct username and password have been supplied 

-

        using HTTP Basic authentication.  Otherwise returns `None`. 

-

        """ 

-

        auth = get_authorization_header(request).split() 

-

 

-

        if not auth or auth[0].lower() != b'basic': 

-

            return None 

-

 

-

        if len(auth) == 1: 

-

            msg = 'Invalid basic header. No credentials provided.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

        elif len(auth) > 2: 

-

            msg = 'Invalid basic header. Credentials string should not contain spaces.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        try: 

-

            auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') 

-

        except (TypeError, UnicodeDecodeError): 

-

            msg = 'Invalid basic header. Credentials not correctly base64 encoded' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        userid, password = auth_parts[0], auth_parts[2] 

-

        return self.authenticate_credentials(userid, password) 

-

 

-

    def authenticate_credentials(self, userid, password): 

-

        """ 

-

        Authenticate the userid and password against username and password. 

-

        """ 

-

        user = authenticate(username=userid, password=password) 

-

        if user is None or not user.is_active: 

-

            raise exceptions.AuthenticationFailed('Invalid username/password') 

-

        return (user, None) 

-

 

-

    def authenticate_header(self, request): 

-

        return 'Basic realm="%s"' % self.www_authenticate_realm 

-

 

-

 

-

class SessionAuthentication(BaseAuthentication): 

-

    """ 

-

    Use Django's session framework for authentication. 

-

    """ 

-

 

-

    def authenticate(self, request): 

-

        """ 

-

        Returns a `User` if the request session currently has a logged in user. 

-

        Otherwise returns `None`. 

-

        """ 

-

 

-

        # Get the underlying HttpRequest object 

-

        http_request = request._request 

-

        user = getattr(http_request, 'user', None) 

-

 

-

        # Unauthenticated, CSRF validation not required 

-

        if not user or not user.is_active: 

-

            return None 

-

 

-

        # Enforce CSRF validation for session based authentication. 

-

        class CSRFCheck(CsrfViewMiddleware): 

-

            def _reject(self, request, reason): 

-

                # Return the failure reason instead of an HttpResponse 

-

                return reason 

-

 

-

        reason = CSRFCheck().process_view(http_request, None, (), {}) 

-

        if reason: 

-

            # CSRF failed, bail with explicit error message 

-

            raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) 

-

 

-

        # CSRF passed with authenticated user 

-

        return (user, None) 

-

 

-

 

-

class TokenAuthentication(BaseAuthentication): 

-

    """ 

-

    Simple token based authentication. 

-

 

-

    Clients should authenticate by passing the token key in the "Authorization" 

-

    HTTP header, prepended with the string "Token ".  For example: 

-

 

-

        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a 

-

    """ 

-

 

-

    model = Token 

-

    """ 

-

    A custom token model may be used, but must have the following properties. 

-

 

-

    * key -- The string identifying the token 

-

    * user -- The user to which the token belongs 

-

    """ 

-

 

-

    def authenticate(self, request): 

-

        auth = get_authorization_header(request).split() 

-

 

-

        if not auth or auth[0].lower() != b'token': 

-

            return None 

-

 

-

        if len(auth) == 1: 

-

            msg = 'Invalid token header. No credentials provided.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

        elif len(auth) > 2: 

-

            msg = 'Invalid token header. Token string should not contain spaces.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        return self.authenticate_credentials(auth[1]) 

-

 

-

    def authenticate_credentials(self, key): 

-

        try: 

-

            token = self.model.objects.get(key=key) 

-

        except self.model.DoesNotExist: 

-

            raise exceptions.AuthenticationFailed('Invalid token') 

-

 

-

        if not token.user.is_active: 

-

            raise exceptions.AuthenticationFailed('User inactive or deleted') 

-

 

-

        return (token.user, token) 

-

 

-

    def authenticate_header(self, request): 

-

        return 'Token' 

-

 

-

 

-

class OAuthAuthentication(BaseAuthentication): 

-

    """ 

-

    OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`. 

-

 

-

    Note: The `oauth2` package actually provides oauth1.0a support.  Urg. 

-

          We import it from the `compat` module as `oauth`. 

-

    """ 

-

    www_authenticate_realm = 'api' 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        super(OAuthAuthentication, self).__init__(*args, **kwargs) 

-

 

-

        if oauth is None: 

-

            raise ImproperlyConfigured( 

-

                "The 'oauth2' package could not be imported." 

-

                "It is required for use with the 'OAuthAuthentication' class.") 

-

 

-

        if oauth_provider is None: 

-

            raise ImproperlyConfigured( 

-

                "The 'django-oauth-plus' package could not be imported." 

-

                "It is required for use with the 'OAuthAuthentication' class.") 

-

 

-

    def authenticate(self, request): 

-

        """ 

-

        Returns two-tuple of (user, token) if authentication succeeds, 

-

        or None otherwise. 

-

        """ 

-

        try: 

-

            oauth_request = oauth_provider.utils.get_oauth_request(request) 

-

        except oauth.Error as err: 

-

            raise exceptions.AuthenticationFailed(err.message) 

-

 

-

        if not oauth_request: 

-

            return None 

-

 

-

        oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES 

-

 

-

        found = any(param for param in oauth_params if param in oauth_request) 

-

        missing = list(param for param in oauth_params if param not in oauth_request) 

-

 

-

        if not found: 

-

            # OAuth authentication was not attempted. 

-

            return None 

-

 

-

        if missing: 

-

            # OAuth was attempted but missing parameters. 

-

            msg = 'Missing parameters: %s' % (', '.join(missing)) 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        if not self.check_nonce(request, oauth_request): 

-

            msg = 'Nonce check failed' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        try: 

-

            consumer_key = oauth_request.get_parameter('oauth_consumer_key') 

-

            consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key) 

-

        except oauth_provider.store.InvalidConsumerError: 

-

            msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key') 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        if consumer.status != oauth_provider.consts.ACCEPTED: 

-

            msg = 'Invalid consumer key status: %s' % consumer.get_status_display() 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        try: 

-

            token_param = oauth_request.get_parameter('oauth_token') 

-

            token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param) 

-

        except oauth_provider.store.InvalidTokenError: 

-

            msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token') 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        try: 

-

            self.validate_token(request, consumer, token) 

-

        except oauth.Error as err: 

-

            raise exceptions.AuthenticationFailed(err.message) 

-

 

-

        user = token.user 

-

 

-

        if not user.is_active: 

-

            msg = 'User inactive or deleted: %s' % user.username 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        return (token.user, token) 

-

 

-

    def authenticate_header(self, request): 

-

        """ 

-

        If permission is denied, return a '401 Unauthorized' response, 

-

        with an appropraite 'WWW-Authenticate' header. 

-

        """ 

-

        return 'OAuth realm="%s"' % self.www_authenticate_realm 

-

 

-

    def validate_token(self, request, consumer, token): 

-

        """ 

-

        Check the token and raise an `oauth.Error` exception if invalid. 

-

        """ 

-

        oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request) 

-

        oauth_server.verify_request(oauth_request, consumer, token) 

-

 

-

    def check_nonce(self, request, oauth_request): 

-

        """ 

-

        Checks nonce of request, and return True if valid. 

-

        """ 

-

        return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce']) 

-

 

-

 

-

class OAuth2Authentication(BaseAuthentication): 

-

    """ 

-

    OAuth 2 authentication backend using `django-oauth2-provider` 

-

    """ 

-

    www_authenticate_realm = 'api' 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        super(OAuth2Authentication, self).__init__(*args, **kwargs) 

-

 

-

        if oauth2_provider is None: 

-

            raise ImproperlyConfigured( 

-

                "The 'django-oauth2-provider' package could not be imported. " 

-

                "It is required for use with the 'OAuth2Authentication' class.") 

-

 

-

    def authenticate(self, request): 

-

        """ 

-

        Returns two-tuple of (user, token) if authentication succeeds, 

-

        or None otherwise. 

-

        """ 

-

 

-

        auth = get_authorization_header(request).split() 

-

 

-

        if not auth or auth[0].lower() != b'bearer': 

-

            return None 

-

 

-

        if len(auth) == 1: 

-

            msg = 'Invalid bearer header. No credentials provided.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

        elif len(auth) > 2: 

-

            msg = 'Invalid bearer header. Token string should not contain spaces.' 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        return self.authenticate_credentials(request, auth[1]) 

-

 

-

    def authenticate_credentials(self, request, access_token): 

-

        """ 

-

        Authenticate the request, given the access token. 

-

        """ 

-

 

-

        try: 

-

            token = oauth2_provider.models.AccessToken.objects.select_related('user') 

-

            # TODO: Change to timezone aware datetime when oauth2_provider add 

-

            # support to it. 

-

            token = token.get(token=access_token, expires__gt=datetime.now()) 

-

        except oauth2_provider.models.AccessToken.DoesNotExist: 

-

            raise exceptions.AuthenticationFailed('Invalid token') 

-

 

-

        user = token.user 

-

 

-

        if not user.is_active: 

-

            msg = 'User inactive or deleted: %s' % user.username 

-

            raise exceptions.AuthenticationFailed(msg) 

-

 

-

        return (user, token) 

-

 

-

    def authenticate_header(self, request): 

-

        """ 

-

        Bearer is the only finalized type currently 

-

 

-

        Check details on the `OAuth2Authentication.authenticate` method 

-

        """ 

-

        return 'Bearer realm="%s"' % self.www_authenticate_realm 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_authtoken___init__.html b/htmlcov/rest_framework_authtoken___init__.html deleted file mode 100644 index f72574937..000000000 --- a/htmlcov/rest_framework_authtoken___init__.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - Coverage for rest_framework/authtoken/__init__: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
- - - -
-
- - - - - diff --git a/htmlcov/rest_framework_authtoken_models.html b/htmlcov/rest_framework_authtoken_models.html deleted file mode 100644 index 27d2fff1d..000000000 --- a/htmlcov/rest_framework_authtoken_models.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - Coverage for rest_framework/authtoken/models: 95% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

- -
-

import uuid 

-

import hmac 

-

from hashlib import sha1 

-

from rest_framework.compat import User 

-

from django.conf import settings 

-

from django.db import models 

-

 

-

 

-

class Token(models.Model): 

-

    """ 

-

    The default authorization token model. 

-

    """ 

-

    key = models.CharField(max_length=40, primary_key=True) 

-

    user = models.OneToOneField(User, related_name='auth_token') 

-

    created = models.DateTimeField(auto_now_add=True) 

-

 

-

    class Meta: 

-

        # Work around for a bug in Django: 

-

        # https://code.djangoproject.com/ticket/19422 

-

        # 

-

        # Also see corresponding ticket: 

-

        # https://github.com/tomchristie/django-rest-framework/issues/705 

-

        abstract = 'rest_framework.authtoken' not in settings.INSTALLED_APPS 

-

 

-

    def save(self, *args, **kwargs): 

-

        if not self.key: 

-

            self.key = self.generate_key() 

-

        return super(Token, self).save(*args, **kwargs) 

-

 

-

    def generate_key(self): 

-

        unique = uuid.uuid4() 

-

        return hmac.new(unique.bytes, digestmod=sha1).hexdigest() 

-

 

-

    def __unicode__(self): 

-

        return self.key 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_authtoken_serializers.html b/htmlcov/rest_framework_authtoken_serializers.html deleted file mode 100644 index 8997d9a7b..000000000 --- a/htmlcov/rest_framework_authtoken_serializers.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - Coverage for rest_framework/authtoken/serializers: 88% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

- -
-

from django.contrib.auth import authenticate 

-

from rest_framework import serializers 

-

 

-

 

-

class AuthTokenSerializer(serializers.Serializer): 

-

    username = serializers.CharField() 

-

    password = serializers.CharField() 

-

 

-

    def validate(self, attrs): 

-

        username = attrs.get('username') 

-

        password = attrs.get('password') 

-

 

-

        if username and password: 

-

            user = authenticate(username=username, password=password) 

-

 

-

            if user: 

-

                if not user.is_active: 

-

                    raise serializers.ValidationError('User account is disabled.') 

-

                attrs['user'] = user 

-

                return attrs 

-

            else: 

-

                raise serializers.ValidationError('Unable to login with provided credentials.') 

-

        else: 

-

            raise serializers.ValidationError('Must include "username" and "password"') 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_authtoken_views.html b/htmlcov/rest_framework_authtoken_views.html deleted file mode 100644 index d13746ea8..000000000 --- a/htmlcov/rest_framework_authtoken_views.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - Coverage for rest_framework/authtoken/views: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

- -
-

from rest_framework.views import APIView 

-

from rest_framework import status 

-

from rest_framework import parsers 

-

from rest_framework import renderers 

-

from rest_framework.response import Response 

-

from rest_framework.authtoken.models import Token 

-

from rest_framework.authtoken.serializers import AuthTokenSerializer 

-

 

-

 

-

class ObtainAuthToken(APIView): 

-

    throttle_classes = () 

-

    permission_classes = () 

-

    parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) 

-

    renderer_classes = (renderers.JSONRenderer,) 

-

    serializer_class = AuthTokenSerializer 

-

    model = Token 

-

 

-

    def post(self, request): 

-

        serializer = self.serializer_class(data=request.DATA) 

-

        if serializer.is_valid(): 

-

            token, created = Token.objects.get_or_create(user=serializer.object['user']) 

-

            return Response({'token': token.key}) 

-

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

-

 

-

 

-

obtain_auth_token = ObtainAuthToken.as_view() 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_decorators.html b/htmlcov/rest_framework_decorators.html deleted file mode 100644 index 6ad6f6b51..000000000 --- a/htmlcov/rest_framework_decorators.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - Coverage for rest_framework/decorators: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

- -
-

""" 

-

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 

-

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 

-

 

-

 

-

def api_view(http_method_names): 

-

 

-

    """ 

-

    Decorator that converts a function-based view into an APIView subclass. 

-

    Takes a list of allowed methods for the view as an argument. 

-

    """ 

-

 

-

    def decorator(func): 

-

 

-

        WrappedAPIView = type( 

-

            six.PY3 and 'WrappedAPIView' or b'WrappedAPIView', 

-

            (APIView,), 

-

            {'__doc__': func.__doc__} 

-

        ) 

-

 

-

        # Note, the above allows us to set the docstring. 

-

        # It is the equivalent of: 

-

        # 

-

        #     class WrappedAPIView(APIView): 

-

        #         pass 

-

        #     WrappedAPIView.__doc__ = func.doc    <--- Not possible to do this 

-

 

-

        # api_view applied without (method_names) 

-

        assert not(isinstance(http_method_names, types.FunctionType)), \ 

-

            '@api_view missing list of allowed HTTP methods' 

-

 

-

        # api_view applied with eg. string instead of list of strings 

-

        assert isinstance(http_method_names, (list, tuple)), \ 

-

            '@api_view expected a list of strings, received %s' % type(http_method_names).__name__ 

-

 

-

        allowed_methods = set(http_method_names) | set(('options',)) 

-

        WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] 

-

 

-

        def handler(self, *args, **kwargs): 

-

            return func(*args, **kwargs) 

-

 

-

        for method in http_method_names: 

-

            setattr(WrappedAPIView, method.lower(), handler) 

-

 

-

        WrappedAPIView.__name__ = func.__name__ 

-

 

-

        WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes', 

-

                                                  APIView.renderer_classes) 

-

 

-

        WrappedAPIView.parser_classes = getattr(func, 'parser_classes', 

-

                                                APIView.parser_classes) 

-

 

-

        WrappedAPIView.authentication_classes = getattr(func, 'authentication_classes', 

-

                                                        APIView.authentication_classes) 

-

 

-

        WrappedAPIView.throttle_classes = getattr(func, 'throttle_classes', 

-

                                                  APIView.throttle_classes) 

-

 

-

        WrappedAPIView.permission_classes = getattr(func, 'permission_classes', 

-

                                                    APIView.permission_classes) 

-

 

-

        return WrappedAPIView.as_view() 

-

    return decorator 

-

 

-

 

-

def renderer_classes(renderer_classes): 

-

    def decorator(func): 

-

        func.renderer_classes = renderer_classes 

-

        return func 

-

    return decorator 

-

 

-

 

-

def parser_classes(parser_classes): 

-

    def decorator(func): 

-

        func.parser_classes = parser_classes 

-

        return func 

-

    return decorator 

-

 

-

 

-

def authentication_classes(authentication_classes): 

-

    def decorator(func): 

-

        func.authentication_classes = authentication_classes 

-

        return func 

-

    return decorator 

-

 

-

 

-

def throttle_classes(throttle_classes): 

-

    def decorator(func): 

-

        func.throttle_classes = throttle_classes 

-

        return func 

-

    return decorator 

-

 

-

 

-

def permission_classes(permission_classes): 

-

    def decorator(func): 

-

        func.permission_classes = permission_classes 

-

        return func 

-

    return decorator 

-

 

-

 

-

def 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.kwargs = kwargs 

-

        return func 

-

    return decorator 

-

 

-

 

-

def 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.kwargs = kwargs 

-

        return func 

-

    return decorator 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_exceptions.html b/htmlcov/rest_framework_exceptions.html deleted file mode 100644 index d975a8481..000000000 --- a/htmlcov/rest_framework_exceptions.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - Coverage for rest_framework/exceptions: 96% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

- -
-

""" 

-

Handled exceptions raised by REST framework. 

-

 

-

In addition Django's built in 403 and 404 exceptions are handled. 

-

(`django.http.Http404` and `django.core.exceptions.PermissionDenied`) 

-

""" 

-

from __future__ import unicode_literals 

-

from rest_framework import status 

-

 

-

 

-

class APIException(Exception): 

-

    """ 

-

    Base class for REST framework exceptions. 

-

    Subclasses should provide `.status_code` and `.detail` properties. 

-

    """ 

-

    pass 

-

 

-

 

-

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 

-

 

-

 

-

class NotAcceptable(APIException): 

-

    status_code = status.HTTP_406_NOT_ACCEPTABLE 

-

    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 

-

        self.available_renderers = available_renderers 

-

 

-

 

-

class UnsupportedMediaType(APIException): 

-

    status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE 

-

    default_detail = "Unsupported media type '%s' in request." 

-

 

-

    def __init__(self, media_type, detail=None): 

-

        self.detail = (detail or self.default_detail) % media_type 

-

 

-

 

-

class Throttled(APIException): 

-

    status_code = status.HTTP_429_TOO_MANY_REQUESTS 

-

    default_detail = "Request was throttled." 

-

    extra_detail = "Expected available in %d second%s." 

-

 

-

    def __init__(self, wait=None, detail=None): 

-

        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 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_fields.html b/htmlcov/rest_framework_fields.html deleted file mode 100644 index cf2731d25..000000000 --- a/htmlcov/rest_framework_fields.html +++ /dev/null @@ -1,1991 +0,0 @@ - - - - - - - - Coverage for rest_framework/fields: 87% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

-

574

-

575

-

576

-

577

-

578

-

579

-

580

-

581

-

582

-

583

-

584

-

585

-

586

-

587

-

588

-

589

-

590

-

591

-

592

-

593

-

594

-

595

-

596

-

597

-

598

-

599

-

600

-

601

-

602

-

603

-

604

-

605

-

606

-

607

-

608

-

609

-

610

-

611

-

612

-

613

-

614

-

615

-

616

-

617

-

618

-

619

-

620

-

621

-

622

-

623

-

624

-

625

-

626

-

627

-

628

-

629

-

630

-

631

-

632

-

633

-

634

-

635

-

636

-

637

-

638

-

639

-

640

-

641

-

642

-

643

-

644

-

645

-

646

-

647

-

648

-

649

-

650

-

651

-

652

-

653

-

654

-

655

-

656

-

657

-

658

-

659

-

660

-

661

-

662

-

663

-

664

-

665

-

666

-

667

-

668

-

669

-

670

-

671

-

672

-

673

-

674

-

675

-

676

-

677

-

678

-

679

-

680

-

681

-

682

-

683

-

684

-

685

-

686

-

687

-

688

-

689

-

690

-

691

-

692

-

693

-

694

-

695

-

696

-

697

-

698

-

699

-

700

-

701

-

702

-

703

-

704

-

705

-

706

-

707

-

708

-

709

-

710

-

711

-

712

-

713

-

714

-

715

-

716

-

717

-

718

-

719

-

720

-

721

-

722

-

723

-

724

-

725

-

726

-

727

-

728

-

729

-

730

-

731

-

732

-

733

-

734

-

735

-

736

-

737

-

738

-

739

-

740

-

741

-

742

-

743

-

744

-

745

-

746

-

747

-

748

-

749

-

750

-

751

-

752

-

753

-

754

-

755

-

756

-

757

-

758

-

759

-

760

-

761

-

762

-

763

-

764

-

765

-

766

-

767

-

768

-

769

-

770

-

771

-

772

-

773

-

774

-

775

-

776

-

777

-

778

-

779

-

780

-

781

-

782

-

783

-

784

-

785

-

786

-

787

-

788

-

789

-

790

-

791

-

792

-

793

-

794

-

795

-

796

-

797

-

798

-

799

-

800

-

801

-

802

-

803

-

804

-

805

-

806

-

807

-

808

-

809

-

810

-

811

-

812

-

813

-

814

-

815

-

816

-

817

-

818

-

819

-

820

-

821

-

822

-

823

-

824

-

825

-

826

-

827

-

828

-

829

-

830

-

831

-

832

-

833

-

834

-

835

-

836

-

837

-

838

-

839

-

840

-

841

-

842

-

843

-

844

-

845

-

846

-

847

-

848

-

849

-

850

-

851

-

852

-

853

-

854

-

855

-

856

-

857

-

858

-

859

-

860

-

861

-

862

-

863

-

864

-

865

-

866

-

867

-

868

-

869

-

870

-

871

-

872

-

873

-

874

-

875

-

876

-

877

-

878

-

879

-

880

-

881

-

882

-

883

-

884

-

885

-

886

-

887

-

888

-

889

-

890

-

891

-

892

-

893

-

894

-

895

-

896

-

897

-

898

-

899

-

900

-

901

-

902

-

903

-

904

-

905

-

906

-

907

-

908

-

909

-

910

-

911

-

912

-

913

-

914

-

915

-

916

-

917

-

918

-

919

-

920

-

921

-

922

-

923

-

924

-

925

-

926

-

927

-

928

-

929

-

930

-

931

-

932

-

933

-

934

-

935

-

936

-

937

-

938

-

939

-

940

-

941

-

942

-

943

-

944

-

945

-

946

-

947

-

948

-

949

-

950

-

951

-

952

-

953

-

954

-

955

- -
-

""" 

-

Serializer fields perform validation on incoming data. 

-

 

-

They are very similar to Django's form fields. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

import copy 

-

import datetime 

-

import inspect 

-

import re 

-

import warnings 

-

from decimal import Decimal, DecimalException 

-

from django import forms 

-

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.forms import widgets 

-

from django.utils.encoding import is_protected_type 

-

from django.utils.translation import ugettext_lazy as _ 

-

from django.utils.datastructures import SortedDict 

-

from rest_framework import ISO_8601 

-

from rest_framework.compat import ( 

-

    timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text, 

-

    force_text, is_non_str_iterable 

-

) 

-

from rest_framework.settings import api_settings 

-

 

-

 

-

def is_simple_callable(obj): 

-

    """ 

-

    True if the object is a callable that takes no arguments. 

-

    """ 

-

    function = inspect.isfunction(obj) 

-

    method = inspect.ismethod(obj) 

-

 

-

    if not (function or method): 

-

        return False 

-

 

-

    args, _, _, defaults = inspect.getargspec(obj) 

-

    len_args = len(args) if function else len(args) - 1 

-

    len_defaults = len(defaults) if defaults else 0 

-

    return len_args <= len_defaults 

-

 

-

 

-

def get_component(obj, attr_name): 

-

    """ 

-

    Given an object, and an attribute name, 

-

    return that attribute on the object. 

-

    """ 

-

    if isinstance(obj, dict): 

-

        val = obj.get(attr_name) 

-

    else: 

-

        val = getattr(obj, attr_name) 

-

 

-

    if is_simple_callable(val): 

-

        return val() 

-

    return val 

-

 

-

 

-

def readable_datetime_formats(formats): 

-

    format = ', '.join(formats).replace(ISO_8601, 

-

             'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') 

-

    return humanize_strptime(format) 

-

 

-

 

-

def readable_date_formats(formats): 

-

    format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]') 

-

    return humanize_strptime(format) 

-

 

-

 

-

def readable_time_formats(formats): 

-

    format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') 

-

    return humanize_strptime(format) 

-

 

-

 

-

def humanize_strptime(format_string): 

-

    # Note that we're missing some of the locale specific mappings that 

-

    # don't really make sense. 

-

    mapping = { 

-

        "%Y": "YYYY", 

-

        "%y": "YY", 

-

        "%m": "MM", 

-

        "%b": "[Jan-Dec]", 

-

        "%B": "[January-December]", 

-

        "%d": "DD", 

-

        "%H": "hh", 

-

        "%I": "hh",  # Requires '%p' to differentiate from '%H'. 

-

        "%M": "mm", 

-

        "%S": "ss", 

-

        "%f": "uuuuuu", 

-

        "%a": "[Mon-Sun]", 

-

        "%A": "[Monday-Sunday]", 

-

        "%p": "[AM|PM]", 

-

        "%z": "[+HHMM|-HHMM]" 

-

    } 

-

    for key, val in mapping.items(): 

-

        format_string = format_string.replace(key, val) 

-

    return format_string 

-

 

-

 

-

class Field(object): 

-

    read_only = True 

-

    creation_counter = 0 

-

    empty = '' 

-

    type_name = None 

-

    partial = False 

-

    use_files = False 

-

    form_field_class = forms.CharField 

-

    type_label = 'field' 

-

 

-

    def __init__(self, source=None, label=None, help_text=None): 

-

        self.parent = None 

-

 

-

        self.creation_counter = Field.creation_counter 

-

        Field.creation_counter += 1 

-

 

-

        self.source = source 

-

 

-

        if label is not None: 

-

            self.label = smart_text(label) 

-

 

-

        if help_text is not None: 

-

            self.help_text = smart_text(help_text) 

-

 

-

    def initialize(self, parent, field_name): 

-

        """ 

-

        Called to set up a field prior to field_to_native or field_from_native. 

-

 

-

        parent - The parent serializer. 

-

        model_field - The model field this field corresponds to, if one exists. 

-

        """ 

-

        self.parent = parent 

-

        self.root = parent.root or parent 

-

        self.context = self.root.context 

-

        self.partial = self.root.partial 

-

        if self.partial: 

-

            self.required = False 

-

 

-

    def field_from_native(self, data, files, field_name, into): 

-

        """ 

-

        Given a dictionary and a field name, updates the dictionary `into`, 

-

        with the field and it's deserialized value. 

-

        """ 

-

        return 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        """ 

-

        Given and object and a field name, returns the value that should be 

-

        serialized for that field. 

-

        """ 

-

        if obj is None: 

-

            return self.empty 

-

 

-

        if self.source == '*': 

-

            return self.to_native(obj) 

-

 

-

        source = self.source or field_name 

-

        value = obj 

-

 

-

        for component in source.split('.'): 

-

            value = get_component(value, component) 

-

            if value is None: 

-

                break 

-

 

-

        return self.to_native(value) 

-

 

-

    def to_native(self, value): 

-

        """ 

-

        Converts the field's value into it's simple representation. 

-

        """ 

-

        if is_simple_callable(value): 

-

            value = value() 

-

 

-

        if is_protected_type(value): 

-

            return value 

-

        elif (is_non_str_iterable(value) and 

-

              not isinstance(value, (dict, six.string_types))): 

-

            return [self.to_native(item) for item in value] 

-

        elif isinstance(value, dict): 

-

            # Make sure we preserve field ordering, if it exists 

-

            ret = SortedDict() 

-

            for key, val in value.items(): 

-

                ret[key] = self.to_native(val) 

-

            return ret 

-

        return force_text(value) 

-

 

-

    def attributes(self): 

-

        """ 

-

        Returns a dictionary of attributes to be used when serializing to xml. 

-

        """ 

-

        if self.type_name: 

-

            return {'type': self.type_name} 

-

        return {} 

-

 

-

    def metadata(self): 

-

        metadata = SortedDict() 

-

        metadata['type'] = self.type_label 

-

        metadata['required'] = getattr(self, 'required', False) 

-

        optional_attrs = ['read_only', 'label', 'help_text', 

-

                          'min_length', 'max_length'] 

-

        for attr in optional_attrs: 

-

            value = getattr(self, attr, None) 

-

            if value is not None and value != '': 

-

                metadata[attr] = force_text(value, strings_only=True) 

-

        return metadata 

-

 

-

 

-

class WritableField(Field): 

-

    """ 

-

    Base for read/write fields. 

-

    """ 

-

    default_validators = [] 

-

    default_error_messages = { 

-

        'required': _('This field is required.'), 

-

        'invalid': _('Invalid value.'), 

-

    } 

-

    widget = widgets.TextInput 

-

    default = None 

-

 

-

    def __init__(self, source=None, label=None, help_text=None, 

-

                 read_only=False, required=None, 

-

                 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 

-

        if required is None: 

-

            self.required = not(read_only) 

-

        else: 

-

            assert not (read_only and required), "Cannot set required=True and read_only=True" 

-

            self.required = required 

-

 

-

        messages = {} 

-

        for c in reversed(self.__class__.__mro__): 

-

            messages.update(getattr(c, 'default_error_messages', {})) 

-

        messages.update(error_messages or {}) 

-

        self.error_messages = messages 

-

 

-

        self.validators = self.default_validators + validators 

-

        self.default = default if default is not None else self.default 

-

 

-

        # Widgets are ony used for HTML forms. 

-

        widget = widget or self.widget 

-

        if isinstance(widget, type): 

-

            widget = widget() 

-

        self.widget = widget 

-

 

-

    def __deepcopy__(self, memo): 

-

        result = copy.copy(self) 

-

        memo[id(self)] = result 

-

        result.validators = self.validators[:] 

-

        return result 

-

 

-

    def validate(self, value): 

-

        if value in validators.EMPTY_VALUES and self.required: 

-

            raise ValidationError(self.error_messages['required']) 

-

 

-

    def run_validators(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return 

-

        errors = [] 

-

        for v in self.validators: 

-

            try: 

-

                v(value) 

-

            except ValidationError as e: 

-

                if hasattr(e, 'code') and e.code in self.error_messages: 

-

                    message = self.error_messages[e.code] 

-

                    if e.params: 

-

                        message = message % e.params 

-

                    errors.append(message) 

-

                else: 

-

                    errors.extend(e.messages) 

-

        if errors: 

-

            raise ValidationError(errors) 

-

 

-

    def field_from_native(self, data, files, field_name, into): 

-

        """ 

-

        Given a dictionary and a field name, updates the dictionary `into`, 

-

        with the field and it's deserialized value. 

-

        """ 

-

        if self.read_only: 

-

            return 

-

 

-

        try: 

-

            if self.use_files: 

-

                files = files or {} 

-

                native = files[field_name] 

-

            else: 

-

                native = data[field_name] 

-

        except KeyError: 

-

            if self.default is not None and not self.partial: 

-

                # Note: partial updates shouldn't set defaults 

-

                if is_simple_callable(self.default): 

-

                    native = self.default() 

-

                else: 

-

                    native = self.default 

-

            else: 

-

                if self.required: 

-

                    raise ValidationError(self.error_messages['required']) 

-

                return 

-

 

-

        value = self.from_native(native) 

-

        if self.source == '*': 

-

            if value: 

-

                into.update(value) 

-

        else: 

-

            self.validate(value) 

-

            self.run_validators(value) 

-

            into[self.source or field_name] = value 

-

 

-

    def from_native(self, value): 

-

        """ 

-

        Reverts a simple representation back to the field's value. 

-

        """ 

-

        return value 

-

 

-

 

-

class ModelField(WritableField): 

-

    """ 

-

    A generic field that can be used against an arbitrary model field. 

-

    """ 

-

    def __init__(self, *args, **kwargs): 

-

        try: 

-

            self.model_field = kwargs.pop('model_field') 

-

        except KeyError: 

-

            raise ValueError("ModelField requires 'model_field' kwarg") 

-

 

-

        self.min_length = kwargs.pop('min_length', 

-

                                     getattr(self.model_field, 'min_length', None)) 

-

        self.max_length = kwargs.pop('max_length', 

-

                                     getattr(self.model_field, 'max_length', None)) 

-

        self.min_value = kwargs.pop('min_value', 

-

                                    getattr(self.model_field, 'min_value', None)) 

-

        self.max_value = kwargs.pop('max_value', 

-

                                    getattr(self.model_field, 'max_value', None)) 

-

 

-

        super(ModelField, self).__init__(*args, **kwargs) 

-

 

-

        if self.min_length is not None: 

-

            self.validators.append(validators.MinLengthValidator(self.min_length)) 

-

        if self.max_length is not None: 

-

            self.validators.append(validators.MaxLengthValidator(self.max_length)) 

-

        if self.min_value is not None: 

-

            self.validators.append(validators.MinValueValidator(self.min_value)) 

-

        if self.max_value is not None: 

-

            self.validators.append(validators.MaxValueValidator(self.max_value)) 

-

 

-

    def from_native(self, value): 

-

        rel = getattr(self.model_field, "rel", None) 

-

        if rel is not None: 

-

            return rel.to._meta.get_field(rel.field_name).to_python(value) 

-

        else: 

-

            return self.model_field.to_python(value) 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        value = self.model_field._get_val_from_obj(obj) 

-

        if is_protected_type(value): 

-

            return value 

-

        return self.model_field.value_to_string(obj) 

-

 

-

    def attributes(self): 

-

        return { 

-

            "type": self.model_field.get_internal_type() 

-

        } 

-

 

-

 

-

##### Typed Fields ##### 

-

 

-

class BooleanField(WritableField): 

-

    type_name = 'BooleanField' 

-

    type_label = 'boolean' 

-

    form_field_class = forms.BooleanField 

-

    widget = widgets.CheckboxInput 

-

    default_error_messages = { 

-

        'invalid': _("'%s' value must be either True or False."), 

-

    } 

-

    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 from_native(self, value): 

-

        if value in ('true', 't', 'True', '1'): 

-

            return True 

-

        if value in ('false', 'f', 'False', '0'): 

-

            return False 

-

        return bool(value) 

-

 

-

 

-

class CharField(WritableField): 

-

    type_name = 'CharField' 

-

    type_label = 'string' 

-

    form_field_class = forms.CharField 

-

 

-

    def __init__(self, max_length=None, min_length=None, *args, **kwargs): 

-

        self.max_length, self.min_length = max_length, min_length 

-

        super(CharField, self).__init__(*args, **kwargs) 

-

        if min_length is not None: 

-

            self.validators.append(validators.MinLengthValidator(min_length)) 

-

        if max_length is not None: 

-

            self.validators.append(validators.MaxLengthValidator(max_length)) 

-

 

-

    def from_native(self, value): 

-

        if isinstance(value, six.string_types) or value is None: 

-

            return value 

-

        return smart_text(value) 

-

 

-

 

-

class URLField(CharField): 

-

    type_name = 'URLField' 

-

    type_label = 'url' 

-

 

-

    def __init__(self, **kwargs): 

-

        kwargs['validators'] = [validators.URLValidator()] 

-

        super(URLField, self).__init__(**kwargs) 

-

 

-

 

-

class SlugField(CharField): 

-

    type_name = 'SlugField' 

-

    type_label = 'slug' 

-

    form_field_class = forms.SlugField 

-

 

-

    default_error_messages = { 

-

        'invalid': _("Enter a valid 'slug' consisting of letters, numbers," 

-

                     " underscores or hyphens."), 

-

    } 

-

    default_validators = [validators.validate_slug] 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        super(SlugField, self).__init__(*args, **kwargs) 

-

 

-

 

-

class ChoiceField(WritableField): 

-

    type_name = 'ChoiceField' 

-

    type_label = 'multiple choice' 

-

    form_field_class = forms.ChoiceField 

-

    widget = widgets.Select 

-

    default_error_messages = { 

-

        'invalid_choice': _('Select a valid choice. %(value)s is not one of ' 

-

                            'the available choices.'), 

-

    } 

-

 

-

    def __init__(self, choices=(), *args, **kwargs): 

-

        super(ChoiceField, self).__init__(*args, **kwargs) 

-

        self.choices = choices 

-

        if not self.required: 

-

            self.choices = BLANK_CHOICE_DASH + self.choices 

-

 

-

    def _get_choices(self): 

-

        return self._choices 

-

 

-

    def _set_choices(self, value): 

-

        # Setting choices also sets the choices on the widget. 

-

        # choices can be any iterable, but we call list() on it because 

-

        # it will be consumed more than once. 

-

        self._choices = self.widget.choices = list(value) 

-

 

-

    choices = property(_get_choices, _set_choices) 

-

 

-

    def validate(self, value): 

-

        """ 

-

        Validates that the input is in self.choices. 

-

        """ 

-

        super(ChoiceField, self).validate(value) 

-

        if value and not self.valid_value(value): 

-

            raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) 

-

 

-

    def valid_value(self, value): 

-

        """ 

-

        Check to see if the provided value is a valid choice. 

-

        """ 

-

        for k, v in self.choices: 

-

            if isinstance(v, (list, tuple)): 

-

                # This is an optgroup, so look inside the group for options 

-

                for k2, v2 in v: 

-

                    if value == smart_text(k2): 

-

                        return True 

-

            else: 

-

                if value == smart_text(k) or value == k: 

-

                    return True 

-

        return False 

-

 

-

 

-

class EmailField(CharField): 

-

    type_name = 'EmailField' 

-

    type_label = 'email' 

-

    form_field_class = forms.EmailField 

-

 

-

    default_error_messages = { 

-

        'invalid': _('Enter a valid e-mail address.'), 

-

    } 

-

    default_validators = [validators.validate_email] 

-

 

-

    def from_native(self, value): 

-

        ret = super(EmailField, self).from_native(value) 

-

        if ret is None: 

-

            return None 

-

        return ret.strip() 

-

 

-

 

-

class RegexField(CharField): 

-

    type_name = 'RegexField' 

-

    type_label = 'regex' 

-

    form_field_class = forms.RegexField 

-

 

-

    def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): 

-

        super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) 

-

        self.regex = regex 

-

 

-

    def _get_regex(self): 

-

        return self._regex 

-

 

-

    def _set_regex(self, regex): 

-

        if isinstance(regex, six.string_types): 

-

            regex = re.compile(regex) 

-

        self._regex = regex 

-

        if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: 

-

            self.validators.remove(self._regex_validator) 

-

        self._regex_validator = validators.RegexValidator(regex=regex) 

-

        self.validators.append(self._regex_validator) 

-

 

-

    regex = property(_get_regex, _set_regex) 

-

 

-

 

-

class DateField(WritableField): 

-

    type_name = 'DateField' 

-

    type_label = 'date' 

-

    widget = widgets.DateInput 

-

    form_field_class = forms.DateField 

-

 

-

    default_error_messages = { 

-

        'invalid': _("Date has wrong format. Use one of these formats instead: %s"), 

-

    } 

-

    empty = None 

-

    input_formats = api_settings.DATE_INPUT_FORMATS 

-

    format = api_settings.DATE_FORMAT 

-

 

-

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

-

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

-

        self.format = format if format is not None else self.format 

-

        super(DateField, self).__init__(*args, **kwargs) 

-

 

-

    def from_native(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        if isinstance(value, datetime.datetime): 

-

            if timezone and settings.USE_TZ and timezone.is_aware(value): 

-

                # Convert aware datetimes to the default time zone 

-

                # before casting them to dates (#17742). 

-

                default_timezone = timezone.get_default_timezone() 

-

                value = timezone.make_naive(value, default_timezone) 

-

            return value.date() 

-

        if isinstance(value, datetime.date): 

-

            return value 

-

 

-

        for format in self.input_formats: 

-

            if format.lower() == ISO_8601: 

-

                try: 

-

                    parsed = parse_date(value) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    if parsed is not None: 

-

                        return parsed 

-

            else: 

-

                try: 

-

                    parsed = datetime.datetime.strptime(value, format) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    return parsed.date() 

-

 

-

        msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats) 

-

        raise ValidationError(msg) 

-

 

-

    def to_native(self, value): 

-

        if value is None or self.format is None: 

-

            return value 

-

 

-

        if isinstance(value, datetime.datetime): 

-

            value = value.date() 

-

 

-

        if self.format.lower() == ISO_8601: 

-

            return value.isoformat() 

-

        return value.strftime(self.format) 

-

 

-

 

-

class DateTimeField(WritableField): 

-

    type_name = 'DateTimeField' 

-

    type_label = 'datetime' 

-

    widget = widgets.DateTimeInput 

-

    form_field_class = forms.DateTimeField 

-

 

-

    default_error_messages = { 

-

        'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), 

-

    } 

-

    empty = None 

-

    input_formats = api_settings.DATETIME_INPUT_FORMATS 

-

    format = api_settings.DATETIME_FORMAT 

-

 

-

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

-

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

-

        self.format = format if format is not None else self.format 

-

        super(DateTimeField, self).__init__(*args, **kwargs) 

-

 

-

    def from_native(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        if isinstance(value, datetime.datetime): 

-

            return value 

-

        if isinstance(value, datetime.date): 

-

            value = datetime.datetime(value.year, value.month, value.day) 

-

            if settings.USE_TZ: 

-

                # For backwards compatibility, interpret naive datetimes in 

-

                # local time. This won't work during DST change, but we can't 

-

                # do much about it, so we let the exceptions percolate up the 

-

                # call stack. 

-

                warnings.warn("DateTimeField received a naive datetime (%s)" 

-

                              " while time zone support is active." % value, 

-

                              RuntimeWarning) 

-

                default_timezone = timezone.get_default_timezone() 

-

                value = timezone.make_aware(value, default_timezone) 

-

            return value 

-

 

-

        for format in self.input_formats: 

-

            if format.lower() == ISO_8601: 

-

                try: 

-

                    parsed = parse_datetime(value) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    if parsed is not None: 

-

                        return parsed 

-

            else: 

-

                try: 

-

                    parsed = datetime.datetime.strptime(value, format) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    return parsed 

-

 

-

        msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats) 

-

        raise ValidationError(msg) 

-

 

-

    def to_native(self, value): 

-

        if value is None or self.format is None: 

-

            return value 

-

 

-

        if self.format.lower() == ISO_8601: 

-

            ret = value.isoformat() 

-

            if ret.endswith('+00:00'): 

-

                ret = ret[:-6] + 'Z' 

-

            return ret 

-

        return value.strftime(self.format) 

-

 

-

 

-

class TimeField(WritableField): 

-

    type_name = 'TimeField' 

-

    type_label = 'time' 

-

    widget = widgets.TimeInput 

-

    form_field_class = forms.TimeField 

-

 

-

    default_error_messages = { 

-

        'invalid': _("Time has wrong format. Use one of these formats instead: %s"), 

-

    } 

-

    empty = None 

-

    input_formats = api_settings.TIME_INPUT_FORMATS 

-

    format = api_settings.TIME_FORMAT 

-

 

-

    def __init__(self, input_formats=None, format=None, *args, **kwargs): 

-

        self.input_formats = input_formats if input_formats is not None else self.input_formats 

-

        self.format = format if format is not None else self.format 

-

        super(TimeField, self).__init__(*args, **kwargs) 

-

 

-

    def from_native(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        if isinstance(value, datetime.time): 

-

            return value 

-

 

-

        for format in self.input_formats: 

-

            if format.lower() == ISO_8601: 

-

                try: 

-

                    parsed = parse_time(value) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    if parsed is not None: 

-

                        return parsed 

-

            else: 

-

                try: 

-

                    parsed = datetime.datetime.strptime(value, format) 

-

                except (ValueError, TypeError): 

-

                    pass 

-

                else: 

-

                    return parsed.time() 

-

 

-

        msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats) 

-

        raise ValidationError(msg) 

-

 

-

    def to_native(self, value): 

-

        if value is None or self.format is None: 

-

            return value 

-

 

-

        if isinstance(value, datetime.datetime): 

-

            value = value.time() 

-

 

-

        if self.format.lower() == ISO_8601: 

-

            return value.isoformat() 

-

        return value.strftime(self.format) 

-

 

-

 

-

class IntegerField(WritableField): 

-

    type_name = 'IntegerField' 

-

    type_label = 'integer' 

-

    form_field_class = forms.IntegerField 

-

 

-

    default_error_messages = { 

-

        'invalid': _('Enter a whole number.'), 

-

        'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'), 

-

        'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'), 

-

    } 

-

 

-

    def __init__(self, max_value=None, min_value=None, *args, **kwargs): 

-

        self.max_value, self.min_value = max_value, min_value 

-

        super(IntegerField, self).__init__(*args, **kwargs) 

-

 

-

        if max_value is not None: 

-

            self.validators.append(validators.MaxValueValidator(max_value)) 

-

        if min_value is not None: 

-

            self.validators.append(validators.MinValueValidator(min_value)) 

-

 

-

    def from_native(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        try: 

-

            value = int(str(value)) 

-

        except (ValueError, TypeError): 

-

            raise ValidationError(self.error_messages['invalid']) 

-

        return value 

-

 

-

 

-

class FloatField(WritableField): 

-

    type_name = 'FloatField' 

-

    type_label = 'float' 

-

    form_field_class = forms.FloatField 

-

 

-

    default_error_messages = { 

-

        'invalid': _("'%s' value must be a float."), 

-

    } 

-

 

-

    def from_native(self, value): 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        try: 

-

            return float(value) 

-

        except (TypeError, ValueError): 

-

            msg = self.error_messages['invalid'] % value 

-

            raise ValidationError(msg) 

-

 

-

 

-

class DecimalField(WritableField): 

-

    type_name = 'DecimalField' 

-

    type_label = 'decimal' 

-

    form_field_class = forms.DecimalField 

-

 

-

    default_error_messages = { 

-

        'invalid': _('Enter a number.'), 

-

        'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'), 

-

        'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'), 

-

        'max_digits': _('Ensure that there are no more than %s digits in total.'), 

-

        'max_decimal_places': _('Ensure that there are no more than %s decimal places.'), 

-

        'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.') 

-

    } 

-

 

-

    def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): 

-

        self.max_value, self.min_value = max_value, min_value 

-

        self.max_digits, self.decimal_places = max_digits, decimal_places 

-

        super(DecimalField, self).__init__(*args, **kwargs) 

-

 

-

        if max_value is not None: 

-

            self.validators.append(validators.MaxValueValidator(max_value)) 

-

        if min_value is not None: 

-

            self.validators.append(validators.MinValueValidator(min_value)) 

-

 

-

    def from_native(self, value): 

-

        """ 

-

        Validates that the input is a decimal number. Returns a Decimal 

-

        instance. Returns None for empty values. Ensures that there are no more 

-

        than max_digits in the number, and no more than decimal_places digits 

-

        after the decimal point. 

-

        """ 

-

        if value in validators.EMPTY_VALUES: 

-

            return None 

-

        value = smart_text(value).strip() 

-

        try: 

-

            value = Decimal(value) 

-

        except DecimalException: 

-

            raise ValidationError(self.error_messages['invalid']) 

-

        return value 

-

 

-

    def validate(self, value): 

-

        super(DecimalField, self).validate(value) 

-

        if value in validators.EMPTY_VALUES: 

-

            return 

-

        # Check for NaN, Inf and -Inf values. We can't compare directly for NaN, 

-

        # since it is never equal to itself. However, NaN is the only value that 

-

        # isn't equal to itself, so we can use this to identify NaN 

-

        if value != value or value == Decimal("Inf") or value == Decimal("-Inf"): 

-

            raise ValidationError(self.error_messages['invalid']) 

-

        sign, digittuple, exponent = value.as_tuple() 

-

        decimals = abs(exponent) 

-

        # digittuple doesn't include any leading zeros. 

-

        digits = len(digittuple) 

-

        if decimals > digits: 

-

            # We have leading zeros up to or past the decimal point.  Count 

-

            # everything past the decimal point as a digit.  We do not count 

-

            # 0 before the decimal point as a digit since that would mean 

-

            # we would not allow max_digits = decimal_places. 

-

            digits = decimals 

-

        whole_digits = digits - decimals 

-

 

-

        if self.max_digits is not None and digits > self.max_digits: 

-

            raise ValidationError(self.error_messages['max_digits'] % self.max_digits) 

-

        if self.decimal_places is not None and decimals > self.decimal_places: 

-

            raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places) 

-

        if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places): 

-

            raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places)) 

-

        return value 

-

 

-

 

-

class FileField(WritableField): 

-

    use_files = True 

-

    type_name = 'FileField' 

-

    type_label = 'file upload' 

-

    form_field_class = forms.FileField 

-

    widget = widgets.FileInput 

-

 

-

    default_error_messages = { 

-

        'invalid': _("No file was submitted. Check the encoding type on the form."), 

-

        'missing': _("No file was submitted."), 

-

        'empty': _("The submitted file is empty."), 

-

        'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'), 

-

        'contradiction': _('Please either submit a file or check the clear checkbox, not both.') 

-

    } 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        self.max_length = kwargs.pop('max_length', None) 

-

        self.allow_empty_file = kwargs.pop('allow_empty_file', False) 

-

        super(FileField, self).__init__(*args, **kwargs) 

-

 

-

    def from_native(self, data): 

-

        if data in validators.EMPTY_VALUES: 

-

            return None 

-

 

-

        # UploadedFile objects should have name and size attributes. 

-

        try: 

-

            file_name = data.name 

-

            file_size = data.size 

-

        except AttributeError: 

-

            raise ValidationError(self.error_messages['invalid']) 

-

 

-

        if self.max_length is not None and len(file_name) > self.max_length: 

-

            error_values = {'max': self.max_length, 'length': len(file_name)} 

-

            raise ValidationError(self.error_messages['max_length'] % error_values) 

-

        if not file_name: 

-

            raise ValidationError(self.error_messages['invalid']) 

-

        if not self.allow_empty_file and not file_size: 

-

            raise ValidationError(self.error_messages['empty']) 

-

 

-

        return data 

-

 

-

    def to_native(self, value): 

-

        return value.name 

-

 

-

 

-

class ImageField(FileField): 

-

    use_files = True 

-

    type_name = 'ImageField' 

-

    type_label = 'image upload' 

-

    form_field_class = forms.ImageField 

-

 

-

    default_error_messages = { 

-

        'invalid_image': _("Upload a valid image. The file you uploaded was " 

-

                           "either not an image or a corrupted image."), 

-

    } 

-

 

-

    def from_native(self, data): 

-

        """ 

-

        Checks that the file-upload field data contains a valid image (GIF, JPG, 

-

        PNG, possibly others -- whatever the Python Imaging Library supports). 

-

        """ 

-

        f = super(ImageField, self).from_native(data) 

-

        if f is None: 

-

            return None 

-

 

-

        from 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 

-

        # have to read the data into memory. 

-

        if hasattr(data, 'temporary_file_path'): 

-

            file = data.temporary_file_path() 

-

        else: 

-

            if hasattr(data, 'read'): 

-

                file = BytesIO(data.read()) 

-

            else: 

-

                file = BytesIO(data['content']) 

-

 

-

        try: 

-

            # load() could spot a truncated JPEG, but it loads the entire 

-

            # image in memory, which is a DoS vector. See #3848 and #18520. 

-

            # verify() must be called immediately after the constructor. 

-

            Image.open(file).verify() 

-

        except ImportError: 

-

            # Under PyPy, it is possible to import PIL. However, the underlying 

-

            # _imaging C module isn't available, so an ImportError will be 

-

            # raised. Catch and re-raise. 

-

            raise 

-

        except Exception:  # Python Imaging Library doesn't recognize it as an image 

-

            raise ValidationError(self.error_messages['invalid_image']) 

-

        if hasattr(f, 'seek') and callable(f.seek): 

-

            f.seek(0) 

-

        return f 

-

 

-

 

-

class SerializerMethodField(Field): 

-

    """ 

-

    A field that gets its value by calling a method on the serializer it's attached to. 

-

    """ 

-

 

-

    def __init__(self, method_name): 

-

        self.method_name = method_name 

-

        super(SerializerMethodField, self).__init__() 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        value = getattr(self.parent, self.method_name)(obj) 

-

        return self.to_native(value) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_filters.html b/htmlcov/rest_framework_filters.html deleted file mode 100644 index 28b6eaae5..000000000 --- a/htmlcov/rest_framework_filters.html +++ /dev/null @@ -1,367 +0,0 @@ - - - - - - - - Coverage for rest_framework/filters: 92% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

- -
-

""" 

-

Provides generic filtering backends that can be used to filter the results 

-

returned by list views. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.db import models 

-

from rest_framework.compat import django_filters, six 

-

from functools import reduce 

-

import operator 

-

 

-

FilterSet = django_filters and django_filters.FilterSet or None 

-

 

-

 

-

class BaseFilterBackend(object): 

-

    """ 

-

    A base class from which all filter backend classes should inherit. 

-

    """ 

-

 

-

    def filter_queryset(self, request, queryset, view): 

-

        """ 

-

        Return a filtered queryset. 

-

        """ 

-

        raise NotImplementedError(".filter_queryset() must be overridden.") 

-

 

-

 

-

class DjangoFilterBackend(BaseFilterBackend): 

-

    """ 

-

    A filter backend that uses django-filter. 

-

    """ 

-

    default_filter_set = FilterSet 

-

 

-

    def __init__(self): 

-

        assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' 

-

 

-

    def get_filter_class(self, view, queryset=None): 

-

        """ 

-

        Return the django-filters `FilterSet` used to filter the queryset. 

-

        """ 

-

        filter_class = getattr(view, 'filter_class', None) 

-

        filter_fields = getattr(view, 'filter_fields', None) 

-

 

-

        if filter_class: 

-

            filter_model = filter_class.Meta.model 

-

 

-

            assert issubclass(filter_model, queryset.model), \ 

-

                'FilterSet model %s does not match queryset model %s' % \ 

-

                (filter_model, queryset.model) 

-

 

-

            return filter_class 

-

 

-

        if filter_fields: 

-

            class AutoFilterSet(self.default_filter_set): 

-

                class Meta: 

-

                    model = queryset.model 

-

                    fields = filter_fields 

-

            return AutoFilterSet 

-

 

-

        return None 

-

 

-

    def filter_queryset(self, request, queryset, view): 

-

        filter_class = self.get_filter_class(view, queryset) 

-

 

-

        if filter_class: 

-

            return filter_class(request.QUERY_PARAMS, queryset=queryset).qs 

-

 

-

        return queryset 

-

 

-

 

-

class SearchFilter(BaseFilterBackend): 

-

    search_param = 'search'  # The URL query parameter used for the search. 

-

 

-

    def get_search_terms(self, request): 

-

        """ 

-

        Search terms are set by a ?search=... query parameter, 

-

        and may be comma and/or whitespace delimited. 

-

        """ 

-

        params = request.QUERY_PARAMS.get(self.search_param, '') 

-

        return params.replace(',', ' ').split() 

-

 

-

    def construct_search(self, field_name): 

-

        if field_name.startswith('^'): 

-

            return "%s__istartswith" % field_name[1:] 

-

        elif field_name.startswith('='): 

-

            return "%s__iexact" % field_name[1:] 

-

        elif field_name.startswith('@'): 

-

            return "%s__search" % field_name[1:] 

-

        else: 

-

            return "%s__icontains" % field_name 

-

 

-

    def filter_queryset(self, request, queryset, view): 

-

        search_fields = getattr(view, 'search_fields', None) 

-

 

-

        if not search_fields: 

-

            return queryset 

-

 

-

        orm_lookups = [self.construct_search(str(search_field)) 

-

                       for search_field in search_fields] 

-

 

-

        for search_term in self.get_search_terms(request): 

-

            or_queries = [models.Q(**{orm_lookup: search_term}) 

-

                          for orm_lookup in orm_lookups] 

-

            queryset = queryset.filter(reduce(operator.or_, or_queries)) 

-

 

-

        return queryset 

-

 

-

 

-

class OrderingFilter(BaseFilterBackend): 

-

    ordering_param = 'ordering'  # The URL query parameter used for the ordering. 

-

 

-

    def get_ordering(self, request): 

-

        """ 

-

        Search terms are set by a ?search=... query parameter, 

-

        and may be comma and/or whitespace delimited. 

-

        """ 

-

        params = request.QUERY_PARAMS.get(self.ordering_param) 

-

        if params: 

-

            return [param.strip() for param in params.split(',')] 

-

 

-

    def get_default_ordering(self, view): 

-

        ordering = getattr(view, 'ordering', None) 

-

        if isinstance(ordering, six.string_types): 

-

            return (ordering,) 

-

        return ordering 

-

 

-

    def remove_invalid_fields(self, queryset, ordering): 

-

        field_names = [field.name for field in queryset.model._meta.fields] 

-

        return [term for term in ordering if term.lstrip('-') in field_names] 

-

 

-

    def filter_queryset(self, request, queryset, view): 

-

        ordering = self.get_ordering(request) 

-

 

-

        if ordering: 

-

            # Skip any incorrect parameters 

-

            ordering = self.remove_invalid_fields(queryset, ordering) 

-

 

-

        if not ordering: 

-

            # Use 'ordering' attribtue by default 

-

            ordering = self.get_default_ordering(view) 

-

 

-

        if ordering: 

-

            return queryset.order_by(*ordering) 

-

 

-

        return queryset 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_generics.html b/htmlcov/rest_framework_generics.html deleted file mode 100644 index 5f5851cbd..000000000 --- a/htmlcov/rest_framework_generics.html +++ /dev/null @@ -1,1079 +0,0 @@ - - - - - - - - Coverage for rest_framework/generics: 83% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

- -
-

""" 

-

Generic views that provide commonly needed behaviour. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from django.core.exceptions import ImproperlyConfigured, PermissionDenied 

-

from django.core.paginator import Paginator, InvalidPage 

-

from django.http import Http404 

-

from django.shortcuts import get_object_or_404 as _get_object_or_404 

-

from django.utils.translation import ugettext as _ 

-

from rest_framework import views, mixins, exceptions 

-

from rest_framework.request import clone_request 

-

from rest_framework.settings import api_settings 

-

import warnings 

-

 

-

 

-

def get_object_or_404(queryset, **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) 

-

    except (TypeError, ValueError): 

-

        raise Http404 

-

 

-

 

-

class GenericAPIView(views.APIView): 

-

    """ 

-

    Base class for all other generic views. 

-

    """ 

-

 

-

    # You'll need to either set these attributes, 

-

    # or override `get_queryset()`/`get_serializer_class()`. 

-

    queryset = None 

-

    serializer_class = None 

-

 

-

    # 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. 

-

    model = None 

-

 

-

    # If you want to use object lookups other than pk, set this attribute. 

-

    # For more complex lookup requirements override `get_object()`. 

-

    lookup_field = 'pk' 

-

 

-

    # Pagination settings 

-

    paginate_by = api_settings.PAGINATE_BY 

-

    paginate_by_param = api_settings.PAGINATE_BY_PARAM 

-

    pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS 

-

    page_kwarg = 'page' 

-

 

-

    # The filter backend classes to use for queryset filtering 

-

    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS 

-

 

-

    # The following attributes may be subject to change, 

-

    # and should be considered private API. 

-

    model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS 

-

    paginator_class = Paginator 

-

 

-

    ###################################### 

-

    # These are pending deprecation... 

-

 

-

    pk_url_kwarg = 'pk' 

-

    slug_url_kwarg = 'slug' 

-

    slug_field = 'slug' 

-

    allow_empty = True 

-

    filter_backend = api_settings.FILTER_BACKEND 

-

 

-

    def get_serializer_context(self): 

-

        """ 

-

        Extra context provided to the serializer class. 

-

        """ 

-

        return { 

-

            'request': self.request, 

-

            'format': self.format_kwarg, 

-

            'view': self 

-

        } 

-

 

-

    def get_serializer(self, instance=None, data=None, 

-

                       files=None, many=False, partial=False): 

-

        """ 

-

        Return the serializer instance that should be used for validating and 

-

        deserializing input, and for serializing output. 

-

        """ 

-

        serializer_class = self.get_serializer_class() 

-

        context = self.get_serializer_context() 

-

        return serializer_class(instance, data=data, files=files, 

-

                                many=many, partial=partial, context=context) 

-

 

-

    def get_pagination_serializer(self, page): 

-

        """ 

-

        Return a serializer instance to use with paginated data. 

-

        """ 

-

        class SerializerClass(self.pagination_serializer_class): 

-

            class Meta: 

-

                object_serializer_class = self.get_serializer_class() 

-

 

-

        pagination_serializer_class = SerializerClass 

-

        context = self.get_serializer_context() 

-

        return pagination_serializer_class(instance=page, context=context) 

-

 

-

    def paginate_queryset(self, queryset, page_size=None): 

-

        """ 

-

        Paginate a queryset if required, either returning a page object, 

-

        or `None` if pagination is not configured for this view. 

-

        """ 

-

        deprecated_style = False 

-

        if page_size is not None: 

-

            warnings.warn('The `page_size` parameter to `paginate_queryset()` ' 

-

                          'is due to be 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) 

-

            deprecated_style = True 

-

        else: 

-

            # Determine the required page size. 

-

            # If pagination is not configured, simply return None. 

-

            page_size = self.get_paginate_by() 

-

            if not page_size: 

-

                return None 

-

 

-

        if not self.allow_empty: 

-

            warnings.warn( 

-

                'The `allow_empty` parameter is due to be deprecated. ' 

-

                'To use `allow_empty=False` style behavior, You should override ' 

-

                '`get_queryset()` and explicitly raise a 404 on empty querysets.', 

-

                PendingDeprecationWarning, stacklevel=2 

-

            ) 

-

 

-

        paginator = self.paginator_class(queryset, page_size, 

-

                                         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 

-

        try: 

-

            page_number = int(page) 

-

        except ValueError: 

-

            if page == 'last': 

-

                page_number = paginator.num_pages 

-

            else: 

-

                raise Http404(_("Page is not 'last', nor can it be converted to an int.")) 

-

        try: 

-

            page = paginator.page(page_number) 

-

        except InvalidPage as e: 

-

            raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { 

-

                                'page_number': page_number, 

-

                                'message': str(e) 

-

            }) 

-

 

-

        if deprecated_style: 

-

            return (paginator, page, page.object_list, page.has_other_pages()) 

-

        return page 

-

 

-

    def filter_queryset(self, queryset): 

-

        """ 

-

        Given a queryset, filter it with whichever filter backend is in use. 

-

 

-

        You are unlikely to want to override this method, although you may need 

-

        to call it either from a list view, or from a custom `get_object` 

-

        method if you want to apply the configured filtering backend to the 

-

        default queryset. 

-

        """ 

-

        filter_backends = self.filter_backends or [] 

-

        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` ' 

-

                'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' 

-

                'a *list* of filter backend classes.', 

-

                PendingDeprecationWarning, stacklevel=2 

-

            ) 

-

            filter_backends = [self.filter_backend] 

-

 

-

        for backend in filter_backends: 

-

            queryset = backend().filter_queryset(self.request, queryset, self) 

-

        return queryset 

-

 

-

    ######################## 

-

    ### The following methods provide default implementations 

-

    ### that you may want to override for more complex cases. 

-

 

-

    def get_paginate_by(self, queryset=None): 

-

        """ 

-

        Return the size of pages to use with pagination. 

-

 

-

        If `PAGINATE_BY_PARAM` is set it will attempt to get the page size 

-

        from a named query parameter in the url, eg. ?page_size=100 

-

 

-

        Otherwise defaults to using `self.paginate_by`. 

-

        """ 

-

        if queryset is not None: 

-

            warnings.warn('The `queryset` parameter to `get_paginate_by()` ' 

-

                          'is due to be deprecated.', 

-

                          PendingDeprecationWarning, stacklevel=2) 

-

 

-

        if self.paginate_by_param: 

-

            query_params = self.request.QUERY_PARAMS 

-

            try: 

-

                return int(query_params[self.paginate_by_param]) 

-

            except (KeyError, ValueError): 

-

                pass 

-

 

-

        return self.paginate_by 

-

 

-

    def get_serializer_class(self): 

-

        """ 

-

        Return the class to use for the serializer. 

-

        Defaults to using `self.serializer_class`. 

-

 

-

        You may want to override this if you need to provide different 

-

        serializations depending on the incoming request. 

-

 

-

        (Eg. admins get full serialization, others get basic serialization) 

-

        """ 

-

        serializer_class = self.serializer_class 

-

        if serializer_class is not None: 

-

            return serializer_class 

-

 

-

        assert self.model is not None, \ 

-

            "'%s' should either include a 'serializer_class' attribute, " \ 

-

            "or use the 'model' attribute as a shortcut for " \ 

-

            "automatically generating a serializer class." \ 

-

            % self.__class__.__name__ 

-

 

-

        class DefaultSerializer(self.model_serializer_class): 

-

            class Meta: 

-

                model = self.model 

-

        return DefaultSerializer 

-

 

-

    def get_queryset(self): 

-

        """ 

-

        Get the list of items for this view. 

-

        This must be an iterable, and may be a queryset. 

-

        Defaults to using `self.queryset`. 

-

 

-

        You may want to override this if you need to provide different 

-

        querysets depending on the incoming request. 

-

 

-

        (Eg. return a list of items that is specific to the user) 

-

        """ 

-

        if self.queryset is not None: 

-

            return self.queryset._clone() 

-

 

-

        if self.model is not None: 

-

            return self.model._default_manager.all() 

-

 

-

        raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" 

-

                                    % self.__class__.__name__) 

-

 

-

    def get_object(self, queryset=None): 

-

        """ 

-

        Returns the object the view is displaying. 

-

 

-

        You may want to override this if you need to provide non-standard 

-

        queryset lookups.  Eg if objects are referenced using multiple 

-

        keyword arguments in the url conf. 

-

        """ 

-

        # Determine the base queryset to use. 

-

        if queryset is None: 

-

            queryset = self.filter_queryset(self.get_queryset()) 

-

        else: 

-

            pass  # Deprecation warning 

-

 

-

        # Perform the lookup filtering. 

-

        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} 

-

        elif pk is not None and self.lookup_field == 'pk': 

-

            warnings.warn( 

-

                'The `pk_url_kwarg` attribute is due to be deprecated. ' 

-

                'Use the `lookup_field` attribute instead', 

-

                PendingDeprecationWarning 

-

            ) 

-

            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. ' 

-

                'Use the `lookup_field` attribute instead', 

-

                PendingDeprecationWarning 

-

            ) 

-

            filter_kwargs = {self.slug_field: slug} 

-

        else: 

-

            raise ImproperlyConfigured( 

-

                'Expected view %s to be called with a URL keyword argument ' 

-

                'named "%s". Fix your URL conf, or set the `.lookup_field` ' 

-

                'attribute on the view correctly.' % 

-

                (self.__class__.__name__, self.lookup_field) 

-

            ) 

-

 

-

        obj = get_object_or_404(queryset, **filter_kwargs) 

-

 

-

        # May raise a permission denied 

-

        self.check_object_permissions(self.request, obj) 

-

 

-

        return obj 

-

 

-

    ######################## 

-

    ### The following are placeholder methods, 

-

    ### and are intended to be overridden. 

-

    ### 

-

    ### The are not called by GenericAPIView directly, 

-

    ### but are used by the mixin methods. 

-

 

-

    def pre_save(self, obj): 

-

        """ 

-

        Placeholder method for calling before saving an object. 

-

 

-

        May be used to set attributes on the object that are implicit 

-

        in either the request, or the url. 

-

        """ 

-

        pass 

-

 

-

    def post_save(self, obj, created=False): 

-

        """ 

-

        Placeholder method for calling after saving an object. 

-

        """ 

-

        pass 

-

 

-

    def metadata(self, request): 

-

        """ 

-

        Return a dictionary of metadata about the view. 

-

        Used to return responses for OPTIONS requests. 

-

 

-

        We override the default behavior, and add some extra information 

-

        about the required request body for POST and PUT operations. 

-

        """ 

-

        ret = super(GenericAPIView, self).metadata(request) 

-

 

-

        actions = {} 

-

        for method in ('PUT', 'POST'): 

-

            if method not in self.allowed_methods: 

-

                continue 

-

 

-

            cloned_request = clone_request(request, method) 

-

            try: 

-

                # Test global permissions 

-

                self.check_permissions(cloned_request) 

-

                # Test object permissions 

-

                if method == 'PUT': 

-

                    self.get_object() 

-

            except (exceptions.APIException, PermissionDenied, Http404): 

-

                pass 

-

            else: 

-

                # If user has appropriate permissions for the view, include 

-

                # appropriate metadata about the fields that should be supplied. 

-

                serializer = self.get_serializer() 

-

                actions[method] = serializer.metadata() 

-

 

-

        if actions: 

-

            ret['actions'] = actions 

-

 

-

        return ret 

-

 

-

 

-

########################################################## 

-

### Concrete view classes that provide method handlers ### 

-

### by composing the mixin classes with the base view. ### 

-

########################################################## 

-

 

-

class CreateAPIView(mixins.CreateModelMixin, 

-

                    GenericAPIView): 

-

 

-

    """ 

-

    Concrete view for creating a model instance. 

-

    """ 

-

    def post(self, request, *args, **kwargs): 

-

        return self.create(request, *args, **kwargs) 

-

 

-

 

-

class ListAPIView(mixins.ListModelMixin, 

-

                  GenericAPIView): 

-

    """ 

-

    Concrete view for listing a queryset. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.list(request, *args, **kwargs) 

-

 

-

 

-

class RetrieveAPIView(mixins.RetrieveModelMixin, 

-

                      GenericAPIView): 

-

    """ 

-

    Concrete view for retrieving a model instance. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.retrieve(request, *args, **kwargs) 

-

 

-

 

-

class DestroyAPIView(mixins.DestroyModelMixin, 

-

                     GenericAPIView): 

-

 

-

    """ 

-

    Concrete view for deleting a model instance. 

-

    """ 

-

    def delete(self, request, *args, **kwargs): 

-

        return self.destroy(request, *args, **kwargs) 

-

 

-

 

-

class UpdateAPIView(mixins.UpdateModelMixin, 

-

                    GenericAPIView): 

-

 

-

    """ 

-

    Concrete view for updating a model instance. 

-

    """ 

-

    def put(self, request, *args, **kwargs): 

-

        return self.update(request, *args, **kwargs) 

-

 

-

    def patch(self, request, *args, **kwargs): 

-

        return self.partial_update(request, *args, **kwargs) 

-

 

-

 

-

class ListCreateAPIView(mixins.ListModelMixin, 

-

                        mixins.CreateModelMixin, 

-

                        GenericAPIView): 

-

    """ 

-

    Concrete view for listing a queryset or creating a model instance. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.list(request, *args, **kwargs) 

-

 

-

    def post(self, request, *args, **kwargs): 

-

        return self.create(request, *args, **kwargs) 

-

 

-

 

-

class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, 

-

                            mixins.UpdateModelMixin, 

-

                            GenericAPIView): 

-

    """ 

-

    Concrete view for retrieving, updating a model instance. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.retrieve(request, *args, **kwargs) 

-

 

-

    def put(self, request, *args, **kwargs): 

-

        return self.update(request, *args, **kwargs) 

-

 

-

    def patch(self, request, *args, **kwargs): 

-

        return self.partial_update(request, *args, **kwargs) 

-

 

-

 

-

class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, 

-

                             mixins.DestroyModelMixin, 

-

                             GenericAPIView): 

-

    """ 

-

    Concrete view for retrieving or deleting a model instance. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.retrieve(request, *args, **kwargs) 

-

 

-

    def delete(self, request, *args, **kwargs): 

-

        return self.destroy(request, *args, **kwargs) 

-

 

-

 

-

class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, 

-

                                   mixins.UpdateModelMixin, 

-

                                   mixins.DestroyModelMixin, 

-

                                   GenericAPIView): 

-

    """ 

-

    Concrete view for retrieving, updating or deleting a model instance. 

-

    """ 

-

    def get(self, request, *args, **kwargs): 

-

        return self.retrieve(request, *args, **kwargs) 

-

 

-

    def put(self, request, *args, **kwargs): 

-

        return self.update(request, *args, **kwargs) 

-

 

-

    def patch(self, request, *args, **kwargs): 

-

        return self.partial_update(request, *args, **kwargs) 

-

 

-

    def delete(self, request, *args, **kwargs): 

-

        return self.destroy(request, *args, **kwargs) 

-

 

-

 

-

########################## 

-

### Deprecated classes ### 

-

########################## 

-

 

-

class MultipleObjectAPIView(GenericAPIView): 

-

    def __init__(self, *args, **kwargs): 

-

        warnings.warn( 

-

            'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' 

-

            'You should simply subclass `GenericAPIView` instead.', 

-

            PendingDeprecationWarning, stacklevel=2 

-

        ) 

-

        super(MultipleObjectAPIView, self).__init__(*args, **kwargs) 

-

 

-

 

-

class SingleObjectAPIView(GenericAPIView): 

-

    def __init__(self, *args, **kwargs): 

-

        warnings.warn( 

-

            'Subclassing `SingleObjectAPIView` is due to be deprecated. ' 

-

            'You should simply subclass `GenericAPIView` instead.', 

-

            PendingDeprecationWarning, stacklevel=2 

-

        ) 

-

        super(SingleObjectAPIView, self).__init__(*args, **kwargs) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_mixins.html b/htmlcov/rest_framework_mixins.html deleted file mode 100644 index fa62f2ae8..000000000 --- a/htmlcov/rest_framework_mixins.html +++ /dev/null @@ -1,449 +0,0 @@ - - - - - - - - Coverage for rest_framework/mixins: 93% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

- -
-

""" 

-

Basic building blocks for generic class based views. 

-

 

-

We don't bind behaviour to http method handlers yet, 

-

which allows mixin classes to be composed in interesting ways. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from django.http import Http404 

-

from rest_framework import status 

-

from rest_framework.response import Response 

-

from rest_framework.request import clone_request 

-

import warnings 

-

 

-

 

-

def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): 

-

    """ 

-

    Given a model instance, and an optional pk and slug field, 

-

    return the full list of all other field names on that model. 

-

 

-

    For use when performing full_clean on a model instance, 

-

    so we only clean the required fields. 

-

    """ 

-

    include = [] 

-

 

-

    if pk: 

-

        # Pending deprecation 

-

        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 

-

        include.append(slug_field) 

-

 

-

    if lookup_field and lookup_field != 'pk': 

-

        include.append(lookup_field) 

-

 

-

    return [field.name for field in obj._meta.fields if field.name not in include] 

-

 

-

 

-

class CreateModelMixin(object): 

-

    """ 

-

    Create a model instance. 

-

    """ 

-

    def create(self, request, *args, **kwargs): 

-

        serializer = self.get_serializer(data=request.DATA, files=request.FILES) 

-

 

-

        if serializer.is_valid(): 

-

            self.pre_save(serializer.object) 

-

            self.object = serializer.save(force_insert=True) 

-

            self.post_save(self.object, created=True) 

-

            headers = self.get_success_headers(serializer.data) 

-

            return Response(serializer.data, status=status.HTTP_201_CREATED, 

-

                            headers=headers) 

-

 

-

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

-

 

-

    def get_success_headers(self, data): 

-

        try: 

-

            return {'Location': data['url']} 

-

        except (TypeError, KeyError): 

-

            return {} 

-

 

-

 

-

class ListModelMixin(object): 

-

    """ 

-

    List a queryset. 

-

    """ 

-

    empty_error = "Empty list and '%(class_name)s.allow_empty' is False." 

-

 

-

    def list(self, request, *args, **kwargs): 

-

        self.object_list = self.filter_queryset(self.get_queryset()) 

-

 

-

        # Default is to allow empty querysets.  This can be altered by setting 

-

        # `.allow_empty = False`, to raise 404 errors on empty querysets. 

-

        if not self.allow_empty and not self.object_list: 

-

            warnings.warn( 

-

                'The `allow_empty` parameter is due to be deprecated. ' 

-

                'To use `allow_empty=False` style behavior, You should override ' 

-

                '`get_queryset()` and explicitly raise a 404 on empty querysets.', 

-

                PendingDeprecationWarning 

-

            ) 

-

            class_name = self.__class__.__name__ 

-

            error_msg = self.empty_error % {'class_name': class_name} 

-

            raise Http404(error_msg) 

-

 

-

        # Switch between paginated or standard style responses 

-

        page = self.paginate_queryset(self.object_list) 

-

        if page is not None: 

-

            serializer = self.get_pagination_serializer(page) 

-

        else: 

-

            serializer = self.get_serializer(self.object_list, many=True) 

-

 

-

        return Response(serializer.data) 

-

 

-

 

-

class RetrieveModelMixin(object): 

-

    """ 

-

    Retrieve a model instance. 

-

    """ 

-

    def retrieve(self, request, *args, **kwargs): 

-

        self.object = self.get_object() 

-

        serializer = self.get_serializer(self.object) 

-

        return Response(serializer.data) 

-

 

-

 

-

class UpdateModelMixin(object): 

-

    """ 

-

    Update a model instance. 

-

    """ 

-

    def update(self, request, *args, **kwargs): 

-

        partial = kwargs.pop('partial', False) 

-

        self.object = self.get_object_or_none() 

-

 

-

        if self.object is None: 

-

            created = True 

-

            save_kwargs = {'force_insert': True} 

-

            success_status_code = status.HTTP_201_CREATED 

-

        else: 

-

            created = False 

-

            save_kwargs = {'force_update': True} 

-

            success_status_code = status.HTTP_200_OK 

-

 

-

        serializer = self.get_serializer(self.object, data=request.DATA, 

-

                                         files=request.FILES, partial=partial) 

-

 

-

        if serializer.is_valid(): 

-

            self.pre_save(serializer.object) 

-

            self.object = serializer.save(**save_kwargs) 

-

            self.post_save(self.object, created=created) 

-

            return Response(serializer.data, status=success_status_code) 

-

 

-

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

-

 

-

    def partial_update(self, request, *args, **kwargs): 

-

        kwargs['partial'] = True 

-

        return self.update(request, *args, **kwargs) 

-

 

-

    def get_object_or_none(self): 

-

        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')) 

-

 

-

    def pre_save(self, obj): 

-

        """ 

-

        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) 

-

        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 

-

 

-

        if lookup: 

-

            setattr(obj, self.lookup_field, lookup) 

-

 

-

        if pk: 

-

            setattr(obj, 'pk', pk) 

-

 

-

        if slug: 

-

            setattr(obj, slug_field, slug) 

-

 

-

        # Ensure we clean the attributes so that we don't eg return integer 

-

        # pk using a string representation, as provided by the url conf kwarg. 

-

        if hasattr(obj, 'full_clean'): 

-

            exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field) 

-

            obj.full_clean(exclude) 

-

 

-

 

-

class DestroyModelMixin(object): 

-

    """ 

-

    Destroy a model instance. 

-

    """ 

-

    def destroy(self, request, *args, **kwargs): 

-

        obj = self.get_object() 

-

        obj.delete() 

-

        return Response(status=status.HTTP_204_NO_CONTENT) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_models.html b/htmlcov/rest_framework_models.html deleted file mode 100644 index 6786c620a..000000000 --- a/htmlcov/rest_framework_models.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - Coverage for rest_framework/models: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

- -
-

# Just to keep things like ./manage.py test happy 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_negotiation.html b/htmlcov/rest_framework_negotiation.html deleted file mode 100644 index 7ed526c97..000000000 --- a/htmlcov/rest_framework_negotiation.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - - Coverage for rest_framework/negotiation: 90% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

- -
-

""" 

-

Content negotiation deals with selecting an appropriate renderer given the 

-

incoming request.  Typically this will be based on the request's Accept header. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.http import Http404 

-

from rest_framework import exceptions 

-

from rest_framework.settings import api_settings 

-

from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches 

-

from rest_framework.utils.mediatypes import _MediaType 

-

 

-

 

-

class BaseContentNegotiation(object): 

-

    def select_parser(self, request, parsers): 

-

        raise NotImplementedError('.select_parser() must be implemented') 

-

 

-

    def select_renderer(self, request, renderers, format_suffix=None): 

-

        raise NotImplementedError('.select_renderer() must be implemented') 

-

 

-

 

-

class DefaultContentNegotiation(BaseContentNegotiation): 

-

    settings = api_settings 

-

 

-

    def select_parser(self, request, parsers): 

-

        """ 

-

        Given a list of parsers and a media type, return the appropriate 

-

        parser to handle the incoming request. 

-

        """ 

-

        for parser in parsers: 

-

            if media_type_matches(parser.media_type, request.content_type): 

-

                return parser 

-

        return None 

-

 

-

    def select_renderer(self, request, renderers, format_suffix=None): 

-

        """ 

-

        Given a request and a list of renderers, return a two-tuple of: 

-

        (renderer, media type). 

-

        """ 

-

        # Allow URL style format override.  eg. "?format=json 

-

        format_query_param = self.settings.URL_FORMAT_OVERRIDE 

-

        format = format_suffix or request.QUERY_PARAMS.get(format_query_param) 

-

 

-

        if format: 

-

            renderers = self.filter_renderers(renderers, format) 

-

 

-

        accepts = self.get_accept_list(request) 

-

 

-

        # Check the acceptable media types against each renderer, 

-

        # attempting more specific media types first 

-

        # NB. The inner loop here isn't as bad as it first looks :) 

-

        #     Worst case is we're looping over len(accept_list) * len(self.renderers) 

-

        for media_type_set in order_by_precedence(accepts): 

-

            for renderer in renderers: 

-

                for media_type in media_type_set: 

-

                    if media_type_matches(renderer.media_type, media_type): 

-

                        # Return the most specific media type as accepted. 

-

                        if (_MediaType(renderer.media_type).precedence > 

-

                            _MediaType(media_type).precedence): 

-

                            # Eg client requests '*/*' 

-

                            # Accepted media type is 'application/json' 

-

                            return renderer, renderer.media_type 

-

                        else: 

-

                            # Eg client requests 'application/json; indent=8' 

-

                            # Accepted media type is 'application/json; indent=8' 

-

                            return renderer, media_type 

-

 

-

        raise exceptions.NotAcceptable(available_renderers=renderers) 

-

 

-

    def filter_renderers(self, renderers, format): 

-

        """ 

-

        If there is a '.json' style format suffix, filter the renderers 

-

        so that we only negotiation against those that accept that format. 

-

        """ 

-

        renderers = [renderer for renderer in renderers 

-

                     if renderer.format == format] 

-

        if not renderers: 

-

            raise Http404 

-

        return renderers 

-

 

-

    def get_accept_list(self, request): 

-

        """ 

-

        Given the incoming request, return a tokenised list of media 

-

        type strings. 

-

 

-

        Allows URL style accept override.  eg. "?accept=application/json" 

-

        """ 

-

        header = request.META.get('HTTP_ACCEPT', '*/*') 

-

        header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header) 

-

        return [token.strip() for token in header.split(',')] 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_pagination.html b/htmlcov/rest_framework_pagination.html deleted file mode 100644 index 5a3f76d82..000000000 --- a/htmlcov/rest_framework_pagination.html +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - - - Coverage for rest_framework/pagination: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

- -
-

""" 

-

Pagination serializers determine the structure of the output that should 

-

be used for paginated responses. 

-

""" 

-

from __future__ import unicode_literals 

-

from rest_framework import serializers 

-

from rest_framework.templatetags.rest_framework import replace_query_param 

-

 

-

 

-

class NextPageField(serializers.Field): 

-

    """ 

-

    Field that returns a link to the next page in paginated results. 

-

    """ 

-

    page_field = 'page' 

-

 

-

    def to_native(self, value): 

-

        if not value.has_next(): 

-

            return None 

-

        page = value.next_page_number() 

-

        request = self.context.get('request') 

-

        url = request and request.build_absolute_uri() or '' 

-

        return replace_query_param(url, self.page_field, page) 

-

 

-

 

-

class PreviousPageField(serializers.Field): 

-

    """ 

-

    Field that returns a link to the previous page in paginated results. 

-

    """ 

-

    page_field = 'page' 

-

 

-

    def to_native(self, value): 

-

        if not value.has_previous(): 

-

            return None 

-

        page = value.previous_page_number() 

-

        request = self.context.get('request') 

-

        url = request and request.build_absolute_uri() or '' 

-

        return replace_query_param(url, self.page_field, page) 

-

 

-

 

-

class DefaultObjectSerializer(serializers.Field): 

-

    """ 

-

    If no object serializer is specified, then this serializer will be applied 

-

    as the default. 

-

    """ 

-

 

-

    def __init__(self, source=None, context=None): 

-

        # Note: Swallow context kwarg - only required for eg. ModelSerializer. 

-

        super(DefaultObjectSerializer, self).__init__(source=source) 

-

 

-

 

-

class PaginationSerializerOptions(serializers.SerializerOptions): 

-

    """ 

-

    An object that stores the options that may be provided to a 

-

    pagination serializer by using the inner `Meta` class. 

-

 

-

    Accessible on the instance as `serializer.opts`. 

-

    """ 

-

    def __init__(self, meta): 

-

        super(PaginationSerializerOptions, self).__init__(meta) 

-

        self.object_serializer_class = getattr(meta, 'object_serializer_class', 

-

                                               DefaultObjectSerializer) 

-

 

-

 

-

class BasePaginationSerializer(serializers.Serializer): 

-

    """ 

-

    A base class for pagination serializers to inherit from, 

-

    to make implementing custom serializers more easy. 

-

    """ 

-

    _options_class = PaginationSerializerOptions 

-

    results_field = 'results' 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        """ 

-

        Override init to add in the object serializer field on-the-fly. 

-

        """ 

-

        super(BasePaginationSerializer, self).__init__(*args, **kwargs) 

-

        results_field = self.results_field 

-

        object_serializer = self.opts.object_serializer_class 

-

 

-

        if 'context' in kwargs: 

-

            context_kwarg = {'context': kwargs['context']} 

-

        else: 

-

            context_kwarg = {} 

-

 

-

        self.fields[results_field] = object_serializer(source='object_list', **context_kwarg) 

-

 

-

 

-

class PaginationSerializer(BasePaginationSerializer): 

-

    """ 

-

    A default implementation of a pagination serializer. 

-

    """ 

-

    count = serializers.Field(source='paginator.count') 

-

    next = NextPageField(source='*') 

-

    previous = PreviousPageField(source='*') 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_parsers.html b/htmlcov/rest_framework_parsers.html deleted file mode 100644 index 92f1db62d..000000000 --- a/htmlcov/rest_framework_parsers.html +++ /dev/null @@ -1,671 +0,0 @@ - - - - - - - - Coverage for rest_framework/parsers: 92% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

- -
-

""" 

-

Parsers are used to parse the content of incoming HTTP requests. 

-

 

-

They give us a generic way of being able to handle various media types 

-

on the request, such as form content or json encoded data. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.conf import settings 

-

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.exceptions import ParseError 

-

from rest_framework.compat import six 

-

import json 

-

import datetime 

-

import decimal 

-

 

-

 

-

class DataAndFiles(object): 

-

    def __init__(self, data, files): 

-

        self.data = data 

-

        self.files = files 

-

 

-

 

-

class BaseParser(object): 

-

    """ 

-

    All parsers should extend `BaseParser`, specifying a `media_type` 

-

    attribute, and overriding the `.parse()` method. 

-

    """ 

-

 

-

    media_type = None 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Given a stream to read from, return the parsed representation. 

-

        Should return parsed data, or a `DataAndFiles` object consisting of the 

-

        parsed data and files. 

-

        """ 

-

        raise NotImplementedError(".parse() must be overridden.") 

-

 

-

 

-

class JSONParser(BaseParser): 

-

    """ 

-

    Parses JSON-serialized data. 

-

    """ 

-

 

-

    media_type = 'application/json' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Returns a 2-tuple of `(data, files)`. 

-

 

-

        `data` will be an object which is the parsed content of the response. 

-

        `files` will always be `None`. 

-

        """ 

-

        parser_context = parser_context or {} 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

 

-

        try: 

-

            data = stream.read().decode(encoding) 

-

            return json.loads(data) 

-

        except ValueError as exc: 

-

            raise ParseError('JSON parse error - %s' % six.text_type(exc)) 

-

 

-

 

-

class YAMLParser(BaseParser): 

-

    """ 

-

    Parses YAML-serialized data. 

-

    """ 

-

 

-

    media_type = 'application/yaml' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Returns a 2-tuple of `(data, files)`. 

-

 

-

        `data` will be an object which is the parsed content of the response. 

-

        `files` will always be `None`. 

-

        """ 

-

        assert yaml, 'YAMLParser requires pyyaml to be installed' 

-

 

-

        parser_context = parser_context or {} 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

 

-

        try: 

-

            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)) 

-

 

-

 

-

class FormParser(BaseParser): 

-

    """ 

-

    Parser for form data. 

-

    """ 

-

 

-

    media_type = 'application/x-www-form-urlencoded' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Returns a 2-tuple of `(data, files)`. 

-

 

-

        `data` will be a :class:`QueryDict` containing all the form parameters. 

-

        `files` will always be :const:`None`. 

-

        """ 

-

        parser_context = parser_context or {} 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

        data = QueryDict(stream.read(), encoding=encoding) 

-

        return data 

-

 

-

 

-

class MultiPartParser(BaseParser): 

-

    """ 

-

    Parser for multipart form data, which may include file data. 

-

    """ 

-

 

-

    media_type = 'multipart/form-data' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Returns a DataAndFiles object. 

-

 

-

        `.data` will be a `QueryDict` containing all the form parameters. 

-

        `.files` will be a `QueryDict` containing all the form files. 

-

        """ 

-

        parser_context = parser_context or {} 

-

        request = parser_context['request'] 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

        meta = request.META 

-

        upload_handlers = request.upload_handlers 

-

 

-

        try: 

-

            parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding) 

-

            data, files = parser.parse() 

-

            return DataAndFiles(data, files) 

-

        except MultiPartParserError as exc: 

-

            raise ParseError('Multipart form parse error - %s' % six.u(exc)) 

-

 

-

 

-

class XMLParser(BaseParser): 

-

    """ 

-

    XML parser. 

-

    """ 

-

 

-

    media_type = 'application/xml' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        assert etree, 'XMLParser requires defusedxml to be installed' 

-

 

-

        parser_context = parser_context or {} 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

        parser = etree.DefusedXMLParser(encoding=encoding) 

-

        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)) 

-

        data = self._xml_convert(tree.getroot()) 

-

 

-

        return data 

-

 

-

    def _xml_convert(self, element): 

-

        """ 

-

        convert the xml `element` into the corresponding python object 

-

        """ 

-

 

-

        children = list(element) 

-

 

-

        if len(children) == 0: 

-

            return self._type_convert(element.text) 

-

        else: 

-

            # if the fist child tag is list-item means all children are list-item 

-

            if children[0].tag == "list-item": 

-

                data = [] 

-

                for child in children: 

-

                    data.append(self._xml_convert(child)) 

-

            else: 

-

                data = {} 

-

                for child in children: 

-

                    data[child.tag] = self._xml_convert(child) 

-

 

-

            return data 

-

 

-

    def _type_convert(self, value): 

-

        """ 

-

        Converts the value returned by the XMl parse into the equivalent 

-

        Python type 

-

        """ 

-

        if value is None: 

-

            return value 

-

 

-

        try: 

-

            return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S') 

-

        except ValueError: 

-

            pass 

-

 

-

        try: 

-

            return int(value) 

-

        except ValueError: 

-

            pass 

-

 

-

        try: 

-

            return decimal.Decimal(value) 

-

        except decimal.InvalidOperation: 

-

            pass 

-

 

-

        return value 

-

 

-

 

-

class FileUploadParser(BaseParser): 

-

    """ 

-

    Parser for file upload data. 

-

    """ 

-

    media_type = '*/*' 

-

 

-

    def parse(self, stream, media_type=None, parser_context=None): 

-

        """ 

-

        Returns a DataAndFiles object. 

-

 

-

        `.data` will be None (we expect request body to be a file content). 

-

        `.files` will be a `QueryDict` containing one 'file' element. 

-

        """ 

-

 

-

        parser_context = parser_context or {} 

-

        request = parser_context['request'] 

-

        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) 

-

        meta = request.META 

-

        upload_handlers = request.upload_handlers 

-

        filename = self.get_filename(stream, media_type, parser_context) 

-

 

-

        # Note that this code is extracted from Django's handling of 

-

        # file uploads in MultiPartParser. 

-

        content_type = meta.get('HTTP_CONTENT_TYPE', 

-

                                meta.get('CONTENT_TYPE', '')) 

-

        try: 

-

            content_length = int(meta.get('HTTP_CONTENT_LENGTH', 

-

                                          meta.get('CONTENT_LENGTH', 0))) 

-

        except (ValueError, TypeError): 

-

            content_length = None 

-

 

-

        # See if the handler will want to take care of the parsing. 

-

        for handler in upload_handlers: 

-

            result = handler.handle_raw_input(None, 

-

                                              meta, 

-

                                              content_length, 

-

                                              None, 

-

                                              encoding) 

-

            if result is not None: 

-

                return DataAndFiles(None, {'file': result[1]}) 

-

 

-

        # This is the standard case. 

-

        possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] 

-

        chunk_size = min([2 ** 31 - 4] + possible_sizes) 

-

        chunks = ChunkIter(stream, chunk_size) 

-

        counters = [0] * len(upload_handlers) 

-

 

-

        for handler in upload_handlers: 

-

            try: 

-

                handler.new_file(None, filename, content_type, 

-

                                 content_length, encoding) 

-

            except StopFutureHandlers: 

-

                break 

-

 

-

        for chunk in chunks: 

-

            for i, handler in enumerate(upload_handlers): 

-

                chunk_length = len(chunk) 

-

                chunk = handler.receive_data_chunk(chunk, counters[i]) 

-

                counters[i] += chunk_length 

-

                if chunk is None: 

-

                    break 

-

 

-

        for i, handler in enumerate(upload_handlers): 

-

            file_obj = handler.file_complete(counters[i]) 

-

            if file_obj: 

-

                return DataAndFiles(None, {'file': file_obj}) 

-

        raise ParseError("FileUpload parse error - " 

-

                         "none of upload handlers can handle the stream") 

-

 

-

    def get_filename(self, stream, media_type, parser_context): 

-

        """ 

-

        Detects the uploaded file name. First searches a 'filename' url kwarg. 

-

        Then tries to parse Content-Disposition header. 

-

        """ 

-

        try: 

-

            return parser_context['kwargs']['filename'] 

-

        except KeyError: 

-

            pass 

-

 

-

        try: 

-

            meta = parser_context['request'].META 

-

            disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION']) 

-

            return disposition[1]['filename'] 

-

        except (AttributeError, KeyError): 

-

            pass 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_permissions.html b/htmlcov/rest_framework_permissions.html deleted file mode 100644 index 20a29522b..000000000 --- a/htmlcov/rest_framework_permissions.html +++ /dev/null @@ -1,429 +0,0 @@ - - - - - - - - Coverage for rest_framework/permissions: 81% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

- -
-

""" 

-

Provides a set of pluggable permission policies. 

-

""" 

-

from __future__ import unicode_literals 

-

import inspect 

-

import warnings 

-

 

-

SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] 

-

 

-

from rest_framework.compat import oauth2_provider_scope, oauth2_constants 

-

 

-

 

-

class BasePermission(object): 

-

    """ 

-

    A base class from which all permission classes should inherit. 

-

    """ 

-

 

-

    def has_permission(self, request, view): 

-

        """ 

-

        Return `True` if permission is granted, `False` otherwise. 

-

        """ 

-

        return True 

-

 

-

    def has_object_permission(self, request, view, obj): 

-

        """ 

-

        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 

-

 

-

 

-

class AllowAny(BasePermission): 

-

    """ 

-

    Allow any access. 

-

    This isn't strictly required, since you could use an empty 

-

    permission_classes list, but it's useful because it makes the intention 

-

    more explicit. 

-

    """ 

-

    def has_permission(self, request, view): 

-

        return True 

-

 

-

 

-

class IsAuthenticated(BasePermission): 

-

    """ 

-

    Allows access only to authenticated users. 

-

    """ 

-

 

-

    def has_permission(self, request, view): 

-

        if request.user and request.user.is_authenticated(): 

-

            return True 

-

        return False 

-

 

-

 

-

class IsAdminUser(BasePermission): 

-

    """ 

-

    Allows access only to admin users. 

-

    """ 

-

 

-

    def has_permission(self, request, view): 

-

        if request.user and request.user.is_staff: 

-

            return True 

-

        return False 

-

 

-

 

-

class IsAuthenticatedOrReadOnly(BasePermission): 

-

    """ 

-

    The request is authenticated as a user, or is a read-only request. 

-

    """ 

-

 

-

    def has_permission(self, request, view): 

-

        if (request.method in SAFE_METHODS or 

-

            request.user and 

-

            request.user.is_authenticated()): 

-

            return True 

-

        return False 

-

 

-

 

-

class DjangoModelPermissions(BasePermission): 

-

    """ 

-

    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 model. 

-

 

-

    This permission can only be applied against view classes that 

-

    provide a `.model` or `.queryset` attribute. 

-

    """ 

-

 

-

    # Map methods into required permission codes. 

-

    # Override this if you need to also provide 'view' permissions, 

-

    # or if you want to provide custom permission codes. 

-

    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'], 

-

    } 

-

 

-

    authenticated_users_only = True 

-

 

-

    def get_required_permissions(self, method, model_cls): 

-

        """ 

-

        Given a model and an HTTP method, return the list of permission 

-

        codes that the user is required to have. 

-

        """ 

-

        kwargs = { 

-

            'app_label': model_cls._meta.app_label, 

-

            'model_name': model_cls._meta.module_name 

-

        } 

-

        return [perm % kwargs for perm in self.perms_map[method]] 

-

 

-

    def has_permission(self, request, 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 

-

 

-

        # Workaround to ensure DjangoModelPermissions are not applied 

-

        # to the root view when using DefaultRouter. 

-

        if model_cls is None and getattr(view, '_ignore_model_permissions', False): 

-

            return True 

-

 

-

        assert model_cls, ('Cannot apply DjangoModelPermissions on a view that' 

-

                           ' does not have `.model` or `.queryset` property.') 

-

 

-

        perms = self.get_required_permissions(request.method, model_cls) 

-

 

-

        if (request.user and 

-

            (request.user.is_authenticated() or not self.authenticated_users_only) and 

-

            request.user.has_perms(perms)): 

-

            return True 

-

        return False 

-

 

-

 

-

class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions): 

-

    """ 

-

    Similar to DjangoModelPermissions, except that anonymous users are 

-

    allowed read-only access. 

-

    """ 

-

    authenticated_users_only = False 

-

 

-

 

-

class TokenHasReadWriteScope(BasePermission): 

-

    """ 

-

    The request is authenticated as a user and the token used has the right scope 

-

    """ 

-

 

-

    def has_permission(self, request, view): 

-

        token = request.auth 

-

        read_only = request.method in SAFE_METHODS 

-

 

-

        if not token: 

-

            return False 

-

 

-

        if hasattr(token, 'resource'):  # OAuth 1 

-

            return read_only or not request.auth.resource.is_readonly 

-

        elif hasattr(token, 'scope'):  # OAuth 2 

-

            required = oauth2_constants.READ if read_only else oauth2_constants.WRITE 

-

            return oauth2_provider_scope.check(required, request.auth.scope) 

-

 

-

        assert False, ('TokenHasReadWriteScope requires either the' 

-

        '`OAuthAuthentication` or `OAuth2Authentication` authentication ' 

-

        'class to be used.') 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_relations.html b/htmlcov/rest_framework_relations.html deleted file mode 100644 index 29ad3cf65..000000000 --- a/htmlcov/rest_framework_relations.html +++ /dev/null @@ -1,1347 +0,0 @@ - - - - - - - - Coverage for rest_framework/relations: 76% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

-

574

-

575

-

576

-

577

-

578

-

579

-

580

-

581

-

582

-

583

-

584

-

585

-

586

-

587

-

588

-

589

-

590

-

591

-

592

-

593

-

594

-

595

-

596

-

597

-

598

-

599

-

600

-

601

-

602

-

603

-

604

-

605

-

606

-

607

-

608

-

609

-

610

-

611

-

612

-

613

-

614

-

615

-

616

-

617

-

618

-

619

-

620

-

621

-

622

-

623

-

624

-

625

-

626

-

627

-

628

-

629

-

630

-

631

-

632

-

633

- -
-

""" 

-

Serializer fields that deal with relationships. 

-

 

-

These fields allow you to specify the style that should be used to represent 

-

model relationships, including hyperlinks, primary keys, or slugs. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.core.exceptions import ObjectDoesNotExist, ValidationError 

-

from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch 

-

from django import forms 

-

from django.db.models.fields import BLANK_CHOICE_DASH 

-

from django.forms import widgets 

-

from django.forms.models import ModelChoiceIterator 

-

from django.utils.translation import ugettext_lazy as _ 

-

from rest_framework.fields import Field, WritableField, get_component, is_simple_callable 

-

from rest_framework.reverse import reverse 

-

from rest_framework.compat import urlparse 

-

from rest_framework.compat import smart_text 

-

import warnings 

-

 

-

 

-

##### Relational fields ##### 

-

 

-

 

-

# Not actually Writable, but subclasses may need to be. 

-

class RelatedField(WritableField): 

-

    """ 

-

    Base class for related model fields. 

-

 

-

    This represents a relationship using the unicode representation of the target. 

-

    """ 

-

    widget = widgets.Select 

-

    many_widget = widgets.SelectMultiple 

-

    form_field_class = forms.ChoiceField 

-

    many_form_field_class = forms.MultipleChoiceField 

-

 

-

    cache_choices = False 

-

    empty_label = None 

-

    read_only = True 

-

    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: 

-

            self.widget = self.many_widget 

-

            self.form_field_class = self.many_form_field_class 

-

 

-

        kwargs['read_only'] = kwargs.pop('read_only', self.read_only) 

-

        super(RelatedField, self).__init__(*args, **kwargs) 

-

 

-

        if not self.required: 

-

            self.empty_label = BLANK_CHOICE_DASH[0][1] 

-

 

-

        self.queryset = queryset 

-

 

-

    def initialize(self, parent, field_name): 

-

        super(RelatedField, self).initialize(parent, field_name) 

-

        if self.queryset is None and not self.read_only: 

-

            try: 

-

                manager = getattr(self.parent.opts.model, self.source or field_name) 

-

                if hasattr(manager, 'related'):  # Forward 

-

                    self.queryset = manager.related.model._default_manager.all() 

-

                else:  # Reverse 

-

                    self.queryset = manager.field.rel.to._default_manager.all() 

-

            except Exception: 

-

                msg = ('Serializer related fields must include a `queryset`' + 

-

                       ' argument or set `read_only=True') 

-

                raise Exception(msg) 

-

 

-

    ### We need this stuff to make form choices work... 

-

 

-

    def prepare_value(self, obj): 

-

        return self.to_native(obj) 

-

 

-

    def label_from_instance(self, obj): 

-

        """ 

-

        Return a readable representation for use with eg. select widgets. 

-

        """ 

-

        desc = smart_text(obj) 

-

        ident = smart_text(self.to_native(obj)) 

-

        if desc == ident: 

-

            return desc 

-

        return "%s - %s" % (desc, ident) 

-

 

-

    def _get_queryset(self): 

-

        return self._queryset 

-

 

-

    def _set_queryset(self, queryset): 

-

        self._queryset = queryset 

-

        self.widget.choices = self.choices 

-

 

-

    queryset = property(_get_queryset, _set_queryset) 

-

 

-

    def _get_choices(self): 

-

        # If self._choices is set, then somebody must have manually set 

-

        # the property self.choices. In this case, just return self._choices. 

-

        if hasattr(self, '_choices'): 

-

            return self._choices 

-

 

-

        # Otherwise, execute the QuerySet in self.queryset to determine the 

-

        # choices dynamically. Return a fresh ModelChoiceIterator that has not been 

-

        # consumed. Note that we're instantiating a new ModelChoiceIterator *each* 

-

        # time _get_choices() is called (and, thus, each time self.choices is 

-

        # accessed) so that we can ensure the QuerySet has not been consumed. This 

-

        # construct might look complicated but it allows for lazy evaluation of 

-

        # the queryset. 

-

        return ModelChoiceIterator(self) 

-

 

-

    def _set_choices(self, value): 

-

        # Setting choices also sets the choices on the widget. 

-

        # choices can be any iterable, but we call list() on it because 

-

        # it will be consumed more than once. 

-

        self._choices = self.widget.choices = list(value) 

-

 

-

    choices = property(_get_choices, _set_choices) 

-

 

-

    ### Regular serializer stuff... 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        try: 

-

            if self.source == '*': 

-

                return self.to_native(obj) 

-

 

-

            source = self.source or field_name 

-

            value = obj 

-

 

-

            for component in source.split('.'): 

-

                value = get_component(value, component) 

-

                if value is None: 

-

                    break 

-

        except ObjectDoesNotExist: 

-

            return None 

-

 

-

        if value is None: 

-

            return None 

-

 

-

        if self.many: 

-

            if is_simple_callable(getattr(value, 'all', None)): 

-

                return [self.to_native(item) for item in value.all()] 

-

            else: 

-

                # Also support non-queryset iterables. 

-

                # This allows us to also support plain lists of related items. 

-

                return [self.to_native(item) for item in value] 

-

        return self.to_native(value) 

-

 

-

    def field_from_native(self, data, files, field_name, into): 

-

        if self.read_only: 

-

            return 

-

 

-

        try: 

-

            if self.many: 

-

                try: 

-

                    # Form data 

-

                    value = data.getlist(field_name) 

-

                    if value == [''] or value == []: 

-

                        raise KeyError 

-

                except AttributeError: 

-

                    # Non-form data 

-

                    value = data[field_name] 

-

            else: 

-

                value = data[field_name] 

-

        except KeyError: 

-

            if self.partial: 

-

                return 

-

            value = [] if self.many else None 

-

 

-

        if value in (None, '') and self.required: 

-

            raise ValidationError(self.error_messages['required']) 

-

        elif value in (None, ''): 

-

            into[(self.source or field_name)] = None 

-

        elif self.many: 

-

            into[(self.source or field_name)] = [self.from_native(item) for item in value] 

-

        else: 

-

            into[(self.source or field_name)] = self.from_native(value) 

-

 

-

 

-

### PrimaryKey relationships 

-

 

-

class PrimaryKeyRelatedField(RelatedField): 

-

    """ 

-

    Represents a relationship as a pk value. 

-

    """ 

-

    read_only = False 

-

 

-

    default_error_messages = { 

-

        'does_not_exist': _("Invalid pk '%s' - object does not exist."), 

-

        'incorrect_type': _('Incorrect type.  Expected pk value, received %s.'), 

-

    } 

-

 

-

    # TODO: Remove these field hacks... 

-

    def prepare_value(self, obj): 

-

        return self.to_native(obj.pk) 

-

 

-

    def label_from_instance(self, obj): 

-

        """ 

-

        Return a readable representation for use with eg. select widgets. 

-

        """ 

-

        desc = smart_text(obj) 

-

        ident = smart_text(self.to_native(obj.pk)) 

-

        if desc == ident: 

-

            return desc 

-

        return "%s - %s" % (desc, ident) 

-

 

-

    # TODO: Possibly change this to just take `obj`, through prob less performant 

-

    def to_native(self, pk): 

-

        return pk 

-

 

-

    def from_native(self, data): 

-

        if self.queryset is None: 

-

            raise Exception('Writable related fields must include a `queryset` argument') 

-

 

-

        try: 

-

            return self.queryset.get(pk=data) 

-

        except ObjectDoesNotExist: 

-

            msg = self.error_messages['does_not_exist'] % smart_text(data) 

-

            raise ValidationError(msg) 

-

        except (TypeError, ValueError): 

-

            received = type(data).__name__ 

-

            msg = self.error_messages['incorrect_type'] % received 

-

            raise ValidationError(msg) 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        if self.many: 

-

            # To-many relationship 

-

 

-

            queryset = None 

-

            if not self.source: 

-

                # Prefer obj.serializable_value for performance reasons 

-

                try: 

-

                    queryset = obj.serializable_value(field_name) 

-

                except AttributeError: 

-

                    pass 

-

            if queryset is None: 

-

                # RelatedManager (reverse relationship) 

-

                source = self.source or field_name 

-

                queryset = obj 

-

                for component in source.split('.'): 

-

                    queryset = get_component(queryset, component) 

-

 

-

            # Forward relationship 

-

            if is_simple_callable(getattr(queryset, 'all', None)): 

-

                return [self.to_native(item.pk) for item in queryset.all()] 

-

            else: 

-

                # Also support non-queryset iterables. 

-

                # This allows us to also support plain lists of related items. 

-

                return [self.to_native(item.pk) for item in queryset] 

-

 

-

        # To-one relationship 

-

        try: 

-

            # Prefer obj.serializable_value for performance reasons 

-

            pk = obj.serializable_value(self.source or field_name) 

-

        except AttributeError: 

-

            # RelatedObject (reverse relationship) 

-

            try: 

-

                pk = getattr(obj, self.source or field_name).pk 

-

            except ObjectDoesNotExist: 

-

                return None 

-

 

-

        # Forward relationship 

-

        return self.to_native(pk) 

-

 

-

 

-

### Slug relationships 

-

 

-

 

-

class SlugRelatedField(RelatedField): 

-

    """ 

-

    Represents a relationship using a unique field on the target. 

-

    """ 

-

    read_only = False 

-

 

-

    default_error_messages = { 

-

        'does_not_exist': _("Object with %s=%s does not exist."), 

-

        'invalid': _('Invalid value.'), 

-

    } 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        self.slug_field = kwargs.pop('slug_field', None) 

-

        assert self.slug_field, 'slug_field is required' 

-

        super(SlugRelatedField, self).__init__(*args, **kwargs) 

-

 

-

    def to_native(self, obj): 

-

        return getattr(obj, self.slug_field) 

-

 

-

    def from_native(self, data): 

-

        if self.queryset is None: 

-

            raise Exception('Writable related fields must include a `queryset` argument') 

-

 

-

        try: 

-

            return self.queryset.get(**{self.slug_field: data}) 

-

        except ObjectDoesNotExist: 

-

            raise ValidationError(self.error_messages['does_not_exist'] % 

-

                                  (self.slug_field, smart_text(data))) 

-

        except (TypeError, ValueError): 

-

            msg = self.error_messages['invalid'] 

-

            raise ValidationError(msg) 

-

 

-

 

-

### Hyperlinked relationships 

-

 

-

class HyperlinkedRelatedField(RelatedField): 

-

    """ 

-

    Represents a relationship using hyperlinking. 

-

    """ 

-

    read_only = False 

-

    lookup_field = 'pk' 

-

 

-

    default_error_messages = { 

-

        'no_match': _('Invalid hyperlink - No URL match'), 

-

        'incorrect_match': _('Invalid hyperlink - Incorrect URL match'), 

-

        'configuration_error': _('Invalid hyperlink due to configuration error'), 

-

        'does_not_exist': _("Invalid hyperlink - object does not exist."), 

-

        'incorrect_type': _('Incorrect type.  Expected url string, received %s.'), 

-

    } 

-

 

-

    # These are all pending deprecation 

-

    pk_url_kwarg = 'pk' 

-

    slug_field = 'slug' 

-

    slug_url_kwarg = None  # Defaults to same as `slug_field` unless overridden 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        try: 

-

            self.view_name = kwargs.pop('view_name') 

-

        except KeyError: 

-

            raise ValueError("Hyperlinked field requires 'view_name' kwarg") 

-

 

-

        self.lookup_field = kwargs.pop('lookup_field', self.lookup_field) 

-

        self.format = kwargs.pop('format', None) 

-

 

-

        # These are pending deprecation 

-

        if 'pk_url_kwarg' in kwargs: 

-

            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' 

-

            warnings.warn(msg, PendingDeprecationWarning, 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) 

-

        if 'slug_field' in kwargs: 

-

            msg = 'slug_field is pending deprecation. Use lookup_field instead.' 

-

            warnings.warn(msg, PendingDeprecationWarning, 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) 

-

        default_slug_kwarg = self.slug_url_kwarg or self.slug_field 

-

        self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) 

-

 

-

        super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) 

-

 

-

    def get_url(self, obj, view_name, request, format): 

-

        """ 

-

        Given an object, return the URL that hyperlinks to the object. 

-

 

-

        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) 

-

        kwargs = {self.lookup_field: lookup_field} 

-

        try: 

-

            return reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

        except NoReverseMatch: 

-

            pass 

-

 

-

        if self.pk_url_kwarg != 'pk': 

-

            # Only try pk if it has been explicitly set. 

-

            # Otherwise, the default `lookup_field = 'pk'` has us covered. 

-

            pk = obj.pk 

-

            kwargs = {self.pk_url_kwarg: pk} 

-

            try: 

-

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

            except NoReverseMatch: 

-

                pass 

-

 

-

        slug = getattr(obj, self.slug_field, None) 

-

        if slug is not None: 

-

            # Only try slug if it corresponds to an attribute on the object. 

-

            kwargs = {self.slug_url_kwarg: slug} 

-

            try: 

-

                ret = reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

                if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug': 

-

                    # 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.' \ 

-

                          'You should set `lookup_field=slug` on the HyperlinkedRelatedField.' 

-

                    warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) 

-

                return ret 

-

            except NoReverseMatch: 

-

                pass 

-

 

-

        raise NoReverseMatch() 

-

 

-

    def get_object(self, queryset, view_name, view_args, view_kwargs): 

-

        """ 

-

        Return the object corresponding to a matched URL. 

-

 

-

        Takes the matched URL conf arguments, and the queryset, and should 

-

        return an object instance, or raise an `ObjectDoesNotExist` exception. 

-

        """ 

-

        lookup = view_kwargs.get(self.lookup_field, None) 

-

        pk = view_kwargs.get(self.pk_url_kwarg, None) 

-

        slug = view_kwargs.get(self.slug_url_kwarg, None) 

-

 

-

        if lookup is not None: 

-

            filter_kwargs = {self.lookup_field: lookup} 

-

        elif pk is not None: 

-

            filter_kwargs = {'pk': pk} 

-

        elif slug is not None: 

-

            filter_kwargs = {self.slug_field: slug} 

-

        else: 

-

            raise ObjectDoesNotExist() 

-

 

-

        return queryset.get(**filter_kwargs) 

-

 

-

    def to_native(self, obj): 

-

        view_name = self.view_name 

-

        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) 

-

 

-

        # If the object has not yet been saved then we cannot hyperlink to it. 

-

        if getattr(obj, 'pk', None) is None: 

-

            return 

-

 

-

        # Return the hyperlink, or error if incorrectly configured. 

-

        try: 

-

            return self.get_url(obj, view_name, request, format) 

-

        except NoReverseMatch: 

-

            msg = ( 

-

                'Could not resolve URL for hyperlinked relationship using ' 

-

                'view name "%s". You may have failed to include the related ' 

-

                'model in your API, or incorrectly configured the ' 

-

                '`lookup_field` attribute on this field.' 

-

            ) 

-

            raise Exception(msg % view_name) 

-

 

-

    def from_native(self, value): 

-

        # Convert URL -> model instance pk 

-

        # TODO: Use values_list 

-

        queryset = self.queryset 

-

        if queryset is None: 

-

            raise Exception('Writable related fields must include a `queryset` argument') 

-

 

-

        try: 

-

            http_prefix = value.startswith(('http:', 'https:')) 

-

        except AttributeError: 

-

            msg = self.error_messages['incorrect_type'] 

-

            raise ValidationError(msg % type(value).__name__) 

-

 

-

        if http_prefix: 

-

            # If needed convert absolute URLs to relative path 

-

            value = urlparse.urlparse(value).path 

-

            prefix = get_script_prefix() 

-

            if value.startswith(prefix): 

-

                value = '/' + value[len(prefix):] 

-

 

-

        try: 

-

            match = resolve(value) 

-

        except Exception: 

-

            raise ValidationError(self.error_messages['no_match']) 

-

 

-

        if match.view_name != self.view_name: 

-

            raise ValidationError(self.error_messages['incorrect_match']) 

-

 

-

        try: 

-

            return self.get_object(queryset, match.view_name, 

-

                                   match.args, match.kwargs) 

-

        except (ObjectDoesNotExist, TypeError, ValueError): 

-

            raise ValidationError(self.error_messages['does_not_exist']) 

-

 

-

 

-

class HyperlinkedIdentityField(Field): 

-

    """ 

-

    Represents the instance, or a property on the instance, using hyperlinking. 

-

    """ 

-

    lookup_field = 'pk' 

-

    read_only = True 

-

 

-

    # These are all pending deprecation 

-

    pk_url_kwarg = 'pk' 

-

    slug_field = 'slug' 

-

    slug_url_kwarg = None  # Defaults to same as `slug_field` unless overridden 

-

 

-

    def __init__(self, *args, **kwargs): 

-

        try: 

-

            self.view_name = kwargs.pop('view_name') 

-

        except KeyError: 

-

            msg = "HyperlinkedIdentityField requires 'view_name' argument" 

-

            raise ValueError(msg) 

-

 

-

        self.format = kwargs.pop('format', None) 

-

        lookup_field = kwargs.pop('lookup_field', None) 

-

        self.lookup_field = lookup_field or self.lookup_field 

-

 

-

        # These are pending deprecation 

-

        if 'pk_url_kwarg' in kwargs: 

-

            msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.' 

-

            warnings.warn(msg, PendingDeprecationWarning, 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) 

-

        if 'slug_field' in kwargs: 

-

            msg = 'slug_field is pending deprecation. Use lookup_field instead.' 

-

            warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) 

-

 

-

        self.slug_field = kwargs.pop('slug_field', self.slug_field) 

-

        default_slug_kwarg = self.slug_url_kwarg or self.slug_field 

-

        self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) 

-

        self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) 

-

 

-

        super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        request = self.context.get('request', None) 

-

        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) 

-

 

-

        # By default use whatever format is given for the current context 

-

        # unless the target is a different type to the source. 

-

        # 

-

        # Eg. Consider a HyperlinkedIdentityField pointing from a json 

-

        # representation to an html property of that representation... 

-

        # 

-

        # '/snippets/1/' should link to '/snippets/1/highlight/' 

-

        # ...but... 

-

        # '/snippets/1/.json' should link to '/snippets/1/highlight/.html' 

-

        if format and self.format and self.format != format: 

-

            format = self.format 

-

 

-

        # Return the hyperlink, or error if incorrectly configured. 

-

        try: 

-

            return self.get_url(obj, view_name, request, format) 

-

        except NoReverseMatch: 

-

            msg = ( 

-

                'Could not resolve URL for hyperlinked relationship using ' 

-

                'view name "%s". You may have failed to include the related ' 

-

                'model in your API, or incorrectly configured the ' 

-

                '`lookup_field` attribute on this field.' 

-

            ) 

-

            raise Exception(msg % view_name) 

-

 

-

    def get_url(self, obj, view_name, request, format): 

-

        """ 

-

        Given an object, return the URL that hyperlinks to the object. 

-

 

-

        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) 

-

        kwargs = {self.lookup_field: lookup_field} 

-

        try: 

-

            return reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

        except NoReverseMatch: 

-

            pass 

-

 

-

        if self.pk_url_kwarg != 'pk': 

-

            # Only try pk lookup if it has been explicitly set. 

-

            # Otherwise, the default `lookup_field = 'pk'` has us covered. 

-

            kwargs = {self.pk_url_kwarg: obj.pk} 

-

            try: 

-

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

            except NoReverseMatch: 

-

                pass 

-

 

-

        slug = getattr(obj, self.slug_field, None) 

-

        if slug: 

-

            # Only use slug lookup if a slug field exists on the model 

-

            kwargs = {self.slug_url_kwarg: slug} 

-

            try: 

-

                return reverse(view_name, kwargs=kwargs, request=request, format=format) 

-

            except NoReverseMatch: 

-

                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/htmlcov/rest_framework_renderers.html b/htmlcov/rest_framework_renderers.html deleted file mode 100644 index 58c71b855..000000000 --- a/htmlcov/rest_framework_renderers.html +++ /dev/null @@ -1,1227 +0,0 @@ - - - - - - - - Coverage for rest_framework/renderers: 92% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

- -
-

""" 

-

Renderers are used to serialize a response into specific media types. 

-

 

-

They give us a generic way of being able to handle various media types 

-

on the response, such as JSON encoded data or HTML output. 

-

 

-

REST framework also provides an HTML renderer the renders the browsable API. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

import copy 

-

import json 

-

from django import forms 

-

from django.core.exceptions import ImproperlyConfigured 

-

from django.http.multipartparser import parse_header 

-

from django.template import RequestContext, loader, Template 

-

from django.utils.xmlutils import SimplerXMLGenerator 

-

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.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 

-

 

-

 

-

class BaseRenderer(object): 

-

    """ 

-

    All renderers should extend this class, setting the `media_type` 

-

    and `format` attributes, and override the `.render()` method. 

-

    """ 

-

 

-

    media_type = None 

-

    format = None 

-

    charset = 'utf-8' 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        raise NotImplemented('Renderer class requires .render() to be implemented') 

-

 

-

 

-

class JSONRenderer(BaseRenderer): 

-

    """ 

-

    Renderer which serializes to JSON. 

-

    Applies JSON's backslash-u character escaping for non-ascii characters. 

-

    """ 

-

 

-

    media_type = 'application/json' 

-

    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. 

-

    # See: http://www.ietf.org/rfc/rfc4627.txt 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Render `data` into JSON. 

-

        """ 

-

        if data is None: 

-

            return '' 

-

 

-

        # If 'indent' is provided in the context, then pretty print the result. 

-

        # E.g. If we're being called by the BrowsableAPIRenderer. 

-

        renderer_context = renderer_context or {} 

-

        indent = renderer_context.get('indent', None) 

-

 

-

        if accepted_media_type: 

-

            # If the media type looks like 'application/json; indent=4', 

-

            # then pretty print the result. 

-

            base_media_type, params = parse_header(accepted_media_type.encode('ascii')) 

-

            indent = params.get('indent', indent) 

-

            try: 

-

                indent = max(min(int(indent), 8), 0) 

-

            except (ValueError, TypeError): 

-

                indent = None 

-

 

-

        ret = json.dumps(data, cls=self.encoder_class, 

-

            indent=indent, ensure_ascii=self.ensure_ascii) 

-

 

-

        # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, 

-

        # but if ensure_ascii=False, the return type is underspecified, 

-

        # 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 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. 

-

    """ 

-

 

-

 

-

class JSONPRenderer(JSONRenderer): 

-

    """ 

-

    Renderer which serializes to json, 

-

    wrapping the json output in a callback function. 

-

    """ 

-

 

-

    media_type = 'application/javascript' 

-

    format = 'jsonp' 

-

    callback_parameter = 'callback' 

-

    default_callback = 'callback' 

-

 

-

    def get_callback(self, renderer_context): 

-

        """ 

-

        Determine the name of the callback to wrap around the json output. 

-

        """ 

-

        request = renderer_context.get('request', None) 

-

        params = request and request.QUERY_PARAMS or {} 

-

        return params.get(self.callback_parameter, self.default_callback) 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Renders into jsonp, wrapping the json output in a callback function. 

-

 

-

        Clients may set the callback function name using a query parameter 

-

        on the URL, for example: ?callback=exampleCallbackName 

-

        """ 

-

        renderer_context = renderer_context or {} 

-

        callback = self.get_callback(renderer_context) 

-

        json = super(JSONPRenderer, self).render(data, accepted_media_type, 

-

                                                 renderer_context) 

-

        return callback.encode(self.charset) + b'(' + json + b');' 

-

 

-

 

-

class XMLRenderer(BaseRenderer): 

-

    """ 

-

    Renderer which serializes to XML. 

-

    """ 

-

 

-

    media_type = 'application/xml' 

-

    format = 'xml' 

-

    charset = 'utf-8' 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Renders *obj* into serialized XML. 

-

        """ 

-

        if data is None: 

-

            return '' 

-

 

-

        stream = StringIO() 

-

 

-

        xml = SimplerXMLGenerator(stream, self.charset) 

-

        xml.startDocument() 

-

        xml.startElement("root", {}) 

-

 

-

        self._to_xml(xml, data) 

-

 

-

        xml.endElement("root") 

-

        xml.endDocument() 

-

        return stream.getvalue() 

-

 

-

    def _to_xml(self, xml, data): 

-

        if isinstance(data, (list, tuple)): 

-

            for item in data: 

-

                xml.startElement("list-item", {}) 

-

                self._to_xml(xml, item) 

-

                xml.endElement("list-item") 

-

 

-

        elif isinstance(data, dict): 

-

            for key, value in six.iteritems(data): 

-

                xml.startElement(key, {}) 

-

                self._to_xml(xml, value) 

-

                xml.endElement(key) 

-

 

-

        elif data is None: 

-

            # Don't output any value 

-

            pass 

-

 

-

        else: 

-

            xml.characters(smart_text(data)) 

-

 

-

 

-

class YAMLRenderer(BaseRenderer): 

-

    """ 

-

    Renderer which serializes to YAML. 

-

    """ 

-

 

-

    media_type = 'application/yaml' 

-

    format = 'yaml' 

-

    encoder = encoders.SafeDumper 

-

    charset = 'utf-8' 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Renders *obj* into serialized YAML. 

-

        """ 

-

        assert yaml, 'YAMLRenderer requires pyyaml to be installed' 

-

 

-

        if data is None: 

-

            return '' 

-

 

-

        return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder) 

-

 

-

 

-

class TemplateHTMLRenderer(BaseRenderer): 

-

    """ 

-

    An HTML renderer for use with templates. 

-

 

-

    The data supplied to the Response object should be a dictionary that will 

-

    be used as context for the template. 

-

 

-

    The template name is determined by (in order of preference): 

-

 

-

    1. An explicit `.template_name` attribute set on the response. 

-

    2. An explicit `.template_name` attribute set on this class. 

-

    3. The return result of calling `view.get_template_names()`. 

-

 

-

    For example: 

-

        data = {'users': User.objects.all()} 

-

        return Response(data, template_name='users.html') 

-

 

-

    For pre-rendered HTML, see StaticHTMLRenderer. 

-

    """ 

-

 

-

    media_type = 'text/html' 

-

    format = 'html' 

-

    template_name = None 

-

    exception_template_names = [ 

-

        '%(status_code)s.html', 

-

        'api_exception.html' 

-

    ] 

-

    charset = 'utf-8' 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Renders data to HTML, using Django's standard template rendering. 

-

 

-

        The template name is determined by (in order of preference): 

-

 

-

        1. An explicit .template_name set on the response. 

-

        2. An explicit .template_name set on this class. 

-

        3. The return result of calling view.get_template_names(). 

-

        """ 

-

        renderer_context = renderer_context or {} 

-

        view = renderer_context['view'] 

-

        request = renderer_context['request'] 

-

        response = renderer_context['response'] 

-

 

-

        if response.exception: 

-

            template = self.get_exception_template(response) 

-

        else: 

-

            template_names = self.get_template_names(response, view) 

-

            template = self.resolve_template(template_names) 

-

 

-

        context = self.resolve_context(data, request, response) 

-

        return template.render(context) 

-

 

-

    def resolve_template(self, template_names): 

-

        return loader.select_template(template_names) 

-

 

-

    def resolve_context(self, data, request, response): 

-

        if response.exception: 

-

            data['status_code'] = response.status_code 

-

        return RequestContext(request, data) 

-

 

-

    def get_template_names(self, response, view): 

-

        if response.template_name: 

-

            return [response.template_name] 

-

        elif self.template_name: 

-

            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') 

-

 

-

    def get_exception_template(self, response): 

-

        template_names = [name % {'status_code': response.status_code} 

-

                          for name in self.exception_template_names] 

-

 

-

        try: 

-

            # Try to find an appropriate error template 

-

            return self.resolve_template(template_names) 

-

        except Exception: 

-

            # Fall back to using eg '404 Not Found' 

-

            return Template('%d %s' % (response.status_code, 

-

                                       response.status_text.title())) 

-

 

-

 

-

# Note, subclass TemplateHTMLRenderer simply for the exception behavior 

-

class StaticHTMLRenderer(TemplateHTMLRenderer): 

-

    """ 

-

    An HTML renderer class that simply returns pre-rendered HTML. 

-

 

-

    The data supplied to the Response object should be a string representing 

-

    the pre-rendered HTML content. 

-

 

-

    For example: 

-

        data = '<html><body>example</body></html>' 

-

        return Response(data) 

-

 

-

    For template rendered HTML, see TemplateHTMLRenderer. 

-

    """ 

-

    media_type = 'text/html' 

-

    format = 'html' 

-

    charset = 'utf-8' 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        renderer_context = renderer_context or {} 

-

        response = renderer_context['response'] 

-

 

-

        if response and response.exception: 

-

            request = renderer_context['request'] 

-

            template = self.get_exception_template(response) 

-

            context = self.resolve_context(data, request, response) 

-

            return template.render(context) 

-

 

-

        return data 

-

 

-

 

-

class BrowsableAPIRenderer(BaseRenderer): 

-

    """ 

-

    HTML renderer used to self-document the API. 

-

    """ 

-

    media_type = 'text/html' 

-

    format = 'api' 

-

    template = 'rest_framework/api.html' 

-

    charset = 'utf-8' 

-

 

-

    def get_default_renderer(self, view): 

-

        """ 

-

        Return an instance of the first valid renderer. 

-

        (Don't use another documenting renderer.) 

-

        """ 

-

        renderers = [renderer for renderer in view.renderer_classes 

-

                     if not issubclass(renderer, BrowsableAPIRenderer)] 

-

        if not renderers: 

-

            return None 

-

        return renderers[0]() 

-

 

-

    def get_content(self, renderer, data, 

-

                    accepted_media_type, renderer_context): 

-

        """ 

-

        Get the content as if it had been rendered by the default 

-

        non-documenting renderer. 

-

        """ 

-

        if not renderer: 

-

            return '[No renderers were found]' 

-

 

-

        renderer_context['indent'] = 4 

-

        content = renderer.render(data, accepted_media_type, renderer_context) 

-

 

-

        if renderer.charset is None: 

-

            return '[%d bytes of binary content]' % len(content) 

-

 

-

        return content 

-

 

-

    def show_form_for_method(self, view, method, request, obj): 

-

        """ 

-

        Returns True if a form should be shown for this method. 

-

        """ 

-

        if not method in view.allowed_methods: 

-

            return  # Not a valid method 

-

 

-

        if not api_settings.FORM_METHOD_OVERRIDE: 

-

            return  # Cannot use form overloading 

-

 

-

        try: 

-

            view.check_permissions(request) 

-

            if obj is not None: 

-

                view.check_object_permissions(request, obj) 

-

        except exceptions.APIException: 

-

            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 

-

        # correct form for each method. 

-

        restore = view.request 

-

        request = clone_request(request, method) 

-

        view.request = request 

-

        try: 

-

            return self.get_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): 

-

        """ 

-

        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. 

-

        """ 

-

        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 not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: 

-

            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 

-

 

-

    def get_raw_data_form(self, view, method, request, media_types): 

-

        """ 

-

        Returns a form that allows for arbitrary content types to be tunneled 

-

        via standard HTML forms. 

-

        (Which are typically application/x-www-form-urlencoded) 

-

        """ 

-

 

-

        # 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 

-

 

-

        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__() 

-

 

-

                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() 

-

 

-

    def get_name(self, view): 

-

        return get_view_name(view.__class__, getattr(view, 'suffix', None)) 

-

 

-

    def get_description(self, view): 

-

        return get_view_description(view.__class__, html=True) 

-

 

-

    def get_breadcrumbs(self, request): 

-

        return get_breadcrumbs(request.path) 

-

 

-

    def render(self, data, accepted_media_type=None, renderer_context=None): 

-

        """ 

-

        Render the HTML for the browsable API representation. 

-

        """ 

-

        accepted_media_type = accepted_media_type or '' 

-

        renderer_context = renderer_context or {} 

-

 

-

        view = renderer_context['view'] 

-

        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) 

-

 

-

        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) 

-

 

-

        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) 

-

        description = self.get_description(view) 

-

        breadcrumb_list = self.get_breadcrumbs(request) 

-

 

-

        template = loader.get_template(self.template) 

-

        context = RequestContext(request, { 

-

            'content': content, 

-

            'view': view, 

-

            'request': request, 

-

            'response': response, 

-

            'description': description, 

-

            'name': name, 

-

            'version': VERSION, 

-

            'breadcrumblist': breadcrumb_list, 

-

            '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, 

-

 

-

            'raw_data_put_form': raw_data_put_form, 

-

            '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, 

-

 

-

            'api_settings': api_settings 

-

        }) 

-

 

-

        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) 

-

        if response.status_code == status.HTTP_204_NO_CONTENT: 

-

            response.status_code = status.HTTP_200_OK 

-

 

-

        return ret 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_request.html b/htmlcov/rest_framework_request.html deleted file mode 100644 index 03f2c3e34..000000000 --- a/htmlcov/rest_framework_request.html +++ /dev/null @@ -1,819 +0,0 @@ - - - - - - - - Coverage for rest_framework/request: 95% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

- -
-

""" 

-

The Request class is used as a wrapper around the standard request object. 

-

 

-

The wrapped request then offers a richer API, in particular : 

-

 

-

    - content automatically parsed according to `Content-Type` header, 

-

      and available as `request.DATA` 

-

    - full support of PUT method, including support for file uploads 

-

    - form overloading of HTTP method, content type and content 

-

""" 

-

from __future__ import unicode_literals 

-

from django.conf import settings 

-

from django.http import QueryDict 

-

from django.http.multipartparser import parse_header 

-

from django.utils.datastructures import MultiValueDict 

-

from rest_framework import HTTP_HEADER_ENCODING 

-

from rest_framework import exceptions 

-

from rest_framework.compat import BytesIO 

-

from rest_framework.settings import api_settings 

-

 

-

 

-

def is_form_media_type(media_type): 

-

    """ 

-

    Return True if the media type is a valid form media type. 

-

    """ 

-

    base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING)) 

-

    return (base_media_type == 'application/x-www-form-urlencoded' or 

-

            base_media_type == 'multipart/form-data') 

-

 

-

 

-

class Empty(object): 

-

    """ 

-

    Placeholder for unset attributes. 

-

    Cannot use `None`, as that may be a valid value. 

-

    """ 

-

    pass 

-

 

-

 

-

def _hasattr(obj, name): 

-

    return not getattr(obj, name) is Empty 

-

 

-

 

-

def clone_request(request, method): 

-

    """ 

-

    Internal helper method to clone a request, replacing with a different 

-

    HTTP method.  Used for checking permissions against other methods. 

-

    """ 

-

    ret = Request(request=request._request, 

-

                  parsers=request.parsers, 

-

                  authenticators=request.authenticators, 

-

                  negotiator=request.negotiator, 

-

                  parser_context=request.parser_context) 

-

    ret._data = request._data 

-

    ret._files = request._files 

-

    ret._content_type = request._content_type 

-

    ret._stream = request._stream 

-

    ret._method = method 

-

    if hasattr(request, '_user'): 

-

        ret._user = request._user 

-

    if hasattr(request, '_auth'): 

-

        ret._auth = request._auth 

-

    if hasattr(request, '_authenticator'): 

-

        ret._authenticator = request._authenticator 

-

    return ret 

-

 

-

 

-

class Request(object): 

-

    """ 

-

    Wrapper allowing to enhance a standard `HttpRequest` instance. 

-

 

-

    Kwargs: 

-

        - request(HttpRequest). The original request instance. 

-

        - parsers_classes(list/tuple). The parsers to use for parsing the 

-

          request content. 

-

        - authentication_classes(list/tuple). The authentications used to try 

-

          authenticating the request's user. 

-

    """ 

-

 

-

    _METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE 

-

    _CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE 

-

    _CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE 

-

 

-

    def __init__(self, request, parsers=None, authenticators=None, 

-

                 negotiator=None, parser_context=None): 

-

        self._request = request 

-

        self.parsers = parsers or () 

-

        self.authenticators = authenticators or () 

-

        self.negotiator = negotiator or self._default_negotiator() 

-

        self.parser_context = parser_context 

-

        self._data = Empty 

-

        self._files = Empty 

-

        self._method = Empty 

-

        self._content_type = Empty 

-

        self._stream = Empty 

-

 

-

        if self.parser_context is None: 

-

            self.parser_context = {} 

-

        self.parser_context['request'] = self 

-

        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET 

-

 

-

    def _default_negotiator(self): 

-

        return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() 

-

 

-

    @property 

-

    def method(self): 

-

        """ 

-

        Returns the HTTP method. 

-

 

-

        This allows the `method` to be overridden by using a hidden `form` 

-

        field on a form POST request. 

-

        """ 

-

        if not _hasattr(self, '_method'): 

-

            self._load_method_and_content_type() 

-

        return self._method 

-

 

-

    @property 

-

    def content_type(self): 

-

        """ 

-

        Returns the content type header. 

-

 

-

        This should be used instead of `request.META.get('HTTP_CONTENT_TYPE')`, 

-

        as it allows the content type to be overridden by using a hidden form 

-

        field on a form POST request. 

-

        """ 

-

        if not _hasattr(self, '_content_type'): 

-

            self._load_method_and_content_type() 

-

        return self._content_type 

-

 

-

    @property 

-

    def stream(self): 

-

        """ 

-

        Returns an object that may be used to stream the request content. 

-

        """ 

-

        if not _hasattr(self, '_stream'): 

-

            self._load_stream() 

-

        return self._stream 

-

 

-

    @property 

-

    def QUERY_PARAMS(self): 

-

        """ 

-

        More semantically correct name for request.GET. 

-

        """ 

-

        return self._request.GET 

-

 

-

    @property 

-

    def DATA(self): 

-

        """ 

-

        Parses the request body and returns the data. 

-

 

-

        Similar to usual behaviour of `request.POST`, except that it handles 

-

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

-

        """ 

-

        if not _hasattr(self, '_data'): 

-

            self._load_data_and_files() 

-

        return self._data 

-

 

-

    @property 

-

    def FILES(self): 

-

        """ 

-

        Parses the request body and returns any files uploaded in the request. 

-

 

-

        Similar to usual behaviour of `request.FILES`, except that it handles 

-

        arbitrary parsers, and also works on methods other than POST (eg PUT). 

-

        """ 

-

        if not _hasattr(self, '_files'): 

-

            self._load_data_and_files() 

-

        return self._files 

-

 

-

    @property 

-

    def user(self): 

-

        """ 

-

        Returns the user associated with the current request, as authenticated 

-

        by the authentication classes provided to the request. 

-

        """ 

-

        if not hasattr(self, '_user'): 

-

            self._authenticate() 

-

        return self._user 

-

 

-

    @user.setter 

-

    def user(self, value): 

-

        """ 

-

        Sets the user on the current request. This is necessary to maintain 

-

        compatilbility with django.contrib.auth where the user proprety is 

-

        set in the login and logout functions. 

-

        """ 

-

        self._user = value 

-

 

-

    @property 

-

    def auth(self): 

-

        """ 

-

        Returns any non-user authentication information associated with the 

-

        request, such as an authentication token. 

-

        """ 

-

        if not hasattr(self, '_auth'): 

-

            self._authenticate() 

-

        return self._auth 

-

 

-

    @auth.setter 

-

    def auth(self, value): 

-

        """ 

-

        Sets any non-user authentication information associated with the 

-

        request, such as an authentication token. 

-

        """ 

-

        self._auth = value 

-

 

-

    @property 

-

    def successful_authenticator(self): 

-

        """ 

-

        Return the instance of the authentication instance class that was used 

-

        to authenticate the request, or `None`. 

-

        """ 

-

        if not hasattr(self, '_authenticator'): 

-

            self._authenticate() 

-

        return self._authenticator 

-

 

-

    def _load_data_and_files(self): 

-

        """ 

-

        Parses the request content into self.DATA and self.FILES. 

-

        """ 

-

        if not _hasattr(self, '_content_type'): 

-

            self._load_method_and_content_type() 

-

 

-

        if not _hasattr(self, '_data'): 

-

            self._data, self._files = self._parse() 

-

 

-

    def _load_method_and_content_type(self): 

-

        """ 

-

        Sets the method and content_type, and then check if they've 

-

        been overridden. 

-

        """ 

-

        self._content_type = self.META.get('HTTP_CONTENT_TYPE', 

-

                                           self.META.get('CONTENT_TYPE', '')) 

-

 

-

        self._perform_form_overloading() 

-

 

-

        if not _hasattr(self, '_method'): 

-

            self._method = self._request.method 

-

 

-

            if self._method == 'POST': 

-

                # Allow X-HTTP-METHOD-OVERRIDE header 

-

                self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', 

-

                                             self._method) 

-

 

-

    def _load_stream(self): 

-

        """ 

-

        Return the content body of the request, as a stream. 

-

        """ 

-

        try: 

-

            content_length = int(self.META.get('CONTENT_LENGTH', 

-

                                    self.META.get('HTTP_CONTENT_LENGTH'))) 

-

        except (ValueError, TypeError): 

-

            content_length = 0 

-

 

-

        if content_length == 0: 

-

            self._stream = None 

-

        elif hasattr(self._request, 'read'): 

-

            self._stream = self._request 

-

        else: 

-

            self._stream = BytesIO(self.raw_post_data) 

-

 

-

    def _perform_form_overloading(self): 

-

        """ 

-

        If this is a form POST request, then we need to check if the method and 

-

        content/content_type have been overridden by setting them in hidden 

-

        form fields or not. 

-

        """ 

-

 

-

        USE_FORM_OVERLOADING = ( 

-

            self._METHOD_PARAM or 

-

            (self._CONTENT_PARAM and self._CONTENTTYPE_PARAM) 

-

        ) 

-

 

-

        # We only need to use form overloading on form POST requests. 

-

        if (not USE_FORM_OVERLOADING 

-

            or self._request.method != 'POST' 

-

            or not is_form_media_type(self._content_type)): 

-

            return 

-

 

-

        # At this point we're committed to parsing the request as form data. 

-

        self._data = self._request.POST 

-

        self._files = self._request.FILES 

-

 

-

        # Method overloading - change the method and remove the param from the content. 

-

        if (self._METHOD_PARAM and 

-

            self._METHOD_PARAM in self._data): 

-

            self._method = self._data[self._METHOD_PARAM].upper() 

-

 

-

        # Content overloading - modify the content type, and force re-parse. 

-

        if (self._CONTENT_PARAM and 

-

            self._CONTENTTYPE_PARAM and 

-

            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._data, self._files = (Empty, Empty) 

-

 

-

    def _parse(self): 

-

        """ 

-

        Parse the request content, returning a two-tuple of (data, files) 

-

 

-

        May raise an `UnsupportedMediaType`, or `ParseError` exception. 

-

        """ 

-

        stream = self.stream 

-

        media_type = self.content_type 

-

 

-

        if stream is None or media_type is None: 

-

            empty_data = QueryDict('', self._request._encoding) 

-

            empty_files = MultiValueDict() 

-

            return (empty_data, empty_files) 

-

 

-

        parser = self.negotiator.select_parser(self, self.parsers) 

-

 

-

        if not parser: 

-

            raise exceptions.UnsupportedMediaType(media_type) 

-

 

-

        parsed = parser.parse(stream, media_type, self.parser_context) 

-

 

-

        # Parser classes may return the raw data, or a 

-

        # DataAndFiles object.  Unpack the result as required. 

-

        try: 

-

            return (parsed.data, parsed.files) 

-

        except AttributeError: 

-

            empty_files = MultiValueDict() 

-

            return (parsed, empty_files) 

-

 

-

    def _authenticate(self): 

-

        """ 

-

        Attempt to authenticate the request using each authentication instance 

-

        in turn. 

-

        Returns a three-tuple of (authenticator, user, authtoken). 

-

        """ 

-

        for authenticator in self.authenticators: 

-

            try: 

-

                user_auth_tuple = authenticator.authenticate(self) 

-

            except exceptions.APIException: 

-

                self._not_authenticated() 

-

                raise 

-

 

-

            if not user_auth_tuple is None: 

-

                self._authenticator = authenticator 

-

                self._user, self._auth = user_auth_tuple 

-

                return 

-

 

-

        self._not_authenticated() 

-

 

-

    def _not_authenticated(self): 

-

        """ 

-

        Return a three-tuple of (authenticator, user, authtoken), representing 

-

        an unauthenticated request. 

-

 

-

        By default this will be (None, AnonymousUser, None). 

-

        """ 

-

        self._authenticator = None 

-

 

-

        if api_settings.UNAUTHENTICATED_USER: 

-

            self._user = api_settings.UNAUTHENTICATED_USER() 

-

        else: 

-

            self._user = None 

-

 

-

        if api_settings.UNAUTHENTICATED_TOKEN: 

-

            self._auth = api_settings.UNAUTHENTICATED_TOKEN() 

-

        else: 

-

            self._auth = None 

-

 

-

    def __getattr__(self, attr): 

-

        """ 

-

        Proxy other attributes to the underlying HttpRequest object. 

-

        """ 

-

        return getattr(self._request, attr) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_response.html b/htmlcov/rest_framework_response.html deleted file mode 100644 index d297ecd0b..000000000 --- a/htmlcov/rest_framework_response.html +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - - Coverage for rest_framework/response: 98% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

- -
-

""" 

-

The Response class in REST framework is similar to HTTPResponse, except that 

-

it is initialized with unrendered data, instead of a pre-rendered string. 

-

 

-

The appropriate renderer is called during Django's template response rendering. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.core.handlers.wsgi import STATUS_CODE_TEXT 

-

from django.template.response import SimpleTemplateResponse 

-

from rest_framework.compat import six 

-

 

-

 

-

class Response(SimpleTemplateResponse): 

-

    """ 

-

    An HttpResponse that allows its data to be rendered into 

-

    arbitrary media types. 

-

    """ 

-

 

-

    def __init__(self, data=None, status=200, 

-

                 template_name=None, headers=None, 

-

                 exception=False, content_type=None): 

-

        """ 

-

        Alters the init arguments slightly. 

-

        For example, drop 'template_name', and instead use 'data'. 

-

 

-

        Setting 'renderer' and 'media_type' will typically be deferred, 

-

        For example being set automatically by the `APIView`. 

-

        """ 

-

        super(Response, self).__init__(None, status=status) 

-

        self.data = data 

-

        self.template_name = template_name 

-

        self.exception = exception 

-

        self.content_type = content_type 

-

 

-

        if headers: 

-

            for name, value in six.iteritems(headers): 

-

                self[name] = value 

-

 

-

    @property 

-

    def rendered_content(self): 

-

        renderer = getattr(self, 'accepted_renderer', None) 

-

        media_type = getattr(self, 'accepted_media_type', None) 

-

        context = getattr(self, 'renderer_context', None) 

-

 

-

        assert renderer, ".accepted_renderer not set on Response" 

-

        assert media_type, ".accepted_media_type not set on Response" 

-

        assert context, ".renderer_context not set on Response" 

-

        context['response'] = self 

-

 

-

        charset = renderer.charset 

-

        content_type = self.content_type 

-

 

-

        if content_type is None and charset is not None: 

-

            content_type = "{0}; charset={1}".format(media_type, charset) 

-

        elif content_type is None: 

-

            content_type = media_type 

-

        self['Content-Type'] = content_type 

-

 

-

        ret = renderer.render(self.data, media_type, context) 

-

        if isinstance(ret, six.text_type): 

-

            assert charset, 'renderer returned unicode, and did not specify ' \ 

-

            'a charset value.' 

-

            return bytes(ret.encode(charset)) 

-

        return ret 

-

 

-

    @property 

-

    def status_text(self): 

-

        """ 

-

        Returns reason text corresponding to our HTTP response status code. 

-

        Provided for convenience. 

-

        """ 

-

        # TODO: Deprecate and use a template tag instead 

-

        # TODO: Status code text for RFC 6585 status codes 

-

        return STATUS_CODE_TEXT.get(self.status_code, '') 

-

 

-

    def __getstate__(self): 

-

        """ 

-

        Remove attributes from the response that shouldn't be cached 

-

        """ 

-

        state = super(Response, self).__getstate__() 

-

        for key in ('accepted_renderer', 'renderer_context', 'data'): 

-

            if key in state: 

-

                del state[key] 

-

        return state 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_reverse.html b/htmlcov/rest_framework_reverse.html deleted file mode 100644 index 4e7a8de23..000000000 --- a/htmlcov/rest_framework_reverse.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - Coverage for rest_framework/reverse: 75% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

- -
-

""" 

-

Provide reverse functions that return fully qualified URLs 

-

""" 

-

from __future__ import unicode_literals 

-

from django.core.urlresolvers import reverse as django_reverse 

-

from django.utils.functional import lazy 

-

 

-

 

-

def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): 

-

    """ 

-

    Same as `django.core.urlresolvers.reverse`, but optionally takes a request 

-

    and returns a fully qualified URL, using the request to get the base URL. 

-

    """ 

-

    if format is not None: 

-

        kwargs = kwargs or {} 

-

        kwargs['format'] = format 

-

    url = django_reverse(viewname, args=args, kwargs=kwargs, **extra) 

-

    if request: 

-

        return request.build_absolute_uri(url) 

-

    return url 

-

 

-

 

-

reverse_lazy = lazy(reverse, str) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_routers.html b/htmlcov/rest_framework_routers.html deleted file mode 100644 index f08d5007e..000000000 --- a/htmlcov/rest_framework_routers.html +++ /dev/null @@ -1,595 +0,0 @@ - - - - - - - - Coverage for rest_framework/routers: 94% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

- -
-

""" 

-

Routers provide a convenient and consistent way of automatically 

-

determining the URL conf for your API. 

-

 

-

They are used by simply instantiating a Router class, and then registering 

-

all the required ViewSets with that router. 

-

 

-

For example, you might have a `urls.py` that looks something like this: 

-

 

-

    router = routers.DefaultRouter() 

-

    router.register('users', UserViewSet, 'user') 

-

    router.register('accounts', AccountViewSet, 'account') 

-

 

-

    urlpatterns = router.urls 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from collections import namedtuple 

-

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 

-

 

-

 

-

Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) 

-

 

-

 

-

def replace_methodname(format_string, methodname): 

-

    """ 

-

    Partially format a format_string, swapping out any 

-

    '{methodname}' or '{methodnamehyphen}' components. 

-

    """ 

-

    methodnamehyphen = methodname.replace('_', '-') 

-

    ret = format_string 

-

    ret = ret.replace('{methodname}', methodname) 

-

    ret = ret.replace('{methodnamehyphen}', methodnamehyphen) 

-

    return ret 

-

 

-

 

-

class BaseRouter(object): 

-

    def __init__(self): 

-

        self.registry = [] 

-

 

-

    def register(self, prefix, viewset, base_name=None): 

-

        if base_name is None: 

-

            base_name = self.get_default_base_name(viewset) 

-

        self.registry.append((prefix, viewset, base_name)) 

-

 

-

    def get_default_base_name(self, viewset): 

-

        """ 

-

        If `base_name` is not specified, attempt to automatically determine 

-

        it from the viewset. 

-

        """ 

-

        raise NotImplemented('get_default_base_name must be overridden') 

-

 

-

    def get_urls(self): 

-

        """ 

-

        Return a list of URL patterns, given the registered viewsets. 

-

        """ 

-

        raise NotImplemented('get_urls must be overridden') 

-

 

-

    @property 

-

    def urls(self): 

-

        if not hasattr(self, '_urls'): 

-

            self._urls = patterns('', *self.get_urls()) 

-

        return self._urls 

-

 

-

 

-

class SimpleRouter(BaseRouter): 

-

    routes = [ 

-

        # List route. 

-

        Route( 

-

            url=r'^{prefix}{trailing_slash}$', 

-

            mapping={ 

-

                'get': 'list', 

-

                'post': 'create' 

-

            }, 

-

            name='{basename}-list', 

-

            initkwargs={'suffix': 'List'} 

-

        ), 

-

        # Detail route. 

-

        Route( 

-

            url=r'^{prefix}/{lookup}{trailing_slash}$', 

-

            mapping={ 

-

                'get': 'retrieve', 

-

                'put': 'update', 

-

                'patch': 'partial_update', 

-

                'delete': 'destroy' 

-

            }, 

-

            name='{basename}-detail', 

-

            initkwargs={'suffix': 'Instance'} 

-

        ), 

-

        # Dynamically generated routes. 

-

        # Generated using @action or @link decorators on methods of the viewset. 

-

        Route( 

-

            url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$', 

-

            mapping={ 

-

                '{httpmethod}': '{methodname}', 

-

            }, 

-

            name='{basename}-{methodnamehyphen}', 

-

            initkwargs={} 

-

        ), 

-

    ] 

-

 

-

    def __init__(self, trailing_slash=True): 

-

        self.trailing_slash = trailing_slash and '/' or '' 

-

        super(SimpleRouter, self).__init__() 

-

 

-

    def get_default_base_name(self, viewset): 

-

        """ 

-

        If `base_name` is not specified, attempt to automatically determine 

-

        it from the viewset. 

-

        """ 

-

        model_cls = getattr(viewset, 'model', None) 

-

        queryset = getattr(viewset, 'queryset', None) 

-

        if model_cls is None and queryset is not None: 

-

            model_cls = queryset.model 

-

 

-

        assert model_cls, '`name` not argument not specified, and could ' \ 

-

            'not automatically determine the name from the viewset, as ' \ 

-

            'it does not have a `.model` or `.queryset` attribute.' 

-

 

-

        return model_cls._meta.object_name.lower() 

-

 

-

    def get_routes(self, viewset): 

-

        """ 

-

        Augment `self.routes` with any dynamically generated routes. 

-

 

-

        Returns a list of the Route namedtuple. 

-

        """ 

-

 

-

        # Determine any `@action` or `@link` decorated methods on the viewset 

-

        dynamic_routes = [] 

-

        for methodname in dir(viewset): 

-

            attr = getattr(viewset, methodname) 

-

            httpmethods = getattr(attr, 'bind_to_methods', None) 

-

            if httpmethods: 

-

                dynamic_routes.append((httpmethods, methodname)) 

-

 

-

        ret = [] 

-

        for route in self.routes: 

-

            if route.mapping == {'{httpmethod}': '{methodname}'}: 

-

                # 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( 

-

                        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) 

-

 

-

        return ret 

-

 

-

    def get_method_map(self, viewset, method_map): 

-

        """ 

-

        Given a viewset, and a mapping of http methods to actions, 

-

        return a new mapping which only includes any mappings that 

-

        are actually implemented by the viewset. 

-

        """ 

-

        bound_methods = {} 

-

        for method, action in method_map.items(): 

-

            if hasattr(viewset, action): 

-

                bound_methods[method] = action 

-

        return bound_methods 

-

 

-

    def get_lookup_regex(self, viewset): 

-

        """ 

-

        Given a viewset, return the portion of URL regex that is used 

-

        to match against a single instance. 

-

        """ 

-

        base_regex = '(?P<{lookup_field}>[^/]+)' 

-

        lookup_field = getattr(viewset, 'lookup_field', 'pk') 

-

        return base_regex.format(lookup_field=lookup_field) 

-

 

-

    def get_urls(self): 

-

        """ 

-

        Use the registered viewsets to generate a list of URL patterns. 

-

        """ 

-

        ret = [] 

-

 

-

        for prefix, viewset, basename in self.registry: 

-

            lookup = self.get_lookup_regex(viewset) 

-

            routes = self.get_routes(viewset) 

-

 

-

            for route in routes: 

-

 

-

                # Only actions which actually exist on the viewset will be bound 

-

                mapping = self.get_method_map(viewset, route.mapping) 

-

                if not mapping: 

-

                    continue 

-

 

-

                # Build the url pattern 

-

                regex = route.url.format( 

-

                    prefix=prefix, 

-

                    lookup=lookup, 

-

                    trailing_slash=self.trailing_slash 

-

                ) 

-

                view = viewset.as_view(mapping, **route.initkwargs) 

-

                name = route.name.format(basename=basename) 

-

                ret.append(url(regex, view, name=name)) 

-

 

-

        return ret 

-

 

-

 

-

class DefaultRouter(SimpleRouter): 

-

    """ 

-

    The default router extends the SimpleRouter, but also adds in a default 

-

    API root view, and adds format suffix patterns to the URLs. 

-

    """ 

-

    include_root_view = True 

-

    include_format_suffixes = True 

-

    root_view_name = 'api-root' 

-

 

-

    def get_api_root_view(self): 

-

        """ 

-

        Return a view to use as the API root. 

-

        """ 

-

        api_root_dict = {} 

-

        list_name = self.routes[0].name 

-

        for prefix, viewset, basename in self.registry: 

-

            api_root_dict[prefix] = list_name.format(basename=basename) 

-

 

-

        class APIRoot(views.APIView): 

-

            _ignore_model_permissions = True 

-

 

-

            def get(self, request, format=None): 

-

                ret = {} 

-

                for key, url_name in api_root_dict.items(): 

-

                    ret[key] = reverse(url_name, request=request, format=format) 

-

                return Response(ret) 

-

 

-

        return APIRoot.as_view() 

-

 

-

    def get_urls(self): 

-

        """ 

-

        Generate the list of URL patterns, including a default root view 

-

        for the API, and appending `.json` style format suffixes. 

-

        """ 

-

        urls = [] 

-

 

-

        if self.include_root_view: 

-

            root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name) 

-

            urls.append(root_url) 

-

 

-

        default_urls = super(DefaultRouter, self).get_urls() 

-

        urls.extend(default_urls) 

-

 

-

        if self.include_format_suffixes: 

-

            urls = format_suffix_patterns(urls) 

-

 

-

        return urls 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_serializers.html b/htmlcov/rest_framework_serializers.html deleted file mode 100644 index 79dc56474..000000000 --- a/htmlcov/rest_framework_serializers.html +++ /dev/null @@ -1,2011 +0,0 @@ - - - - - - - - Coverage for rest_framework/serializers: 94% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

-

574

-

575

-

576

-

577

-

578

-

579

-

580

-

581

-

582

-

583

-

584

-

585

-

586

-

587

-

588

-

589

-

590

-

591

-

592

-

593

-

594

-

595

-

596

-

597

-

598

-

599

-

600

-

601

-

602

-

603

-

604

-

605

-

606

-

607

-

608

-

609

-

610

-

611

-

612

-

613

-

614

-

615

-

616

-

617

-

618

-

619

-

620

-

621

-

622

-

623

-

624

-

625

-

626

-

627

-

628

-

629

-

630

-

631

-

632

-

633

-

634

-

635

-

636

-

637

-

638

-

639

-

640

-

641

-

642

-

643

-

644

-

645

-

646

-

647

-

648

-

649

-

650

-

651

-

652

-

653

-

654

-

655

-

656

-

657

-

658

-

659

-

660

-

661

-

662

-

663

-

664

-

665

-

666

-

667

-

668

-

669

-

670

-

671

-

672

-

673

-

674

-

675

-

676

-

677

-

678

-

679

-

680

-

681

-

682

-

683

-

684

-

685

-

686

-

687

-

688

-

689

-

690

-

691

-

692

-

693

-

694

-

695

-

696

-

697

-

698

-

699

-

700

-

701

-

702

-

703

-

704

-

705

-

706

-

707

-

708

-

709

-

710

-

711

-

712

-

713

-

714

-

715

-

716

-

717

-

718

-

719

-

720

-

721

-

722

-

723

-

724

-

725

-

726

-

727

-

728

-

729

-

730

-

731

-

732

-

733

-

734

-

735

-

736

-

737

-

738

-

739

-

740

-

741

-

742

-

743

-

744

-

745

-

746

-

747

-

748

-

749

-

750

-

751

-

752

-

753

-

754

-

755

-

756

-

757

-

758

-

759

-

760

-

761

-

762

-

763

-

764

-

765

-

766

-

767

-

768

-

769

-

770

-

771

-

772

-

773

-

774

-

775

-

776

-

777

-

778

-

779

-

780

-

781

-

782

-

783

-

784

-

785

-

786

-

787

-

788

-

789

-

790

-

791

-

792

-

793

-

794

-

795

-

796

-

797

-

798

-

799

-

800

-

801

-

802

-

803

-

804

-

805

-

806

-

807

-

808

-

809

-

810

-

811

-

812

-

813

-

814

-

815

-

816

-

817

-

818

-

819

-

820

-

821

-

822

-

823

-

824

-

825

-

826

-

827

-

828

-

829

-

830

-

831

-

832

-

833

-

834

-

835

-

836

-

837

-

838

-

839

-

840

-

841

-

842

-

843

-

844

-

845

-

846

-

847

-

848

-

849

-

850

-

851

-

852

-

853

-

854

-

855

-

856

-

857

-

858

-

859

-

860

-

861

-

862

-

863

-

864

-

865

-

866

-

867

-

868

-

869

-

870

-

871

-

872

-

873

-

874

-

875

-

876

-

877

-

878

-

879

-

880

-

881

-

882

-

883

-

884

-

885

-

886

-

887

-

888

-

889

-

890

-

891

-

892

-

893

-

894

-

895

-

896

-

897

-

898

-

899

-

900

-

901

-

902

-

903

-

904

-

905

-

906

-

907

-

908

-

909

-

910

-

911

-

912

-

913

-

914

-

915

-

916

-

917

-

918

-

919

-

920

-

921

-

922

-

923

-

924

-

925

-

926

-

927

-

928

-

929

-

930

-

931

-

932

-

933

-

934

-

935

-

936

-

937

-

938

-

939

-

940

-

941

-

942

-

943

-

944

-

945

-

946

-

947

-

948

-

949

-

950

-

951

-

952

-

953

-

954

-

955

-

956

-

957

-

958

-

959

-

960

-

961

-

962

-

963

-

964

-

965

- -
-

""" 

-

Serializers and ModelSerializers are similar to Forms and ModelForms. 

-

Unlike forms, they are not constrained to dealing with HTML output, and 

-

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 

-

response content is handled by parsers and renderers. 

-

""" 

-

from __future__ import unicode_literals 

-

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 

-

from rest_framework.compat import get_concrete_model, six 

-

 

-

# Note: We do the following so that users of the framework can use this style: 

-

# 

-

#     example_field = serializers.CharField(...) 

-

# 

-

# This helps keep the separation between model fields, form fields, and 

-

# serializer fields more explicit. 

-

 

-

from rest_framework.relations import * 

-

from rest_framework.fields import * 

-

 

-

 

-

class NestedValidationError(ValidationError): 

-

    """ 

-

    The default ValidationError behavior is to stringify each item in the list 

-

    if the messages are a list of error messages. 

-

 

-

    In the case of nested serializers, where the parent has many children, 

-

    then the child's `serializer.errors` will be a list of dicts.  In the case 

-

    of a single child, the `serializer.errors` will be a dict. 

-

 

-

    We need to override the default behavior to get properly nested error dicts. 

-

    """ 

-

 

-

    def __init__(self, message): 

-

        if isinstance(message, dict): 

-

            self.messages = [message] 

-

        else: 

-

            self.messages = message 

-

 

-

 

-

class DictWithMetadata(dict): 

-

    """ 

-

    A dict-like object, that can have additional properties attached. 

-

    """ 

-

    def __getstate__(self): 

-

        """ 

-

        Used by pickle (e.g., caching). 

-

        Overridden to remove the metadata from the dict, since it shouldn't be 

-

        pickled and may in some instances be unpickleable. 

-

        """ 

-

        return dict(self) 

-

 

-

 

-

class SortedDictWithMetadata(SortedDict): 

-

    """ 

-

    A sorted dict-like object, that can have additional properties attached. 

-

    """ 

-

    def __getstate__(self): 

-

        """ 

-

        Used by pickle (e.g., caching). 

-

        Overriden to remove the metadata from the dict, since it shouldn't be 

-

        pickle and may in some instances be unpickleable. 

-

        """ 

-

        return SortedDict(self).__dict__ 

-

 

-

 

-

def _is_protected_type(obj): 

-

    """ 

-

    True if the object is a native datatype that does not need to 

-

    be serialized further. 

-

    """ 

-

    return isinstance(obj, ( 

-

        types.NoneType, 

-

        int, long, 

-

        datetime.datetime, datetime.date, datetime.time, 

-

        float, Decimal, 

-

        basestring) 

-

    ) 

-

 

-

 

-

def _get_declared_fields(bases, attrs): 

-

    """ 

-

    Create a list of serializer field instances from the passed in 'attrs', 

-

    plus any fields on the base classes (in 'bases'). 

-

 

-

    Note that all fields from the base classes are used. 

-

    """ 

-

    fields = [(field_name, attrs.pop(field_name)) 

-

              for field_name, obj in list(six.iteritems(attrs)) 

-

              if isinstance(obj, Field)] 

-

    fields.sort(key=lambda x: x[1].creation_counter) 

-

 

-

    # If this class is subclassing another Serializer, add that Serializer's 

-

    # fields.  Note that we loop over the bases in *reverse*. This is necessary 

-

    # in order to maintain the correct order of fields. 

-

    for base in bases[::-1]: 

-

        if hasattr(base, 'base_fields'): 

-

            fields = list(base.base_fields.items()) + fields 

-

 

-

    return SortedDict(fields) 

-

 

-

 

-

class SerializerMetaclass(type): 

-

    def __new__(cls, name, bases, attrs): 

-

        attrs['base_fields'] = _get_declared_fields(bases, attrs) 

-

        return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) 

-

 

-

 

-

class SerializerOptions(object): 

-

    """ 

-

    Meta class options for Serializer 

-

    """ 

-

    def __init__(self, meta): 

-

        self.depth = getattr(meta, 'depth', 0) 

-

        self.fields = getattr(meta, 'fields', ()) 

-

        self.exclude = getattr(meta, 'exclude', ()) 

-

 

-

 

-

class BaseSerializer(WritableField): 

-

    """ 

-

    This is the Serializer implementation. 

-

    We need to implement it as `BaseSerializer` due to metaclass magicks. 

-

    """ 

-

    class Meta(object): 

-

        pass 

-

 

-

    _options_class = SerializerOptions 

-

    _dict_class = SortedDictWithMetadata 

-

 

-

    def __init__(self, instance=None, data=None, files=None, 

-

                 context=None, partial=False, many=None, 

-

                 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_add_remove = allow_add_remove 

-

 

-

        self.context = context or {} 

-

 

-

        self.init_data = data 

-

        self.init_files = files 

-

        self.object = instance 

-

        self.fields = self.get_fields() 

-

 

-

        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') 

-

 

-

        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. 

-

 

-

    def get_default_fields(self): 

-

        """ 

-

        Return the complete set of default fields for the object, as a dict. 

-

        """ 

-

        return {} 

-

 

-

    def get_fields(self): 

-

        """ 

-

        Returns the complete set of fields for the object as a dict. 

-

 

-

        This will be the set of any explicitly declared fields, 

-

        plus the set of fields returned by get_default_fields(). 

-

        """ 

-

        ret = SortedDict() 

-

 

-

        # Get the explicitly declared fields 

-

        base_fields = copy.deepcopy(self.base_fields) 

-

        for key, field in base_fields.items(): 

-

            ret[key] = field 

-

 

-

        # Add in the default fields 

-

        default_fields = self.get_default_fields() 

-

        for key, val in default_fields.items(): 

-

            if key not in ret: 

-

                ret[key] = val 

-

 

-

        # If 'fields' is specified, use those fields, in that order. 

-

        if self.opts.fields: 

-

            assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple' 

-

            new = SortedDict() 

-

            for key in self.opts.fields: 

-

                new[key] = ret[key] 

-

            ret = new 

-

 

-

        # Remove anything in 'exclude' 

-

        if self.opts.exclude: 

-

            assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple' 

-

            for key in self.opts.exclude: 

-

                ret.pop(key, None) 

-

 

-

        for key, field in ret.items(): 

-

            field.initialize(parent=self, field_name=key) 

-

 

-

        return ret 

-

 

-

    ##### 

-

    # Methods to convert or revert from objects <--> primitive representations. 

-

 

-

    def get_field_key(self, field_name): 

-

        """ 

-

        Return the key that should be used for a given field. 

-

        """ 

-

        return field_name 

-

 

-

    def restore_fields(self, data, files): 

-

        """ 

-

        Core of deserialization, together with `restore_object`. 

-

        Converts a dictionary of data into a dictionary of deserialized fields. 

-

        """ 

-

        reverted_data = {} 

-

 

-

        if data is not None and not isinstance(data, dict): 

-

            self._errors['non_field_errors'] = ['Invalid data'] 

-

            return None 

-

 

-

        for field_name, field in self.fields.items(): 

-

            field.initialize(parent=self, field_name=field_name) 

-

            try: 

-

                field.field_from_native(data, files, field_name, reverted_data) 

-

            except ValidationError as err: 

-

                self._errors[field_name] = list(err.messages) 

-

 

-

        return reverted_data 

-

 

-

    def perform_validation(self, attrs): 

-

        """ 

-

        Run `validate_<fieldname>()` and `validate()` methods on the serializer 

-

        """ 

-

        for field_name, field in self.fields.items(): 

-

            if field_name in self._errors: 

-

                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) 

-

 

-

        # If there are already errors, we don't run .validate() because 

-

        # field-validation failed and thus `attrs` may not be complete. 

-

        # which in turn can cause inconsistent validation errors. 

-

        if not self._errors: 

-

            try: 

-

                attrs = self.validate(attrs) 

-

            except ValidationError as err: 

-

                if hasattr(err, 'message_dict'): 

-

                    for field_name, error_messages in err.message_dict.items(): 

-

                        self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) 

-

                elif hasattr(err, 'messages'): 

-

                    self._errors['non_field_errors'] = err.messages 

-

 

-

        return attrs 

-

 

-

    def validate(self, attrs): 

-

        """ 

-

        Stub method, to be overridden in Serializer subclasses 

-

        """ 

-

        return attrs 

-

 

-

    def restore_object(self, attrs, instance=None): 

-

        """ 

-

        Deserialize a dictionary of attributes into an object instance. 

-

        You should override this method to control how deserialized objects 

-

        are instantiated. 

-

        """ 

-

        if instance is not None: 

-

            instance.update(attrs) 

-

            return instance 

-

        return attrs 

-

 

-

    def to_native(self, obj): 

-

        """ 

-

        Serialize objects -> primitives. 

-

        """ 

-

        ret = self._dict_class() 

-

        ret.fields = {} 

-

 

-

        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) 

-

            ret[key] = value 

-

            ret.fields[key] = field 

-

        return ret 

-

 

-

    def from_native(self, data, files): 

-

        """ 

-

        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: 

-

                attrs = self.perform_validation(attrs) 

-

        else: 

-

            self._errors['non_field_errors'] = ['No input provided'] 

-

 

-

        if not self._errors: 

-

            return self.restore_object(attrs, instance=getattr(self, 'object', None)) 

-

 

-

    def field_to_native(self, obj, field_name): 

-

        """ 

-

        Override default so that the serializer can be used as a nested field 

-

        across relationships. 

-

        """ 

-

        if self.source == '*': 

-

            return self.to_native(obj) 

-

 

-

        try: 

-

            source = self.source or field_name 

-

            value = obj 

-

 

-

            for component in source.split('.'): 

-

                value = get_component(value, component) 

-

                if value is None: 

-

                    break 

-

        except ObjectDoesNotExist: 

-

            return None 

-

 

-

        if is_simple_callable(getattr(value, 'all', None)): 

-

            return [self.to_native(item) for item in value.all()] 

-

 

-

        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: 

-

            return [self.to_native(item) for item in value] 

-

        return self.to_native(value) 

-

 

-

    def field_from_native(self, data, files, field_name, into): 

-

        """ 

-

        Override default so that the serializer can be used as a writable 

-

        nested field across relationships. 

-

        """ 

-

        if self.read_only: 

-

            return 

-

 

-

        try: 

-

            value = data[field_name] 

-

        except KeyError: 

-

            if self.default is not None and not self.partial: 

-

                # Note: partial updates shouldn't set defaults 

-

                value = copy.deepcopy(self.default) 

-

            else: 

-

                if self.required: 

-

                    raise ValidationError(self.error_messages['required']) 

-

                return 

-

 

-

        # Set the serializer object if it exists 

-

        obj = getattr(self.parent.object, field_name) if self.parent.object else None 

-

 

-

        if self.source == '*': 

-

            if value: 

-

                into.update(value) 

-

        else: 

-

            if value in (None, ''): 

-

                into[(self.source or field_name)] = None 

-

            else: 

-

                kwargs = { 

-

                    'instance': obj, 

-

                    'data': value, 

-

                    'context': self.context, 

-

                    'partial': self.partial, 

-

                    'many': self.many 

-

                } 

-

                serializer = self.__class__(**kwargs) 

-

 

-

                if serializer.is_valid(): 

-

                    into[self.source or field_name] = serializer.object 

-

                else: 

-

                    # Propagate errors up to our parent 

-

                    raise NestedValidationError(serializer.errors) 

-

 

-

    def get_identity(self, data): 

-

        """ 

-

        This hook is required for bulk update. 

-

        It is used to determine the canonical identity of a given object. 

-

 

-

        Note that the data has not been validated at this point, so we need 

-

        to make sure that we catch any cases of incorrect datatypes being 

-

        passed to this method. 

-

        """ 

-

        try: 

-

            return data.get('id', None) 

-

        except AttributeError: 

-

            return None 

-

 

-

    @property 

-

    def errors(self): 

-

        """ 

-

        Run deserialization and return error data, 

-

        setting self.object if no errors occurred. 

-

        """ 

-

        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: 

-

                ret = [] 

-

                errors = [] 

-

                update = self.object is not None 

-

 

-

                if update: 

-

                    # If this is a bulk update we need to map all the objects 

-

                    # to a canonical identity so we can determine which 

-

                    # individual object is being updated for each item in the 

-

                    # incoming data 

-

                    objects = self.object 

-

                    identities = [self.get_identity(self.to_native(obj)) for obj in objects] 

-

                    identity_to_objects = dict(zip(identities, objects)) 

-

 

-

                if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): 

-

                    for item in data: 

-

                        if update: 

-

                            # Determine which object we're updating 

-

                            identity = self.get_identity(item) 

-

                            self.object = identity_to_objects.pop(identity, None) 

-

                            if self.object is None and not self.allow_add_remove: 

-

                                ret.append(None) 

-

                                errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}) 

-

                                continue 

-

 

-

                        ret.append(self.from_native(item, None)) 

-

                        errors.append(self._errors) 

-

 

-

                    if update: 

-

                        self._deleted = identity_to_objects.values() 

-

 

-

                    self._errors = any(errors) and errors or [] 

-

                else: 

-

                    self._errors = {'non_field_errors': ['Expected a list of items.']} 

-

            else: 

-

                ret = self.from_native(data, files) 

-

 

-

            if not self._errors: 

-

                self.object = ret 

-

 

-

        return self._errors 

-

 

-

    def is_valid(self): 

-

        return not self.errors 

-

 

-

    @property 

-

    def data(self): 

-

        """ 

-

        Returns the serialized data on the serializer. 

-

        """ 

-

        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: 

-

                self._data = [self.to_native(item) for item in obj] 

-

            else: 

-

                self._data = self.to_native(obj) 

-

 

-

        return self._data 

-

 

-

    def save_object(self, obj, **kwargs): 

-

        obj.save(**kwargs) 

-

 

-

    def delete_object(self, obj): 

-

        obj.delete() 

-

 

-

    def save(self, **kwargs): 

-

        """ 

-

        Save the deserialized object and return it. 

-

        """ 

-

        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] 

-

 

-

        return self.object 

-

 

-

    def metadata(self): 

-

        """ 

-

        Return a dictionary of metadata about the fields on the serializer. 

-

        Useful for things like responding to OPTIONS requests, or generating 

-

        API schemas for auto-documentation. 

-

        """ 

-

        return SortedDict( 

-

            [(field_name, field.metadata()) 

-

            for field_name, field in six.iteritems(self.fields)] 

-

        ) 

-

 

-

 

-

class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): 

-

    pass 

-

 

-

 

-

class ModelSerializerOptions(SerializerOptions): 

-

    """ 

-

    Meta class options for ModelSerializer 

-

    """ 

-

    def __init__(self, meta): 

-

        super(ModelSerializerOptions, self).__init__(meta) 

-

        self.model = getattr(meta, 'model', None) 

-

        self.read_only_fields = getattr(meta, 'read_only_fields', ()) 

-

 

-

 

-

class ModelSerializer(Serializer): 

-

    """ 

-

    A serializer that deals with model instances and querysets. 

-

    """ 

-

    _options_class = ModelSerializerOptions 

-

 

-

    field_mapping = { 

-

        models.AutoField: IntegerField, 

-

        models.FloatField: FloatField, 

-

        models.IntegerField: IntegerField, 

-

        models.PositiveIntegerField: IntegerField, 

-

        models.SmallIntegerField: IntegerField, 

-

        models.PositiveSmallIntegerField: IntegerField, 

-

        models.DateTimeField: DateTimeField, 

-

        models.DateField: DateField, 

-

        models.TimeField: TimeField, 

-

        models.DecimalField: DecimalField, 

-

        models.EmailField: EmailField, 

-

        models.CharField: CharField, 

-

        models.URLField: URLField, 

-

        models.SlugField: SlugField, 

-

        models.TextField: CharField, 

-

        models.CommaSeparatedIntegerField: CharField, 

-

        models.BooleanField: BooleanField, 

-

        models.FileField: FileField, 

-

        models.ImageField: ImageField, 

-

    } 

-

 

-

    def get_default_fields(self): 

-

        """ 

-

        Return all the fields that should be serialized for the model. 

-

        """ 

-

 

-

        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 

-

        ret = SortedDict() 

-

        nested = bool(self.opts.depth) 

-

 

-

        # Deal with adding the primary key field 

-

        pk_field = opts.pk 

-

        while pk_field.rel and pk_field.rel.parent_link: 

-

            # If model is a child via multitable inheritance, use parent's pk 

-

            pk_field = pk_field.rel.to._meta.pk 

-

 

-

        field = self.get_pk_field(pk_field) 

-

        if field: 

-

            ret[pk_field.name] = field 

-

 

-

        # Deal with forward relationships 

-

        forward_rels = [field for field in opts.fields if field.serialize] 

-

        forward_rels += [field for field in opts.many_to_many if field.serialize] 

-

 

-

        for model_field in forward_rels: 

-

            has_through_model = False 

-

 

-

            if model_field.rel: 

-

                to_many = isinstance(model_field, 

-

                                     models.fields.related.ManyToManyField) 

-

                related_model = model_field.rel.to 

-

 

-

                if to_many and not model_field.rel.through._meta.auto_created: 

-

                    has_through_model = True 

-

 

-

            if model_field.rel and nested: 

-

                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. ' 

-

                        'Use `get_nested_field(model_field, related_model, ' 

-

                        'to_many) instead', 

-

                        PendingDeprecationWarning 

-

                    ) 

-

                    field = self.get_nested_field(model_field) 

-

                else: 

-

                    field = self.get_nested_field(model_field, related_model, to_many) 

-

            elif model_field.rel: 

-

                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. ' 

-

                        'Use `get_related_field(model_field, related_model, ' 

-

                        'to_many) instead', 

-

                        PendingDeprecationWarning 

-

                    ) 

-

                    field = self.get_related_field(model_field, to_many=to_many) 

-

                else: 

-

                    field = self.get_related_field(model_field, related_model, to_many) 

-

            else: 

-

                field = self.get_field(model_field) 

-

 

-

            if field: 

-

                if has_through_model: 

-

                    field.read_only = True 

-

 

-

                ret[model_field.name] = field 

-

 

-

        # Deal with reverse relationships 

-

        if not self.opts.fields: 

-

            reverse_rels = [] 

-

        else: 

-

            # Reverse relationships are only included if they are explicitly 

-

            # present in the `fields` option on the serializer 

-

            reverse_rels = opts.get_all_related_objects() 

-

            reverse_rels += opts.get_all_related_many_to_many_objects() 

-

 

-

        for relation in reverse_rels: 

-

            accessor_name = relation.get_accessor_name() 

-

            if not self.opts.fields or accessor_name not in self.opts.fields: 

-

                continue 

-

            related_model = relation.model 

-

            to_many = relation.field.rel.multiple 

-

            has_through_model = False 

-

            is_m2m = isinstance(relation.field, 

-

                                models.fields.related.ManyToManyField) 

-

 

-

            if is_m2m and not relation.field.rel.through._meta.auto_created: 

-

                has_through_model = True 

-

 

-

            if nested: 

-

                field = self.get_nested_field(None, related_model, to_many) 

-

            else: 

-

                field = self.get_related_field(None, related_model, to_many) 

-

 

-

            if field: 

-

                if has_through_model: 

-

                    field.read_only = True 

-

 

-

                ret[accessor_name] = field 

-

 

-

        # Add the `read_only` flag to any fields that have bee specified 

-

        # 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 " \ 

-

                "`read_only_fields`, but also added " \ 

-

                "as an explict 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` " \ 

-

                "on serializer '%s'." % \ 

-

                (self.__class__.__name__, field_name) 

-

            ret[field_name].read_only = True 

-

 

-

        return ret 

-

 

-

    def get_pk_field(self, model_field): 

-

        """ 

-

        Returns a default instance of the pk field. 

-

        """ 

-

        return self.get_field(model_field) 

-

 

-

    def get_nested_field(self, model_field, related_model, to_many): 

-

        """ 

-

        Creates a default instance of a nested relational field. 

-

 

-

        Note that model_field will be `None` for reverse relationships. 

-

        """ 

-

        class NestedModelSerializer(ModelSerializer): 

-

            class Meta: 

-

                model = related_model 

-

                depth = self.opts.depth - 1 

-

 

-

        return NestedModelSerializer(many=to_many) 

-

 

-

    def get_related_field(self, model_field, related_model, to_many): 

-

        """ 

-

        Creates a default instance of a flat relational field. 

-

 

-

        Note that model_field will be `None` for reverse relationships. 

-

        """ 

-

        # TODO: filter queryset using: 

-

        # .using(db).complex_filter(self.rel.limit_choices_to) 

-

 

-

        kwargs = { 

-

            'queryset': related_model._default_manager, 

-

            'many': to_many 

-

        } 

-

 

-

        if model_field: 

-

            kwargs['required'] = not(model_field.null or model_field.blank) 

-

 

-

        return PrimaryKeyRelatedField(**kwargs) 

-

 

-

    def get_field(self, model_field): 

-

        """ 

-

        Creates a default instance of a basic non-relational field. 

-

        """ 

-

        kwargs = {} 

-

 

-

        if model_field.null or model_field.blank: 

-

            kwargs['required'] = False 

-

 

-

        if isinstance(model_field, models.AutoField) or not model_field.editable: 

-

            kwargs['read_only'] = True 

-

 

-

        if model_field.has_default(): 

-

            kwargs['default'] = model_field.get_default() 

-

 

-

        if issubclass(model_field.__class__, models.TextField): 

-

            kwargs['widget'] = widgets.Textarea 

-

 

-

        if model_field.verbose_name is not None: 

-

            kwargs['label'] = model_field.verbose_name 

-

 

-

        if model_field.help_text is not None: 

-

            kwargs['help_text'] = model_field.help_text 

-

 

-

        # TODO: TypedChoiceField? 

-

        if model_field.flatchoices:  # This ModelField contains choices 

-

            kwargs['choices'] = model_field.flatchoices 

-

            return ChoiceField(**kwargs) 

-

 

-

        # put this below the ChoiceField because min_value isn't a valid initializer 

-

        if issubclass(model_field.__class__, models.PositiveIntegerField) or\ 

-

                issubclass(model_field.__class__, models.PositiveSmallIntegerField): 

-

            kwargs['min_value'] = 0 

-

 

-

        attribute_dict = { 

-

            models.CharField: ['max_length'], 

-

            models.CommaSeparatedIntegerField: ['max_length'], 

-

            models.DecimalField: ['max_digits', 'decimal_places'], 

-

            models.EmailField: ['max_length'], 

-

            models.FileField: ['max_length'], 

-

            models.ImageField: ['max_length'], 

-

            models.SlugField: ['max_length'], 

-

            models.URLField: ['max_length'], 

-

        } 

-

 

-

        if model_field.__class__ in attribute_dict: 

-

            attributes = attribute_dict[model_field.__class__] 

-

            for attribute in attributes: 

-

                kwargs.update({attribute: getattr(model_field, attribute)}) 

-

 

-

        try: 

-

            return self.field_mapping[model_field.__class__](**kwargs) 

-

        except KeyError: 

-

            return ModelField(model_field=model_field, **kwargs) 

-

 

-

    def get_validation_exclusions(self): 

-

        """ 

-

        Return a list of field names to exclude from model validation. 

-

        """ 

-

        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: 

-

                exclusions.remove(field_name) 

-

        return exclusions 

-

 

-

    def full_clean(self, instance): 

-

        """ 

-

        Perform Django's full_clean, and populate the `errors` dictionary 

-

        if any validation errors occur. 

-

 

-

        Note that we don't perform this inside the `.restore_object()` method, 

-

        so that subclasses can override `.restore_object()`, and still get 

-

        the full_clean validation checking. 

-

        """ 

-

        try: 

-

            instance.full_clean(exclude=self.get_validation_exclusions()) 

-

        except ValidationError as err: 

-

            self._errors = err.message_dict 

-

            return None 

-

        return instance 

-

 

-

    def restore_object(self, attrs, instance=None): 

-

        """ 

-

        Restore the model instance. 

-

        """ 

-

        m2m_data = {} 

-

        related_data = {} 

-

        meta = self.opts.model._meta 

-

 

-

        # 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() 

-

            if field_name in attrs: 

-

                related_data[field_name] = attrs.pop(field_name) 

-

 

-

        # Reverse m2m relations 

-

        for (obj, model) in meta.get_all_related_m2m_objects_with_model(): 

-

            field_name = obj.field.related_query_name() 

-

            if field_name in attrs: 

-

                m2m_data[field_name] = attrs.pop(field_name) 

-

 

-

        # Forward m2m relations 

-

        for field in meta.many_to_many: 

-

            if field.name in attrs: 

-

                m2m_data[field.name] = attrs.pop(field.name) 

-

 

-

        # Update an existing instance... 

-

        if instance is not None: 

-

            for key, val in attrs.items(): 

-

                setattr(instance, key, val) 

-

 

-

        # ...or create a new instance 

-

        else: 

-

            instance = self.opts.model(**attrs) 

-

 

-

        # Any relations that cannot be set until we've 

-

        # saved the model get hidden away on these 

-

        # private attributes, so we can deal with them 

-

        # at the point of save. 

-

        instance._related_data = related_data 

-

        instance._m2m_data = m2m_data 

-

 

-

        return instance 

-

 

-

    def from_native(self, data, files): 

-

        """ 

-

        Override the default method to also include model field validation. 

-

        """ 

-

        instance = super(ModelSerializer, self).from_native(data, files) 

-

        if not self._errors: 

-

            return self.full_clean(instance) 

-

 

-

    def save_object(self, obj, **kwargs): 

-

        """ 

-

        Save the deserialized object and return it. 

-

        """ 

-

        obj.save(**kwargs) 

-

 

-

        if getattr(obj, '_m2m_data', None): 

-

            for accessor_name, object_list in obj._m2m_data.items(): 

-

                setattr(obj, accessor_name, object_list) 

-

            del(obj._m2m_data) 

-

 

-

        if getattr(obj, '_related_data', None): 

-

            for accessor_name, related in obj._related_data.items(): 

-

                setattr(obj, accessor_name, related) 

-

            del(obj._related_data) 

-

 

-

 

-

class HyperlinkedModelSerializerOptions(ModelSerializerOptions): 

-

    """ 

-

    Options for HyperlinkedModelSerializer 

-

    """ 

-

    def __init__(self, meta): 

-

        super(HyperlinkedModelSerializerOptions, self).__init__(meta) 

-

        self.view_name = getattr(meta, 'view_name', None) 

-

        self.lookup_field = getattr(meta, 'lookup_field', None) 

-

 

-

 

-

class HyperlinkedModelSerializer(ModelSerializer): 

-

    """ 

-

    A subclass of ModelSerializer that uses hyperlinked relationships, 

-

    instead of primary key relationships. 

-

    """ 

-

    _options_class = HyperlinkedModelSerializerOptions 

-

    _default_view_name = '%(model_name)s-detail' 

-

    _hyperlink_field_class = HyperlinkedRelatedField 

-

 

-

    def get_default_fields(self): 

-

        fields = super(HyperlinkedModelSerializer, self).get_default_fields() 

-

 

-

        if self.opts.view_name is None: 

-

            self.opts.view_name = self._get_default_view_name(self.opts.model) 

-

 

-

        if 'url' not in fields: 

-

            url_field = HyperlinkedIdentityField( 

-

                view_name=self.opts.view_name, 

-

                lookup_field=self.opts.lookup_field 

-

            ) 

-

            fields.insert(0, 'url', url_field) 

-

 

-

        return fields 

-

 

-

    def get_pk_field(self, model_field): 

-

        if self.opts.fields and model_field.name in self.opts.fields: 

-

            return self.get_field(model_field) 

-

 

-

    def get_related_field(self, model_field, related_model, to_many): 

-

        """ 

-

        Creates a default instance of a flat relational field. 

-

        """ 

-

        # TODO: filter queryset using: 

-

        # .using(db).complex_filter(self.rel.limit_choices_to) 

-

        kwargs = { 

-

            'queryset': related_model._default_manager, 

-

            'view_name': self._get_default_view_name(related_model), 

-

            'many': to_many 

-

        } 

-

 

-

        if model_field: 

-

            kwargs['required'] = not(model_field.null or model_field.blank) 

-

 

-

        if self.opts.lookup_field: 

-

            kwargs['lookup_field'] = self.opts.lookup_field 

-

 

-

        return self._hyperlink_field_class(**kwargs) 

-

 

-

    def get_identity(self, data): 

-

        """ 

-

        This hook is required for bulk update. 

-

        We need to override the default, to use the url as the identity. 

-

        """ 

-

        try: 

-

            return data.get('url', None) 

-

        except AttributeError: 

-

            return None 

-

 

-

    def _get_default_view_name(self, model): 

-

        """ 

-

        Return the view name to use if 'view_name' is not specified in 'Meta' 

-

        """ 

-

        model_meta = model._meta 

-

        format_kwargs = { 

-

            'app_label': model_meta.app_label, 

-

            'model_name': model_meta.object_name.lower() 

-

        } 

-

        return self._default_view_name % format_kwargs 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_settings.html b/htmlcov/rest_framework_settings.html deleted file mode 100644 index ae47b5bc8..000000000 --- a/htmlcov/rest_framework_settings.html +++ /dev/null @@ -1,465 +0,0 @@ - - - - - - - - Coverage for rest_framework/settings: 95% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

- -
-

""" 

-

Settings for REST framework are all namespaced in the REST_FRAMEWORK setting. 

-

For example your project's `settings.py` file might look like this: 

-

 

-

REST_FRAMEWORK = { 

-

    'DEFAULT_RENDERER_CLASSES': ( 

-

        'rest_framework.renderers.JSONRenderer', 

-

        'rest_framework.renderers.YAMLRenderer', 

-

    ) 

-

    'DEFAULT_PARSER_CLASSES': ( 

-

        'rest_framework.parsers.JSONParser', 

-

        'rest_framework.parsers.YAMLParser', 

-

    ) 

-

} 

-

 

-

This module provides the `api_setting` object, that is used to access 

-

REST framework settings, checking for user settings first, then falling 

-

back to the defaults. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from django.conf import settings 

-

from django.utils import importlib 

-

 

-

from rest_framework import ISO_8601 

-

from rest_framework.compat import six 

-

 

-

 

-

USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) 

-

 

-

DEFAULTS = { 

-

    # Base API policies 

-

    'DEFAULT_RENDERER_CLASSES': ( 

-

        'rest_framework.renderers.JSONRenderer', 

-

        'rest_framework.renderers.BrowsableAPIRenderer', 

-

    ), 

-

    'DEFAULT_PARSER_CLASSES': ( 

-

        'rest_framework.parsers.JSONParser', 

-

        'rest_framework.parsers.FormParser', 

-

        'rest_framework.parsers.MultiPartParser' 

-

    ), 

-

    'DEFAULT_AUTHENTICATION_CLASSES': ( 

-

        'rest_framework.authentication.SessionAuthentication', 

-

        'rest_framework.authentication.BasicAuthentication' 

-

    ), 

-

    'DEFAULT_PERMISSION_CLASSES': ( 

-

        'rest_framework.permissions.AllowAny', 

-

    ), 

-

    'DEFAULT_THROTTLE_CLASSES': ( 

-

    ), 

-

 

-

    'DEFAULT_CONTENT_NEGOTIATION_CLASS': 

-

        'rest_framework.negotiation.DefaultContentNegotiation', 

-

 

-

    # Genric view behavior 

-

    'DEFAULT_MODEL_SERIALIZER_CLASS': 

-

        'rest_framework.serializers.ModelSerializer', 

-

    'DEFAULT_PAGINATION_SERIALIZER_CLASS': 

-

        'rest_framework.pagination.PaginationSerializer', 

-

    'DEFAULT_FILTER_BACKENDS': (), 

-

 

-

    # Throttling 

-

    'DEFAULT_THROTTLE_RATES': { 

-

        'user': None, 

-

        'anon': None, 

-

    }, 

-

 

-

    # Pagination 

-

    'PAGINATE_BY': None, 

-

    'PAGINATE_BY_PARAM': None, 

-

 

-

    # Authentication 

-

    'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 

-

    'UNAUTHENTICATED_TOKEN': None, 

-

 

-

    # Browser enhancements 

-

    'FORM_METHOD_OVERRIDE': '_method', 

-

    'FORM_CONTENT_OVERRIDE': '_content', 

-

    'FORM_CONTENTTYPE_OVERRIDE': '_content_type', 

-

    'URL_ACCEPT_OVERRIDE': 'accept', 

-

    'URL_FORMAT_OVERRIDE': 'format', 

-

 

-

    'FORMAT_SUFFIX_KWARG': 'format', 

-

 

-

    # Input and output formats 

-

    'DATE_INPUT_FORMATS': ( 

-

        ISO_8601, 

-

    ), 

-

    'DATE_FORMAT': None, 

-

 

-

    'DATETIME_INPUT_FORMATS': ( 

-

        ISO_8601, 

-

    ), 

-

    'DATETIME_FORMAT': None, 

-

 

-

    'TIME_INPUT_FORMATS': ( 

-

        ISO_8601, 

-

    ), 

-

    'TIME_FORMAT': None, 

-

 

-

    # Pending deprecation 

-

    'FILTER_BACKEND': None, 

-

} 

-

 

-

 

-

# List of settings that may be in string import notation. 

-

IMPORT_STRINGS = ( 

-

    'DEFAULT_RENDERER_CLASSES', 

-

    'DEFAULT_PARSER_CLASSES', 

-

    'DEFAULT_AUTHENTICATION_CLASSES', 

-

    'DEFAULT_PERMISSION_CLASSES', 

-

    'DEFAULT_THROTTLE_CLASSES', 

-

    'DEFAULT_CONTENT_NEGOTIATION_CLASS', 

-

    'DEFAULT_MODEL_SERIALIZER_CLASS', 

-

    'DEFAULT_PAGINATION_SERIALIZER_CLASS', 

-

    'DEFAULT_FILTER_BACKENDS', 

-

    'FILTER_BACKEND', 

-

    'UNAUTHENTICATED_USER', 

-

    'UNAUTHENTICATED_TOKEN', 

-

) 

-

 

-

 

-

def perform_import(val, setting_name): 

-

    """ 

-

    If the given setting is a string import notation, 

-

    then perform the necessary import or imports. 

-

    """ 

-

    if isinstance(val, six.string_types): 

-

        return import_from_string(val, setting_name) 

-

    elif isinstance(val, (list, tuple)): 

-

        return [import_from_string(item, setting_name) for item in val] 

-

    return val 

-

 

-

 

-

def import_from_string(val, setting_name): 

-

    """ 

-

    Attempt to import a class from a string representation. 

-

    """ 

-

    try: 

-

        # Nod to tastypie's use of importlib. 

-

        parts = val.split('.') 

-

        module_path, class_name = '.'.join(parts[:-1]), parts[-1] 

-

        module = importlib.import_module(module_path) 

-

        return getattr(module, class_name) 

-

    except ImportError as e: 

-

        msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) 

-

        raise ImportError(msg) 

-

 

-

 

-

class APISettings(object): 

-

    """ 

-

    A settings object, that allows API settings to be accessed as properties. 

-

    For example: 

-

 

-

        from rest_framework.settings import api_settings 

-

        print api_settings.DEFAULT_RENDERER_CLASSES 

-

 

-

    Any setting with string import paths will be automatically resolved 

-

    and return the class, rather than the string literal. 

-

    """ 

-

    def __init__(self, user_settings=None, defaults=None, import_strings=None): 

-

        self.user_settings = user_settings or {} 

-

        self.defaults = defaults or {} 

-

        self.import_strings = import_strings or () 

-

 

-

    def __getattr__(self, attr): 

-

        if attr not in self.defaults.keys(): 

-

            raise AttributeError("Invalid API setting: '%s'" % attr) 

-

 

-

        try: 

-

            # Check if present in user settings 

-

            val = self.user_settings[attr] 

-

        except KeyError: 

-

            # Fall back to defaults 

-

            val = self.defaults[attr] 

-

 

-

        # Coerce import strings into classes 

-

        if val and attr in self.import_strings: 

-

            val = perform_import(val, attr) 

-

 

-

        self.validate_setting(attr, val) 

-

 

-

        # Cache the result 

-

        setattr(self, attr, val) 

-

        return val 

-

 

-

    def validate_setting(self, attr, val): 

-

        if attr == 'FILTER_BACKEND' and val is not None: 

-

            # Make sure we can initialize the class 

-

            val() 

-

 

-

api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_status.html b/htmlcov/rest_framework_status.html deleted file mode 100644 index 85f919f6b..000000000 --- a/htmlcov/rest_framework_status.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - Coverage for rest_framework/status: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

- -
-

""" 

-

Descriptive HTTP status codes, for code readability. 

-

 

-

See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 

-

And RFC 6585 - http://tools.ietf.org/html/rfc6585 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

HTTP_100_CONTINUE = 100 

-

HTTP_101_SWITCHING_PROTOCOLS = 101 

-

HTTP_200_OK = 200 

-

HTTP_201_CREATED = 201 

-

HTTP_202_ACCEPTED = 202 

-

HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 

-

HTTP_204_NO_CONTENT = 204 

-

HTTP_205_RESET_CONTENT = 205 

-

HTTP_206_PARTIAL_CONTENT = 206 

-

HTTP_300_MULTIPLE_CHOICES = 300 

-

HTTP_301_MOVED_PERMANENTLY = 301 

-

HTTP_302_FOUND = 302 

-

HTTP_303_SEE_OTHER = 303 

-

HTTP_304_NOT_MODIFIED = 304 

-

HTTP_305_USE_PROXY = 305 

-

HTTP_306_RESERVED = 306 

-

HTTP_307_TEMPORARY_REDIRECT = 307 

-

HTTP_400_BAD_REQUEST = 400 

-

HTTP_401_UNAUTHORIZED = 401 

-

HTTP_402_PAYMENT_REQUIRED = 402 

-

HTTP_403_FORBIDDEN = 403 

-

HTTP_404_NOT_FOUND = 404 

-

HTTP_405_METHOD_NOT_ALLOWED = 405 

-

HTTP_406_NOT_ACCEPTABLE = 406 

-

HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 

-

HTTP_408_REQUEST_TIMEOUT = 408 

-

HTTP_409_CONFLICT = 409 

-

HTTP_410_GONE = 410 

-

HTTP_411_LENGTH_REQUIRED = 411 

-

HTTP_412_PRECONDITION_FAILED = 412 

-

HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 

-

HTTP_414_REQUEST_URI_TOO_LONG = 414 

-

HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 

-

HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 

-

HTTP_417_EXPECTATION_FAILED = 417 

-

HTTP_428_PRECONDITION_REQUIRED = 428 

-

HTTP_429_TOO_MANY_REQUESTS = 429 

-

HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 

-

HTTP_500_INTERNAL_SERVER_ERROR = 500 

-

HTTP_501_NOT_IMPLEMENTED = 501 

-

HTTP_502_BAD_GATEWAY = 502 

-

HTTP_503_SERVICE_UNAVAILABLE = 503 

-

HTTP_504_GATEWAY_TIMEOUT = 504 

-

HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 

-

HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_throttling.html b/htmlcov/rest_framework_throttling.html deleted file mode 100644 index 778b0293b..000000000 --- a/htmlcov/rest_framework_throttling.html +++ /dev/null @@ -1,533 +0,0 @@ - - - - - - - - Coverage for rest_framework/throttling: 81% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

- -
-

""" 

-

Provides various throttling policies. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.core.cache import cache 

-

from django.core.exceptions import ImproperlyConfigured 

-

from rest_framework.settings import api_settings 

-

import time 

-

 

-

 

-

class BaseThrottle(object): 

-

    """ 

-

    Rate throttling of requests. 

-

    """ 

-

    def allow_request(self, request, view): 

-

        """ 

-

        Return `True` if the request should be allowed, `False` otherwise. 

-

        """ 

-

        raise NotImplementedError('.allow_request() must be overridden') 

-

 

-

    def wait(self): 

-

        """ 

-

        Optionally, return a recommended number of seconds to wait before 

-

        the next request. 

-

        """ 

-

        return None 

-

 

-

 

-

class SimpleRateThrottle(BaseThrottle): 

-

    """ 

-

    A simple cache implementation, that only requires `.get_cache_key()` 

-

    to be overridden. 

-

 

-

    The rate (requests / seconds) is set by a `throttle` attribute on the View 

-

    class.  The attribute is a string of the form 'number_of_requests/period'. 

-

 

-

    Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day') 

-

 

-

    Previous request information used for throttling is stored in the cache. 

-

    """ 

-

 

-

    timer = time.time 

-

    cache_format = 'throtte_%(scope)s_%(ident)s' 

-

    scope = None 

-

    THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES 

-

 

-

    def __init__(self): 

-

        if not getattr(self, 'rate', None): 

-

            self.rate = self.get_rate() 

-

        self.num_requests, self.duration = self.parse_rate(self.rate) 

-

 

-

    def get_cache_key(self, request, view): 

-

        """ 

-

        Should return a unique cache-key which can be used for throttling. 

-

        Must be overridden. 

-

 

-

        May return `None` if the request should not be throttled. 

-

        """ 

-

        raise NotImplementedError('.get_cache_key() must be overridden') 

-

 

-

    def get_rate(self): 

-

        """ 

-

        Determine the string representation of the allowed request rate. 

-

        """ 

-

        if not getattr(self, 'scope', None): 

-

            msg = ("You must set either `.scope` or `.rate` for '%s' throttle" % 

-

                   self.__class__.__name__) 

-

            raise ImproperlyConfigured(msg) 

-

 

-

        try: 

-

            return self.THROTTLE_RATES[self.scope] 

-

        except KeyError: 

-

            msg = "No default throttle rate set for '%s' scope" % self.scope 

-

            raise ImproperlyConfigured(msg) 

-

 

-

    def parse_rate(self, rate): 

-

        """ 

-

        Given the request rate string, return a two tuple of: 

-

        <allowed number of requests>, <period of time in seconds> 

-

        """ 

-

        if rate is None: 

-

            return (None, None) 

-

        num, period = rate.split('/') 

-

        num_requests = int(num) 

-

        duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] 

-

        return (num_requests, duration) 

-

 

-

    def allow_request(self, request, view): 

-

        """ 

-

        Implement the check to see if the request should be throttled. 

-

 

-

        On success calls `throttle_success`. 

-

        On failure calls `throttle_failure`. 

-

        """ 

-

        if self.rate is None: 

-

            return True 

-

 

-

        self.key = self.get_cache_key(request, view) 

-

        self.history = cache.get(self.key, []) 

-

        self.now = self.timer() 

-

 

-

        # Drop any requests from the history which have now passed the 

-

        # throttle duration 

-

        while self.history and self.history[-1] <= self.now - self.duration: 

-

            self.history.pop() 

-

        if len(self.history) >= self.num_requests: 

-

            return self.throttle_failure() 

-

        return self.throttle_success() 

-

 

-

    def throttle_success(self): 

-

        """ 

-

        Inserts the current request's timestamp along with the key 

-

        into the cache. 

-

        """ 

-

        self.history.insert(0, self.now) 

-

        cache.set(self.key, self.history, self.duration) 

-

        return True 

-

 

-

    def throttle_failure(self): 

-

        """ 

-

        Called when a request to the API has failed due to throttling. 

-

        """ 

-

        return False 

-

 

-

    def wait(self): 

-

        """ 

-

        Returns the recommended next request time in seconds. 

-

        """ 

-

        if self.history: 

-

            remaining_duration = self.duration - (self.now - self.history[-1]) 

-

        else: 

-

            remaining_duration = self.duration 

-

 

-

        available_requests = self.num_requests - len(self.history) + 1 

-

 

-

        return remaining_duration / float(available_requests) 

-

 

-

 

-

class AnonRateThrottle(SimpleRateThrottle): 

-

    """ 

-

    Limits the rate of API calls that may be made by a anonymous users. 

-

 

-

    The IP address of the request will be used as the unique cache key. 

-

    """ 

-

    scope = 'anon' 

-

 

-

    def get_cache_key(self, request, view): 

-

        if request.user.is_authenticated(): 

-

            return None  # Only throttle unauthenticated requests. 

-

 

-

        ident = request.META.get('REMOTE_ADDR', None) 

-

 

-

        return self.cache_format % { 

-

            'scope': self.scope, 

-

            'ident': ident 

-

        } 

-

 

-

 

-

class UserRateThrottle(SimpleRateThrottle): 

-

    """ 

-

    Limits the rate of API calls that may be made by a given user. 

-

 

-

    The user id will be used as a unique cache key if the user is 

-

    authenticated.  For anonymous requests, the IP address of the request will 

-

    be used. 

-

    """ 

-

    scope = 'user' 

-

 

-

    def get_cache_key(self, request, view): 

-

        if request.user.is_authenticated(): 

-

            ident = request.user.id 

-

        else: 

-

            ident = request.META.get('REMOTE_ADDR', None) 

-

 

-

        return self.cache_format % { 

-

            'scope': self.scope, 

-

            'ident': ident 

-

        } 

-

 

-

 

-

class ScopedRateThrottle(SimpleRateThrottle): 

-

    """ 

-

    Limits the rate of API calls by different amounts for various parts of 

-

    the API.  Any view that has the `throttle_scope` property set will be 

-

    throttled.  The unique cache key will be generated by concatenating the 

-

    user id of the request, and the scope of the view being accessed. 

-

    """ 

-

    scope_attr = 'throttle_scope' 

-

 

-

    def __init__(self): 

-

        # Override the usual SimpleRateThrottle, because we can't determine 

-

        # the rate until called by the view. 

-

        pass 

-

 

-

    def allow_request(self, request, view): 

-

        # We can only determine the scope once we're called by the view. 

-

        self.scope = getattr(view, self.scope_attr, None) 

-

 

-

        # If a view does not have a `throttle_scope` always allow the request 

-

        if not self.scope: 

-

            return True 

-

 

-

        # Determine the allowed request rate as we normally would during 

-

        # the `__init__` call. 

-

        self.rate = self.get_rate() 

-

        self.num_requests, self.duration = self.parse_rate(self.rate) 

-

 

-

        # We can now proceed as normal. 

-

        return super(ScopedRateThrottle, self).allow_request(request, view) 

-

 

-

    def get_cache_key(self, request, view): 

-

        """ 

-

        If `view.throttle_scope` is not set, don't apply this throttle. 

-

 

-

        Otherwise generate the unique cache key by concatenating the user id 

-

        with the '.throttle_scope` property of the view. 

-

        """ 

-

        if request.user.is_authenticated(): 

-

            ident = request.user.id 

-

        else: 

-

            ident = request.META.get('REMOTE_ADDR', None) 

-

 

-

        return self.cache_format % { 

-

            'scope': self.scope, 

-

            'ident': ident 

-

        } 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_urlpatterns.html b/htmlcov/rest_framework_urlpatterns.html deleted file mode 100644 index 4c824a770..000000000 --- a/htmlcov/rest_framework_urlpatterns.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - Coverage for rest_framework/urlpatterns: 87% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

- -
-

from __future__ import unicode_literals 

-

from django.core.urlresolvers import RegexURLResolver 

-

from rest_framework.compat import url, include 

-

from rest_framework.settings import api_settings 

-

 

-

 

-

def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): 

-

    ret = [] 

-

    for urlpattern in urlpatterns: 

-

        if isinstance(urlpattern, RegexURLResolver): 

-

            # Set of included URL patterns 

-

            regex = urlpattern.regex.pattern 

-

            namespace = urlpattern.namespace 

-

            app_name = urlpattern.app_name 

-

            kwargs = urlpattern.default_kwargs 

-

            # Add in the included patterns, after applying the suffixes 

-

            patterns = apply_suffix_patterns(urlpattern.url_patterns, 

-

                                             suffix_pattern, 

-

                                             suffix_required) 

-

            ret.append(url(regex, include(patterns, namespace, app_name), kwargs)) 

-

 

-

        else: 

-

            # Regular URL pattern 

-

            regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern 

-

            view = urlpattern._callback or urlpattern._callback_str 

-

            kwargs = urlpattern.default_args 

-

            name = urlpattern.name 

-

            # Add in both the existing and the new urlpattern 

-

            if not suffix_required: 

-

                ret.append(urlpattern) 

-

            ret.append(url(regex, view, kwargs, name)) 

-

 

-

    return ret 

-

 

-

 

-

def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): 

-

    """ 

-

    Supplement existing urlpatterns with corresponding patterns that also 

-

    include a '.format' suffix.  Retains urlpattern ordering. 

-

 

-

    urlpatterns: 

-

        A list of URL patterns. 

-

 

-

    suffix_required: 

-

        If `True`, only suffixed URLs will be generated, and non-suffixed 

-

        URLs will not be used.  Defaults to `False`. 

-

 

-

    allowed: 

-

        An optional tuple/list of allowed suffixes.  eg ['json', 'api'] 

-

        Defaults to `None`, which allows any suffix. 

-

    """ 

-

    suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG 

-

    if allowed: 

-

        if len(allowed) == 1: 

-

            allowed_pattern = allowed[0] 

-

        else: 

-

            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 

-

 

-

    return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_urls.html b/htmlcov/rest_framework_urls.html deleted file mode 100644 index 7720a6d40..000000000 --- a/htmlcov/rest_framework_urls.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - Coverage for rest_framework/urls: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

- -
-

""" 

-

Login and logout views for the browsable API. 

-

 

-

Add these to your root URLconf if you're using the browsable API and 

-

your API requires authentication. 

-

 

-

The urls must be namespaced as 'rest_framework', and you should make sure 

-

your authentication settings include `SessionAuthentication`. 

-

 

-

    urlpatterns = patterns('', 

-

        ... 

-

        url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) 

-

    ) 

-

""" 

-

from __future__ import unicode_literals 

-

from rest_framework.compat import patterns, url 

-

 

-

 

-

template_name = {'template_name': 'rest_framework/login.html'} 

-

 

-

urlpatterns = patterns('django.contrib.auth.views', 

-

    url(r'^login/$', 'login', template_name, name='login'), 

-

    url(r'^logout/$', 'logout', template_name, name='logout'), 

-

) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_utils___init__.html b/htmlcov/rest_framework_utils___init__.html deleted file mode 100644 index 99eb18c4f..000000000 --- a/htmlcov/rest_framework_utils___init__.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - Coverage for rest_framework/utils/__init__: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
- - - -
-
- - - - - diff --git a/htmlcov/rest_framework_utils_breadcrumbs.html b/htmlcov/rest_framework_utils_breadcrumbs.html deleted file mode 100644 index 14fb8955d..000000000 --- a/htmlcov/rest_framework_utils_breadcrumbs.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - Coverage for rest_framework/utils/breadcrumbs: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

- -
-

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): 

-

    """ 

-

    Given a url returns a list of breadcrumbs, which are each a 

-

    tuple of (name, url). 

-

    """ 

-

 

-

    from rest_framework.views import APIView 

-

 

-

    def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): 

-

        """ 

-

        Add tuples of (name, url) to the breadcrumbs list, 

-

        progressively chomping off parts of the url. 

-

        """ 

-

 

-

        try: 

-

            (view, unused_args, unused_kwargs) = resolve(url) 

-

        except Exception: 

-

            pass 

-

        else: 

-

            # Check if this is a REST framework view, 

-

            # and if so add it to the breadcrumbs 

-

            cls = getattr(view, 'cls', None) 

-

            if cls is not None and issubclass(cls, APIView): 

-

                # 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) 

-

                    breadcrumbs_list.insert(0, (name, prefix + url)) 

-

                    seen.append(view) 

-

 

-

        if url == '': 

-

            # All done 

-

            return breadcrumbs_list 

-

 

-

        elif url.endswith('/'): 

-

            # Drop trailing slash off the end and continue to try to 

-

            # resolve more breadcrumbs 

-

            url = url.rstrip('/') 

-

            return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen) 

-

 

-

        # Drop trailing non-slash off the end and continue to try to 

-

        # resolve more breadcrumbs 

-

        url = url[:url.rfind('/') + 1] 

-

        return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen) 

-

 

-

    prefix = get_script_prefix().rstrip('/') 

-

    url = url[len(prefix):] 

-

    return breadcrumbs_recursive(url, [], prefix, []) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_utils_encoders.html b/htmlcov/rest_framework_utils_encoders.html deleted file mode 100644 index 9f0ca343a..000000000 --- a/htmlcov/rest_framework_utils_encoders.html +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - Coverage for rest_framework/utils/encoders: 73% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

- -
-

""" 

-

Helper classes for parsers. 

-

""" 

-

from __future__ import unicode_literals 

-

from django.utils.datastructures import SortedDict 

-

from django.utils.functional import Promise 

-

from rest_framework.compat import timezone, force_text 

-

from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata 

-

import datetime 

-

import decimal 

-

import types 

-

import json 

-

 

-

 

-

class JSONEncoder(json.JSONEncoder): 

-

    """ 

-

    JSONEncoder subclass that knows how to encode date/time/timedelta, 

-

    decimal types, and generators. 

-

    """ 

-

    def default(self, o): 

-

        # For Date Time string spec, see ECMA 262 

-

        # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 

-

        if isinstance(o, Promise): 

-

            return force_text(o) 

-

        elif isinstance(o, datetime.datetime): 

-

            r = o.isoformat() 

-

            if o.microsecond: 

-

                r = r[:23] + r[26:] 

-

            if r.endswith('+00:00'): 

-

                r = r[:-6] + 'Z' 

-

            return r 

-

        elif isinstance(o, datetime.date): 

-

            return o.isoformat() 

-

        elif isinstance(o, datetime.time): 

-

            if timezone and timezone.is_aware(o): 

-

                raise ValueError("JSON can't represent timezone-aware times.") 

-

            r = o.isoformat() 

-

            if o.microsecond: 

-

                r = r[:12] 

-

            return r 

-

        elif isinstance(o, datetime.timedelta): 

-

            return str(o.total_seconds()) 

-

        elif isinstance(o, decimal.Decimal): 

-

            return str(o) 

-

        elif hasattr(o, '__iter__'): 

-

            return [i for i in o] 

-

        return super(JSONEncoder, self).default(o) 

-

 

-

 

-

try: 

-

    import yaml 

-

except ImportError: 

-

    SafeDumper = None 

-

else: 

-

    # Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py 

-

    class SafeDumper(yaml.SafeDumper): 

-

        """ 

-

        Handles decimals as strings. 

-

        Handles SortedDicts as usual dicts, but preserves field order, rather 

-

        than the usual behaviour of sorting the keys. 

-

        """ 

-

        def represent_decimal(self, data): 

-

            return self.represent_scalar('tag:yaml.org,2002:str', str(data)) 

-

 

-

        def represent_mapping(self, tag, mapping, flow_style=None): 

-

            value = [] 

-

            node = yaml.MappingNode(tag, value, flow_style=flow_style) 

-

            if self.alias_key is not None: 

-

                self.represented_objects[self.alias_key] = node 

-

            best_style = True 

-

            if hasattr(mapping, 'items'): 

-

                mapping = list(mapping.items()) 

-

                if not isinstance(mapping, SortedDict): 

-

                    mapping.sort() 

-

            for item_key, item_value in mapping: 

-

                node_key = self.represent_data(item_key) 

-

                node_value = self.represent_data(item_value) 

-

                if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): 

-

                    best_style = False 

-

                if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): 

-

                    best_style = False 

-

                value.append((node_key, node_value)) 

-

            if flow_style is None: 

-

                if self.default_flow_style is not None: 

-

                    node.flow_style = self.default_flow_style 

-

                else: 

-

                    node.flow_style = best_style 

-

            return node 

-

 

-

    SafeDumper.add_representer(SortedDict, 

-

            yaml.representer.SafeRepresenter.represent_dict) 

-

    SafeDumper.add_representer(DictWithMetadata, 

-

            yaml.representer.SafeRepresenter.represent_dict) 

-

    SafeDumper.add_representer(SortedDictWithMetadata, 

-

            yaml.representer.SafeRepresenter.represent_dict) 

-

    SafeDumper.add_representer(types.GeneratorType, 

-

            yaml.representer.SafeRepresenter.represent_list) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_utils_formatting.html b/htmlcov/rest_framework_utils_formatting.html deleted file mode 100644 index 54e1570f7..000000000 --- a/htmlcov/rest_framework_utils_formatting.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - - - Coverage for rest_framework/utils/formatting: 97% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

- -
-

""" 

-

Utility functions to return a formatted name and description for a given view. 

-

""" 

-

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 

-

import re 

-

 

-

 

-

def _remove_trailing_string(content, trailing): 

-

    """ 

-

    Strip trailing component `trailing` from `content` if it exists. 

-

    Used when generating names from view classes. 

-

    """ 

-

    if content.endswith(trailing) and content != trailing: 

-

        return content[:-len(trailing)] 

-

    return content 

-

 

-

 

-

def _remove_leading_indent(content): 

-

    """ 

-

    Remove leading indent from a block of text. 

-

    Used when generating descriptions from docstrings. 

-

    """ 

-

    whitespace_counts = [len(line) - len(line.lstrip(' ')) 

-

                         for line in content.splitlines()[1:] if line.lstrip()] 

-

 

-

    # unindent the content if needed 

-

    if whitespace_counts: 

-

        whitespace_pattern = '^' + (' ' * min(whitespace_counts)) 

-

        content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) 

-

    content = content.strip('\n') 

-

    return content 

-

 

-

 

-

def _camelcase_to_spaces(content): 

-

    """ 

-

    Translate 'CamelCaseNames' to 'Camel Case Names'. 

-

    Used when generating names from view classes. 

-

    """ 

-

    camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' 

-

    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. 

-

    """ 

-

    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 get_view_description(cls, html=False): 

-

    """ 

-

    Return a description for an `APIView` class or `@api_view` function. 

-

    """ 

-

    description = cls.__doc__ or '' 

-

    description = _remove_leading_indent(description) 

-

    if html: 

-

        return markup_description(description) 

-

    return description 

-

 

-

 

-

def markup_description(description): 

-

    """ 

-

    Apply HTML markup to the given description. 

-

    """ 

-

    if apply_markdown: 

-

        description = apply_markdown(description) 

-

    else: 

-

        description = escape(description).replace('\n', '<br />') 

-

    return mark_safe(description) 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_utils_mediatypes.html b/htmlcov/rest_framework_utils_mediatypes.html deleted file mode 100644 index 2ce44ab59..000000000 --- a/htmlcov/rest_framework_utils_mediatypes.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - Coverage for rest_framework/utils/mediatypes: 77% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

- -
-

""" 

-

Handling of media types, as found in HTTP Content-Type and Accept headers. 

-

 

-

See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 

-

""" 

-

from __future__ import unicode_literals 

-

from django.http.multipartparser import parse_header 

-

from rest_framework import HTTP_HEADER_ENCODING 

-

 

-

 

-

def media_type_matches(lhs, rhs): 

-

    """ 

-

    Returns ``True`` if the media type in the first argument <= the 

-

    media type in the second argument.  The media types are strings 

-

    as described by the HTTP spec. 

-

 

-

    Valid media type strings include: 

-

 

-

    'application/json; indent=4' 

-

    'application/json' 

-

    'text/*' 

-

    '*/*' 

-

    """ 

-

    lhs = _MediaType(lhs) 

-

    rhs = _MediaType(rhs) 

-

    return lhs.match(rhs) 

-

 

-

 

-

def order_by_precedence(media_type_lst): 

-

    """ 

-

    Returns a list of sets of media type strings, ordered by precedence. 

-

    Precedence is determined by how specific a media type is: 

-

 

-

    3. 'type/subtype; param=val' 

-

    2. 'type/subtype' 

-

    1. 'type/*' 

-

    0. '*/*' 

-

    """ 

-

    ret = [set(), set(), set(), set()] 

-

    for media_type in media_type_lst: 

-

        precedence = _MediaType(media_type).precedence 

-

        ret[3 - precedence].add(media_type) 

-

    return [media_types for media_types in ret if media_types] 

-

 

-

 

-

class _MediaType(object): 

-

    def __init__(self, media_type_str): 

-

        if media_type_str is None: 

-

            media_type_str = '' 

-

        self.orig = media_type_str 

-

        self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING)) 

-

        self.main_type, sep, self.sub_type = self.full_type.partition('/') 

-

 

-

    def match(self, other): 

-

        """Return true if this MediaType satisfies the given MediaType.""" 

-

        for key in self.params.keys(): 

-

            if key != 'q' and other.params.get(key, None) != self.params.get(key, None): 

-

                return False 

-

 

-

        if self.sub_type != '*' and other.sub_type != '*'  and other.sub_type != self.sub_type: 

-

            return False 

-

 

-

        if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: 

-

            return False 

-

 

-

        return True 

-

 

-

    @property 

-

    def precedence(self): 

-

        """ 

-

        Return a precedence level from 0-3 for the media type given how specific it is. 

-

        """ 

-

        if self.main_type == '*': 

-

            return 0 

-

        elif self.sub_type == '*': 

-

            return 1 

-

        elif not self.params or self.params.keys() == ['q']: 

-

            return 2 

-

        return 3 

-

 

-

    def __str__(self): 

-

        return unicode(self).encode('utf-8') 

-

 

-

    def __unicode__(self): 

-

        ret = "%s/%s" % (self.main_type, self.sub_type) 

-

        for key, val in self.params.items(): 

-

            ret += "; %s=%s" % (key, val) 

-

        return ret 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_views.html b/htmlcov/rest_framework_views.html deleted file mode 100644 index f836e71fb..000000000 --- a/htmlcov/rest_framework_views.html +++ /dev/null @@ -1,793 +0,0 @@ - - - - - - - - Coverage for rest_framework/views: 100% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

- -
-

""" 

-

Provides an APIView class that is the base of all views in REST framework. 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from django.core.exceptions import PermissionDenied 

-

from django.http import Http404, HttpResponse 

-

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 

-

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): 

-

    settings = api_settings 

-

 

-

    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES 

-

    parser_classes = api_settings.DEFAULT_PARSER_CLASSES 

-

    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 

-

    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES 

-

    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES 

-

    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS 

-

 

-

    @classmethod 

-

    def as_view(cls, **initkwargs): 

-

        """ 

-

        Store the original class on the view function. 

-

 

-

        This allows us to discover information about the view when we do URL 

-

        reverse lookups.  Used for breadcrumb generation. 

-

        """ 

-

        view = super(APIView, cls).as_view(**initkwargs) 

-

        view.cls = cls 

-

        return view 

-

 

-

    @property 

-

    def allowed_methods(self): 

-

        """ 

-

        Wrap Django's private `_allowed_methods` interface in a public property. 

-

        """ 

-

        return self._allowed_methods() 

-

 

-

    @property 

-

    def default_response_headers(self): 

-

        # TODO: deprecate? 

-

        # TODO: Only vary by accept if multiple renderers 

-

        return { 

-

            'Allow': ', '.join(self.allowed_methods), 

-

            'Vary': 'Accept' 

-

        } 

-

 

-

    def http_method_not_allowed(self, request, *args, **kwargs): 

-

        """ 

-

        If `request.method` does not correspond to a handler method, 

-

        determine what kind of exception to raise. 

-

        """ 

-

        raise exceptions.MethodNotAllowed(request.method) 

-

 

-

    def permission_denied(self, request): 

-

        """ 

-

        If request is not permitted, determine what kind of exception to raise. 

-

        """ 

-

        if not self.request.successful_authenticator: 

-

            raise exceptions.NotAuthenticated() 

-

        raise exceptions.PermissionDenied() 

-

 

-

    def throttled(self, request, wait): 

-

        """ 

-

        If request is throttled, determine what kind of exception to raise. 

-

        """ 

-

        raise exceptions.Throttled(wait) 

-

 

-

    def get_authenticate_header(self, request): 

-

        """ 

-

        If a request is unauthenticated, determine the WWW-Authenticate 

-

        header to use for 401 responses, if any. 

-

        """ 

-

        authenticators = self.get_authenticators() 

-

        if authenticators: 

-

            return authenticators[0].authenticate_header(request) 

-

 

-

    def get_parser_context(self, http_request): 

-

        """ 

-

        Returns a dict that is passed through to Parser.parse(), 

-

        as the `parser_context` keyword argument. 

-

        """ 

-

        # Note: Additionally `request` will also be added to the context 

-

        #       by the Request object. 

-

        return { 

-

            'view': self, 

-

            'args': getattr(self, 'args', ()), 

-

            'kwargs': getattr(self, 'kwargs', {}) 

-

        } 

-

 

-

    def get_renderer_context(self): 

-

        """ 

-

        Returns a dict that is passed through to Renderer.render(), 

-

        as the `renderer_context` keyword argument. 

-

        """ 

-

        # Note: Additionally 'response' will also be added to the context, 

-

        #       by the Response object. 

-

        return { 

-

            'view': self, 

-

            'args': getattr(self, 'args', ()), 

-

            'kwargs': getattr(self, 'kwargs', {}), 

-

            'request': getattr(self, 'request', None) 

-

        } 

-

 

-

    # API policy instantiation methods 

-

 

-

    def get_format_suffix(self, **kwargs): 

-

        """ 

-

        Determine if the request includes a '.json' style format suffix 

-

        """ 

-

        if self.settings.FORMAT_SUFFIX_KWARG: 

-

            return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) 

-

 

-

    def get_renderers(self): 

-

        """ 

-

        Instantiates and returns the list of renderers that this view can use. 

-

        """ 

-

        return [renderer() for renderer in self.renderer_classes] 

-

 

-

    def get_parsers(self): 

-

        """ 

-

        Instantiates and returns the list of parsers that this view can use. 

-

        """ 

-

        return [parser() for parser in self.parser_classes] 

-

 

-

    def get_authenticators(self): 

-

        """ 

-

        Instantiates and returns the list of authenticators that this view can use. 

-

        """ 

-

        return [auth() for auth in self.authentication_classes] 

-

 

-

    def get_permissions(self): 

-

        """ 

-

        Instantiates and returns the list of permissions that this view requires. 

-

        """ 

-

        return [permission() for permission in self.permission_classes] 

-

 

-

    def get_throttles(self): 

-

        """ 

-

        Instantiates and returns the list of throttles that this view uses. 

-

        """ 

-

        return [throttle() for throttle in self.throttle_classes] 

-

 

-

    def get_content_negotiator(self): 

-

        """ 

-

        Instantiate and return the content negotiation class to use. 

-

        """ 

-

        if not getattr(self, '_negotiator', None): 

-

            self._negotiator = self.content_negotiation_class() 

-

        return self._negotiator 

-

 

-

    # API policy implementation methods 

-

 

-

    def perform_content_negotiation(self, request, force=False): 

-

        """ 

-

        Determine which renderer and media type to use render the response. 

-

        """ 

-

        renderers = self.get_renderers() 

-

        conneg = self.get_content_negotiator() 

-

 

-

        try: 

-

            return conneg.select_renderer(request, renderers, self.format_kwarg) 

-

        except Exception: 

-

            if force: 

-

                return (renderers[0], renderers[0].media_type) 

-

            raise 

-

 

-

    def perform_authentication(self, request): 

-

        """ 

-

        Perform authentication on the incoming request. 

-

 

-

        Note that if you override this and simply 'pass', then authentication 

-

        will instead be performed lazily, the first time either 

-

        `request.user` or `request.auth` is accessed. 

-

        """ 

-

        request.user 

-

 

-

    def check_permissions(self, request): 

-

        """ 

-

        Check if the request should be permitted. 

-

        Raises an appropriate exception if the request is not permitted. 

-

        """ 

-

        for permission in self.get_permissions(): 

-

            if not permission.has_permission(request, self): 

-

                self.permission_denied(request) 

-

 

-

    def check_object_permissions(self, request, obj): 

-

        """ 

-

        Check if the request should be permitted for a given object. 

-

        Raises an appropriate exception if the request is not permitted. 

-

        """ 

-

        for permission in self.get_permissions(): 

-

            if not permission.has_object_permission(request, self, obj): 

-

                self.permission_denied(request) 

-

 

-

    def check_throttles(self, request): 

-

        """ 

-

        Check if request should be throttled. 

-

        Raises an appropriate exception if the request is throttled. 

-

        """ 

-

        for throttle in self.get_throttles(): 

-

            if not throttle.allow_request(request, self): 

-

                self.throttled(request, throttle.wait()) 

-

 

-

    # Dispatch methods 

-

 

-

    def initialize_request(self, request, *args, **kargs): 

-

        """ 

-

        Returns the initial request object. 

-

        """ 

-

        parser_context = self.get_parser_context(request) 

-

 

-

        return Request(request, 

-

                       parsers=self.get_parsers(), 

-

                       authenticators=self.get_authenticators(), 

-

                       negotiator=self.get_content_negotiator(), 

-

                       parser_context=parser_context) 

-

 

-

    def initial(self, request, *args, **kwargs): 

-

        """ 

-

        Runs anything that needs to occur prior to calling the method handler. 

-

        """ 

-

        self.format_kwarg = self.get_format_suffix(**kwargs) 

-

 

-

        # Ensure that the incoming request is permitted 

-

        self.perform_authentication(request) 

-

        self.check_permissions(request) 

-

        self.check_throttles(request) 

-

 

-

        # Perform content negotiation and store the accepted info on the request 

-

        neg = self.perform_content_negotiation(request) 

-

        request.accepted_renderer, request.accepted_media_type = neg 

-

 

-

    def finalize_response(self, request, response, *args, **kwargs): 

-

        """ 

-

        Returns the final response object. 

-

        """ 

-

        # Make the error obvious if a proper response is not returned 

-

        assert isinstance(response, HttpResponse), ( 

-

            'Expected a `Response` to be returned from the view, ' 

-

            'but received a `%s`' % type(response) 

-

        ) 

-

 

-

        if isinstance(response, Response): 

-

            if not getattr(request, 'accepted_renderer', None): 

-

                neg = self.perform_content_negotiation(request, force=True) 

-

                request.accepted_renderer, request.accepted_media_type = neg 

-

 

-

            response.accepted_renderer = request.accepted_renderer 

-

            response.accepted_media_type = request.accepted_media_type 

-

            response.renderer_context = self.get_renderer_context() 

-

 

-

        for key, value in self.headers.items(): 

-

            response[key] = value 

-

 

-

        return response 

-

 

-

    def handle_exception(self, exc): 

-

        """ 

-

        Handle any exception that occurs, by returning an appropriate response, 

-

        or re-raising the error. 

-

        """ 

-

        if isinstance(exc, exceptions.Throttled): 

-

            # 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 

-

            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 

-

 

-

    # Note: session based authentication is explicitly CSRF validated, 

-

    # all other authentication is CSRF exempt. 

-

    @csrf_exempt 

-

    def dispatch(self, request, *args, **kwargs): 

-

        """ 

-

        `.dispatch()` is pretty much the same as Django's regular dispatch, 

-

        but with extra hooks for startup, finalize, and exception handling. 

-

        """ 

-

        self.args = args 

-

        self.kwargs = kwargs 

-

        request = self.initialize_request(request, *args, **kwargs) 

-

        self.request = request 

-

        self.headers = self.default_response_headers  # deprecate? 

-

 

-

        try: 

-

            self.initial(request, *args, **kwargs) 

-

 

-

            # Get the appropriate handler method 

-

            if request.method.lower() in self.http_method_names: 

-

                handler = getattr(self, request.method.lower(), 

-

                                  self.http_method_not_allowed) 

-

            else: 

-

                handler = self.http_method_not_allowed 

-

 

-

            response = handler(request, *args, **kwargs) 

-

 

-

        except Exception as exc: 

-

            response = self.handle_exception(exc) 

-

 

-

        self.response = self.finalize_response(request, response, *args, **kwargs) 

-

        return self.response 

-

 

-

    def options(self, request, *args, **kwargs): 

-

        """ 

-

        Handler method for HTTP 'OPTIONS' request. 

-

        We may as well implement this as Django will otherwise provide 

-

        a less useful default implementation. 

-

        """ 

-

        return Response(self.metadata(request), status=status.HTTP_200_OK) 

-

 

-

    def metadata(self, request): 

-

        """ 

-

        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['renders'] = [renderer.media_type for renderer in self.renderer_classes] 

-

        ret['parses'] = [parser.media_type for parser in self.parser_classes] 

-

        return ret 

- -
-
- - - - - diff --git a/htmlcov/rest_framework_viewsets.html b/htmlcov/rest_framework_viewsets.html deleted file mode 100644 index 8264ddc0c..000000000 --- a/htmlcov/rest_framework_viewsets.html +++ /dev/null @@ -1,359 +0,0 @@ - - - - - - - - Coverage for rest_framework/viewsets: 95% - - - - - - - - - - - -
- -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

- -
-

""" 

-

ViewSets are essentially just a type of class based view, that doesn't provide 

-

any method handlers, such as `get()`, `post()`, etc... but instead has actions, 

-

such as `list()`, `retrieve()`, `create()`, etc... 

-

 

-

Actions are only bound to methods at the point of instantiating the views. 

-

 

-

    user_list = UserViewSet.as_view({'get': 'list'}) 

-

    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 

-

automatically. 

-

 

-

    router = DefaultRouter() 

-

    router.register(r'users', UserViewSet, 'user') 

-

    urlpatterns = router.urls 

-

""" 

-

from __future__ import unicode_literals 

-

 

-

from functools import update_wrapper 

-

from django.utils.decorators import classonlymethod 

-

from rest_framework import views, generics, mixins 

-

 

-

 

-

class ViewSetMixin(object): 

-

    """ 

-

    This is the magic. 

-

 

-

    Overrides `.as_view()` so that it takes an `actions` keyword that performs 

-

    the binding of HTTP methods to actions on the Resource. 

-

 

-

    For example, to create a concrete view binding the 'GET' and 'POST' methods 

-

    to the 'list' and 'create' actions... 

-

 

-

    view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) 

-

    """ 

-

 

-

    @classonlymethod 

-

    def as_view(cls, actions=None, **initkwargs): 

-

        """ 

-

        Because of the way class based views create a closure around the 

-

        instantiated view, we need to totally reimplement `.as_view`, 

-

        and slightly modify the view function that is created and returned. 

-

        """ 

-

        # The suffix initkwarg is reserved for identifing the viewset type 

-

        # eg. 'List' or 'Instance'. 

-

        cls.suffix = None 

-

 

-

        # 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) 

-

            # We also store the mapping of request methods to actions, 

-

            # so that we can later set the action attribute. 

-

            # eg. `self.action = 'list'` on an incoming GET request. 

-

            self.action_map = actions 

-

 

-

            # Bind methods to actions 

-

            # This is the bit that's different to a standard view 

-

            for method, action in actions.items(): 

-

                handler = getattr(self, action) 

-

                setattr(self, method, handler) 

-

 

-

            # Patch this in as it's otherwise only present from 1.5 onwards 

-

            if hasattr(self, 'get') and not hasattr(self, 'head'): 

-

                self.head = self.get 

-

 

-

            # And continue as usual 

-

            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=()) 

-

 

-

        # We need to set these on the view function, so that breadcrumb 

-

        # generation can pick out these bits of information from a 

-

        # resolved URL. 

-

        view.cls = cls 

-

        view.suffix = initkwargs.get('suffix', None) 

-

        return view 

-

 

-

    def initialize_request(self, request, *args, **kargs): 

-

        """ 

-

        Set the `.action` attribute on the view, 

-

        depending on the request method. 

-

        """ 

-

        request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs) 

-

        self.action = self.action_map.get(request.method.lower()) 

-

        return request 

-

 

-

 

-

class ViewSet(ViewSetMixin, views.APIView): 

-

    """ 

-

    The base ViewSet class does not provide any actions by default. 

-

    """ 

-

    pass 

-

 

-

 

-

class GenericViewSet(ViewSetMixin, generics.GenericAPIView): 

-

    """ 

-

    The GenericViewSet class does not provide any actions by default, 

-

    but does include the base set of generic view behavior, such as 

-

    the `get_object` and `get_queryset` methods. 

-

    """ 

-

    pass 

-

 

-

 

-

class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, 

-

                           mixins.ListModelMixin, 

-

                           GenericViewSet): 

-

    """ 

-

    A viewset that provides default `list()` and `retrieve()` actions. 

-

    """ 

-

    pass 

-

 

-

 

-

class ModelViewSet(mixins.CreateModelMixin, 

-

                    mixins.RetrieveModelMixin, 

-

                    mixins.UpdateModelMixin, 

-

                    mixins.DestroyModelMixin, 

-

                    mixins.ListModelMixin, 

-

                    GenericViewSet): 

-

    """ 

-

    A viewset that provides default `create()`, `retrieve()`, `update()`, 

-

    `partial_update()`, `destroy()` and `list()` actions. 

-

    """ 

-

    pass 

- -
-
- - - - - diff --git a/htmlcov/status.dat b/htmlcov/status.dat deleted file mode 100644 index 9e448e377..000000000 --- a/htmlcov/status.dat +++ /dev/null @@ -1,1258 +0,0 @@ -(dp1 -S'files' -p2 -(dp3 -S'rest_framework_utils_encoders' -p4 -(dp5 -S'index' -p6 -(dp7 -S'par' -p8 -I0 -sS'html_filename' -p9 -S'rest_framework_utils_encoders.html' -p10 -sS'name' -p11 -S'rest_framework/utils/encoders' -p12 -sS'nums' -p13 -ccopy_reg -_reconstructor -p14 -(ccoverage.results -Numbers -p15 -c__builtin__ -object -p16 -NtRp17 -(dp18 -S'n_files' -p19 -I1 -sS'n_branches' -p20 -I0 -sS'n_statements' -p21 -I70 -sS'n_excluded' -p22 -I0 -sS'n_missing' -p23 -I19 -sS'n_missing_branches' -p24 -I0 -sbssS'hash' -p25 -S'\x9d|\xea|-p\x0c#\xef\x82\x8c\x91\xf1\xcd}f' -p26 -ssS'rest_framework___init__' -p27 -(dp28 -g6 -(dp29 -g8 -I0 -sg9 -S'rest_framework___init__.html' -p30 -sg11 -S'rest_framework/__init__' -p31 -sg13 -g14 -(g15 -g16 -NtRp32 -(dp33 -g19 -I1 -sg20 -I0 -sg21 -I4 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\xbc\xf1f\xd9[\xd4\xcf\x9cQ\x94H\xfd3)\xea[' -p34 -ssS'rest_framework_urlpatterns' -p35 -(dp36 -g6 -(dp37 -g8 -I0 -sg9 -S'rest_framework_urlpatterns.html' -p38 -sg11 -S'rest_framework/urlpatterns' -p39 -sg13 -g14 -(g15 -g16 -NtRp40 -(dp41 -g19 -I1 -sg20 -I0 -sg21 -I31 -sg22 -I0 -sg23 -I4 -sg24 -I0 -sbssg25 -S"\x84\xb0-\xc6Y\x7f\xebA'\x8c5+\xcf\xf6\xcf\xda" -p42 -ssS'rest_framework_permissions' -p43 -(dp44 -g6 -(dp45 -g8 -I0 -sg9 -S'rest_framework_permissions.html' -p46 -sg11 -S'rest_framework/permissions' -p47 -sg13 -g14 -(g15 -g16 -NtRp48 -(dp49 -g19 -I1 -sg20 -I0 -sg21 -I63 -sg22 -I0 -sg23 -I12 -sg24 -I0 -sbssg25 -S",\xc4,\xda\x05\x86\x17\xe8u2~ls*'\xc1" -p50 -ssS'rest_framework_fields' -p51 -(dp52 -g6 -(dp53 -g8 -I0 -sg9 -S'rest_framework_fields.html' -p54 -sg11 -S'rest_framework/fields' -p55 -sg13 -g14 -(g15 -g16 -NtRp56 -(dp57 -g19 -I1 -sg20 -I0 -sg21 -I594 -sg22 -I0 -sg23 -I80 -sg24 -I0 -sbssg25 -S'\x08\x1b\xd2m\x91l\x14e\x97CDA\x1c&k\xf9' -p58 -ssS'rest_framework_models' -p59 -(dp60 -g6 -(dp61 -g8 -I0 -sg9 -S'rest_framework_models.html' -p62 -sg11 -S'rest_framework/models' -p63 -sg13 -g14 -(g15 -g16 -NtRp64 -(dp65 -g19 -I1 -sg20 -I0 -sg21 -I0 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S' E\xaf\xdd\xe7\xbb\xc4\x11z\xf8\x80\x18v.\xec\xf6' -p66 -ssS'rest_framework_utils_breadcrumbs' -p67 -(dp68 -g6 -(dp69 -g8 -I0 -sg9 -S'rest_framework_utils_breadcrumbs.html' -p70 -sg11 -S'rest_framework/utils/breadcrumbs' -p71 -sg13 -g14 -(g15 -g16 -NtRp72 -(dp73 -g19 -I1 -sg20 -I0 -sg21 -I27 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'V"\xf6\xbc\\m)\x12R4>c\xff\xea\xde\x8b' -p74 -ssS'rest_framework_urls' -p75 -(dp76 -g6 -(dp77 -g8 -I0 -sg9 -S'rest_framework_urls.html' -p78 -sg11 -S'rest_framework/urls' -p79 -sg13 -g14 -(g15 -g16 -NtRp80 -(dp81 -g19 -I1 -sg20 -I0 -sg21 -I4 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\xba\x9b\xdaeu\x17\x8b\xe0e\xc6-\xc5R\xba\xa2\xd5' -p82 -ssS'rest_framework_serializers' -p83 -(dp84 -g6 -(dp85 -g8 -I0 -sg9 -S'rest_framework_serializers.html' -p86 -sg11 -S'rest_framework/serializers' -p87 -sg13 -g14 -(g15 -g16 -NtRp88 -(dp89 -g19 -I1 -sg20 -I0 -sg21 -I464 -sg22 -I0 -sg23 -I27 -sg24 -I0 -sbssg25 -S'O\\\xf6\x81y\x95\xae\x9a)\xe9~\xb8\xab\t\x88#' -p90 -ssS'rest_framework_exceptions' -p91 -(dp92 -g6 -(dp93 -g8 -I0 -sg9 -S'rest_framework_exceptions.html' -p94 -sg11 -S'rest_framework/exceptions' -p95 -sg13 -g14 -(g15 -g16 -NtRp96 -(dp97 -g19 -I1 -sg20 -I0 -sg21 -I51 -sg22 -I0 -sg23 -I2 -sg24 -I0 -sbssg25 -S'\xdd\xcaE\x12\x1f4V\xe6\x91\x11\xef:T\xe1r\xca' -p98 -ssS'rest_framework_status' -p99 -(dp100 -g6 -(dp101 -g8 -I0 -sg9 -S'rest_framework_status.html' -p102 -sg11 -S'rest_framework/status' -p103 -sg13 -g14 -(g15 -g16 -NtRp104 -(dp105 -g19 -I1 -sg20 -I0 -sg21 -I46 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\x97z\xcd\xfd\xdc\x0c\xe3\xa9j\x04\xab\x13]\x98\xbf\x80' -p106 -ssS'rest_framework_relations' -p107 -(dp108 -g6 -(dp109 -g8 -I0 -sg9 -S'rest_framework_relations.html' -p110 -sg11 -S'rest_framework/relations' -p111 -sg13 -g14 -(g15 -g16 -NtRp112 -(dp113 -g19 -I1 -sg20 -I0 -sg21 -I365 -sg22 -I0 -sg23 -I88 -sg24 -I0 -sbssg25 -S'\xdb"\xfe\xc2\xb3\x8a\xe2(\xbeoNk\x1b\xd3H9' -p114 -ssS'rest_framework_authtoken_views' -p115 -(dp116 -g6 -(dp117 -g8 -I0 -sg9 -S'rest_framework_authtoken_views.html' -p118 -sg11 -S'rest_framework/authtoken/views' -p119 -sg13 -g14 -(g15 -g16 -NtRp120 -(dp121 -g19 -I1 -sg20 -I0 -sg21 -I21 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\xb8A\x13\xee\xfc9\x8b\x1eY-\xad\x00\xa7\x9dH]' -p122 -ssS'rest_framework_mixins' -p123 -(dp124 -g6 -(dp125 -g8 -I0 -sg9 -S'rest_framework_mixins.html' -p126 -sg11 -S'rest_framework/mixins' -p127 -sg13 -g14 -(g15 -g16 -NtRp128 -(dp129 -g19 -I1 -sg20 -I0 -sg21 -I97 -sg22 -I0 -sg23 -I7 -sg24 -I0 -sbssg25 -S'\xcd\xe5\x9f\xc2\xbb\xd9\xcb\x14*\x88\x99\xe8\xdf\xd2\xa8\xd6' -p130 -ssS'rest_framework_views' -p131 -(dp132 -g6 -(dp133 -g8 -I0 -sg9 -S'rest_framework_views.html' -p134 -sg11 -S'rest_framework/views' -p135 -sg13 -g14 -(g15 -g16 -NtRp136 -(dp137 -g19 -I1 -sg20 -I0 -sg21 -I146 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'ZBo\x84oh^\x1f\x8c\x94Mp$\xf3\xd2\xa1' -p138 -ssS'rest_framework_generics' -p139 -(dp140 -g6 -(dp141 -g8 -I0 -sg9 -S'rest_framework_generics.html' -p142 -sg11 -S'rest_framework/generics' -p143 -sg13 -g14 -(g15 -g16 -NtRp144 -(dp145 -g19 -I1 -sg20 -I0 -sg21 -I196 -sg22 -I0 -sg23 -I34 -sg24 -I0 -sbssg25 -S'@\x1c\x97\x176\x18\x9c\xfc"| |\xb8^\xbb\x83' -p146 -ssS'rest_framework_utils___init__' -p147 -(dp148 -g6 -(dp149 -g8 -I0 -sg9 -S'rest_framework_utils___init__.html' -p150 -sg11 -S'rest_framework/utils/__init__' -p151 -sg13 -g14 -(g15 -g16 -NtRp152 -(dp153 -g19 -I1 -sg20 -I0 -sg21 -I0 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\xb0\xc8pN\xaf>\xa0\xbaz\x144\xe0A9\xb8?' -p154 -ssS'rest_framework_renderers' -p155 -(dp156 -g6 -(dp157 -g8 -I0 -sg9 -S'rest_framework_renderers.html' -p158 -sg11 -S'rest_framework/renderers' -p159 -sg13 -g14 -(g15 -g16 -NtRp160 -(dp161 -g19 -I1 -sg20 -I0 -sg21 -I282 -sg22 -I0 -sg23 -I23 -sg24 -I0 -sbssg25 -S'\t\x11\xd4\xafO\xae\\*\x8d\xaf\xa4f\xde\x86\xe8N' -p162 -ssS'rest_framework_negotiation' -p163 -(dp164 -g6 -(dp165 -g8 -I0 -sg9 -S'rest_framework_negotiation.html' -p166 -sg11 -S'rest_framework/negotiation' -p167 -sg13 -g14 -(g15 -g16 -NtRp168 -(dp169 -g19 -I1 -sg20 -I0 -sg21 -I41 -sg22 -I0 -sg23 -I4 -sg24 -I0 -sbssg25 -S'\xd2\xa2\x94\xc8}y\xba\x9eZE\xe5M\xa5>\x9f\x8d' -p170 -ssS'rest_framework_throttling' -p171 -(dp172 -g6 -(dp173 -g8 -I0 -sg9 -S'rest_framework_throttling.html' -p174 -sg11 -S'rest_framework/throttling' -p175 -sg13 -g14 -(g15 -g16 -NtRp176 -(dp177 -g19 -I1 -sg20 -I0 -sg21 -I90 -sg22 -I0 -sg23 -I17 -sg24 -I0 -sbssg25 -S'a\xbcT\xe7\xff\x1an\xb5\x886\xa3\xa2e\x90PZ' -p178 -ssS'rest_framework_reverse' -p179 -(dp180 -g6 -(dp181 -g8 -I0 -sg9 -S'rest_framework_reverse.html' -p182 -sg11 -S'rest_framework/reverse' -p183 -sg13 -g14 -(g15 -g16 -NtRp184 -(dp185 -g19 -I1 -sg20 -I0 -sg21 -I12 -sg22 -I0 -sg23 -I3 -sg24 -I0 -sbssg25 -S"#\xe7D\x01\x10\xe8'1\x9c\xc9yX4\xb4\xef\x19" -p186 -ssS'rest_framework_request' -p187 -(dp188 -g6 -(dp189 -g8 -I0 -sg9 -S'rest_framework_request.html' -p190 -sg11 -S'rest_framework/request' -p191 -sg13 -g14 -(g15 -g16 -NtRp192 -(dp193 -g19 -I1 -sg20 -I0 -sg21 -I161 -sg22 -I0 -sg23 -I8 -sg24 -I0 -sbssg25 -S'C\xd4v\x9b\xf2Z\xe47\xe8\xc8\x03\xf4\xf8\xac\xefs' -p194 -ssS'rest_framework_parsers' -p195 -(dp196 -g6 -(dp197 -g8 -I0 -sg9 -S'rest_framework_parsers.html' -p198 -sg11 -S'rest_framework/parsers' -p199 -sg13 -g14 -(g15 -g16 -NtRp200 -(dp201 -g19 -I1 -sg20 -I0 -sg21 -I153 -sg22 -I0 -sg23 -I13 -sg24 -I0 -sbssg25 -S'\x11o\x05[\x99{\x9c\x8bj\xa8\xd0t\xe8\x16\\\xae' -p202 -ssS'rest_framework_settings' -p203 -(dp204 -g6 -(dp205 -g8 -I0 -sg9 -S'rest_framework_settings.html' -p206 -sg11 -S'rest_framework/settings' -p207 -sg13 -g14 -(g15 -g16 -NtRp208 -(dp209 -g19 -I1 -sg20 -I0 -sg21 -I44 -sg22 -I0 -sg23 -I2 -sg24 -I0 -sbssg25 -S'\n\xb8|\x03\xa7d|\xfc9\xda\xb5\xb9\x1a\x00@\xc3' -p210 -ssS'rest_framework_authtoken_models' -p211 -(dp212 -g6 -(dp213 -g8 -I0 -sg9 -S'rest_framework_authtoken_models.html' -p214 -sg11 -S'rest_framework/authtoken/models' -p215 -sg13 -g14 -(g15 -g16 -NtRp216 -(dp217 -g19 -I1 -sg20 -I0 -sg21 -I21 -sg22 -I0 -sg23 -I1 -sg24 -I0 -sbssg25 -S'U;\xc7\xf5{\xf6r\xc7]\x95\xffF\xde\x8caE' -p218 -ssS'rest_framework_decorators' -p219 -(dp220 -g6 -(dp221 -g8 -I0 -sg9 -S'rest_framework_decorators.html' -p222 -sg11 -S'rest_framework/decorators' -p223 -sg13 -g14 -(g15 -g16 -NtRp224 -(dp225 -g19 -I1 -sg20 -I0 -sg21 -I60 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S"\xd4\x88\xa2\x16\xf4#X\xb4X\xe97Lj\xeb\x16'" -p226 -ssS'rest_framework_authentication' -p227 -(dp228 -g6 -(dp229 -g8 -I0 -sg9 -S'rest_framework_authentication.html' -p230 -sg11 -S'rest_framework/authentication' -p231 -sg13 -g14 -(g15 -g16 -NtRp232 -(dp233 -g19 -I1 -sg20 -I0 -sg21 -I169 -sg22 -I0 -sg23 -I33 -sg24 -I0 -sbssg25 -S'^\x80:,\x1cL\xde\t\xc1\x93\xe0\x8b\x11\xf4\xb8\x06' -p234 -ssS'rest_framework_utils_formatting' -p235 -(dp236 -g6 -(dp237 -g8 -I0 -sg9 -S'rest_framework_utils_formatting.html' -p238 -sg11 -S'rest_framework/utils/formatting' -p239 -sg13 -g14 -(g15 -g16 -NtRp240 -(dp241 -g19 -I1 -sg20 -I0 -sg21 -I39 -sg22 -I0 -sg23 -I1 -sg24 -I0 -sbssg25 -S'\xdd\x05M\xeb\xfe\tl\xe6\xdd\xc5k\xae\xa8\xf9um' -p242 -ssS'rest_framework_pagination' -p243 -(dp244 -g6 -(dp245 -g8 -I0 -sg9 -S'rest_framework_pagination.html' -p246 -sg11 -S'rest_framework/pagination' -p247 -sg13 -g14 -(g15 -g16 -NtRp248 -(dp249 -g19 -I1 -sg20 -I0 -sg21 -I43 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'y\xa8f\rv\x8c\x9b\x9a:9\xdc\x89\t>\x0c' -p282 -ssS'rest_framework_viewsets' -p283 -(dp284 -g6 -(dp285 -g8 -I0 -sg9 -S'rest_framework_viewsets.html' -p286 -sg11 -S'rest_framework/viewsets' -p287 -sg13 -g14 -(g15 -g16 -NtRp288 -(dp289 -g19 -I1 -sg20 -I0 -sg21 -I39 -sg22 -I0 -sg23 -I2 -sg24 -I0 -sbssg25 -S'ic\x82\xc6e\x93$\x1b\x0c\x8bK\x10\x0f9\xe8\n' -p290 -ssS'rest_framework_authtoken___init__' -p291 -(dp292 -g6 -(dp293 -g8 -I0 -sg9 -S'rest_framework_authtoken___init__.html' -p294 -sg11 -S'rest_framework/authtoken/__init__' -p295 -sg13 -g14 -(g15 -g16 -NtRp296 -(dp297 -g19 -I1 -sg20 -I0 -sg21 -I0 -sg22 -I0 -sg23 -I0 -sg24 -I0 -sbssg25 -S'\xb0\xc8pN\xaf>\xa0\xbaz\x144\xe0A9\xb8?' -p298 -ssS'rest_framework_routers' -p299 -(dp300 -g6 -(dp301 -g8 -I0 -sg9 -S'rest_framework_routers.html' -p302 -sg11 -S'rest_framework/routers' -p303 -sg13 -g14 -(g15 -g16 -NtRp304 -(dp305 -g19 -I1 -sg20 -I0 -sg21 -I108 -sg22 -I0 -sg23 -I7 -sg24 -I0 -sbssg25 -S'i\xa8[\x1f\x0f|\xd6\xa0R\x98\xa9\xecs\xe53\xb3' -p306 -sssS'version' -p307 -S'3.5.1' -p308 -sS'settings' -p309 -S'\xfe\xa4\x01e\x06\x8a\x97H\x97\xaf\xbf\xcd\xfez\xe4\xbf' -p310 -sS'format' -p311 -I1 -s. \ No newline at end of file diff --git a/htmlcov/style.css b/htmlcov/style.css deleted file mode 100644 index c40357b8b..000000000 --- a/htmlcov/style.css +++ /dev/null @@ -1,275 +0,0 @@ -/* CSS styles for Coverage. */ -/* Page-wide styles */ -html, body, h1, h2, h3, p, td, th { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; - } - -/* Set baseline grid to 16 pt. */ -body { - font-family: georgia, serif; - font-size: 1em; - } - -html>body { - font-size: 16px; - } - -/* Set base font size to 12/16 */ -p { - font-size: .75em; /* 12/16 */ - line-height: 1.3333em; /* 16/12 */ - } - -table { - border-collapse: collapse; - } - -a.nav { - text-decoration: none; - color: inherit; - } -a.nav:hover { - text-decoration: underline; - color: inherit; - } - -/* Page structure */ -#header { - background: #f8f8f8; - width: 100%; - border-bottom: 1px solid #eee; - } - -#source { - padding: 1em; - font-family: "courier new", monospace; - } - -#indexfile #footer { - margin: 1em 3em; - } - -#pyfile #footer { - margin: 1em 1em; - } - -#footer .content { - padding: 0; - font-size: 85%; - font-family: verdana, sans-serif; - color: #666666; - font-style: italic; - } - -#index { - margin: 1em 0 0 3em; - } - -/* Header styles */ -#header .content { - padding: 1em 3em; - } - -h1 { - font-size: 1.25em; -} - -h2.stats { - margin-top: .5em; - font-size: 1em; -} -.stats span { - border: 1px solid; - padding: .1em .25em; - margin: 0 .1em; - cursor: pointer; - border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} - -/* Help panel */ -#keyboard_icon { - float: right; - cursor: pointer; -} - -.help_panel { - position: absolute; - background: #ffc; - padding: .5em; - border: 1px solid #883; - display: none; -} - -#indexfile .help_panel { - width: 20em; height: 4em; -} - -#pyfile .help_panel { - width: 16em; height: 8em; -} - -.help_panel .legend { - font-style: italic; - margin-bottom: 1em; -} - -#panel_icon { - float: right; - cursor: pointer; -} - -.keyhelp { - margin: .75em; -} - -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; -} - -/* Source file styles */ -.linenos p { - text-align: right; - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - font-size: .625em; /* 10/16 */ - line-height: 1.6em; /* 16/10 */ - } -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - } -.linenos p a:hover { - text-decoration: underline; - color: #999999; - } - -td.text { - width: 100%; - } -.text p { - margin: 0; - padding: 0 0 0 .5em; - border-left: 2px solid #ffffff; - white-space: nowrap; - } - -.text p.mis { - background: #ffdddd; - border-left: 2px solid #ff0000; - } -.text p.run, .text p.run.hide_par { - background: #ddffdd; - border-left: 2px solid #00ff00; - } -.text p.exc { - background: #eeeeee; - border-left: 2px solid #808080; - } -.text p.par, .text p.par.hide_run { - background: #ffffaa; - border-left: 2px solid #eeee99; - } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, -.text p.hide_run.hide_par { - background: inherit; - } - -.text span.annotate { - font-family: georgia; - font-style: italic; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } - -/* Syntax coloring */ -.text .com { - color: green; - font-style: italic; - line-height: 1px; - } -.text .key { - font-weight: bold; - line-height: 1px; - } -.text .str { - color: #000080; - } - -/* index styles */ -#index td, #index th { - text-align: right; - width: 5em; - padding: .25em .5em; - border-bottom: 1px solid #eee; - } -#index th { - font-style: italic; - color: #333; - border-bottom: 1px solid #ccc; - cursor: pointer; - } -#index th:hover { - background: #eee; - border-bottom: 1px solid #999; - } -#index td.left, #index th.left { - padding-left: 0; - } -#index td.right, #index th.right { - padding-right: 0; - } -#index th.headerSortDown, #index th.headerSortUp { - border-bottom: 1px solid #000; - } -#index td.name, #index th.name { - text-align: left; - width: auto; - } -#index td.name a { - text-decoration: none; - color: #000; - } -#index td.name a:hover { - text-decoration: underline; - color: #000; - } -#index tr.total { - } -#index tr.total td { - font-weight: bold; - border-top: 1px solid #ccc; - border-bottom: none; - } -#index tr.file:hover { - background: #eeeeee; - } From 209b65f426e1935c970c95fad389ba5c03388592 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:12:37 +0100 Subject: [PATCH 012/206] Update assertion error to reference 'base_name' argument, not incorrect 'name' argument. Closes #933 --- rest_framework/routers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index f70c2cdb1..2a26f6a72 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -117,7 +117,7 @@ class SimpleRouter(BaseRouter): if model_cls is None and queryset is not None: model_cls = queryset.model - assert model_cls, '`name` not argument not specified, and could ' \ + assert model_cls, '`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.' From 2d5f7f201ffcc8c371e9f36821c2ae0e13dcecca Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:19:14 +0100 Subject: [PATCH 013/206] Update router docs on base_name. Refs #933. --- 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 f16fa9468..b74b6e13b 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -26,7 +26,7 @@ There are two mandatory arguments to the `register()` method: Optionally, you may also specify an additional argument: -* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. +* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset. The example above would generate the following URL patterns: From a68f473dd8438c7d7b0f8ec4b4dc74aa0544143d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 23:25:14 +0200 Subject: [PATCH 014/206] Brackets not required on decorator without arguments --- 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 25d11bfb5..ad961636d 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -108,7 +108,7 @@ For example: queryset = User.objects.all() serializer_class = UserSerializer - @action() + @action def set_password(self, request, pk=None): user = self.get_object() serializer = PasswordSerializer(data=request.DATA) From 4f7f93e20ef53fbc0b66766158bca75ebddce2ed Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:28:36 +0100 Subject: [PATCH 015/206] Added @freakydug, for changes in #941. Thanks :) --- docs/topics/credits.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 3f0ee429c..a7c09b5bc 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -142,6 +142,7 @@ The following people have helped make REST framework great. * Areski Belaid - [areski] * Ethan Freman - [mindlace] * David Sanders - [davesque] +* Philip Douglas - [freakydug] Many thanks to everyone who's contributed to the project. @@ -320,4 +321,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [areski]: https://github.com/areski [mindlace]: https://github.com/mindlace [davesque]: https://github.com/davesque - +[freakydug]: https://github.com/freakydug From 8cc63b09f6065e0197e060cc4d62b560196c8877 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:42:04 +0100 Subject: [PATCH 016/206] Add support for StreamingHttpResponse. Closes #939 --- docs/api-guide/responses.md | 2 +- rest_framework/compat.py | 6 ++++++ rest_framework/views.py | 11 ++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index f83b8194a..399b7c23f 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -10,7 +10,7 @@ REST framework supports HTTP content negotiation by providing a `Response` class The `Response` class subclasses Django's `SimpleTemplateResponse`. `Response` objects are initialised with data, which should consist of native Python primitives. REST framework then uses standard HTTP content negotiation to determine how it should render the final response content. -There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` objects from your views if you want, but it provides a nicer interface for returning Web API responses. +There's no requirement for you to use the `Response` class, you can also return regular `HttpResponse` or `StreamingHttpResponse` objects from your views if required. Using the `Response` class simply provides a nicer interface for returning content-negotiated Web API responses, that can be rendered to multiple formats. Unless you want to heavily customize REST framework for some reason, you should always use an `APIView` class or `@api_view` function for views that return `Response` objects. Doing so ensures that the view can perform content negotiation and select the appropriate renderer for the response, before it is returned from the view. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 76dc00526..a19bd778b 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -33,6 +33,12 @@ except ImportError: from django.utils.encoding import force_unicode as force_text +# HttpResponseBase only exists from 1.5 onwards +try: + from django.http.response import HttpResponseBase +except ImportError: + from django.http import HttpResponse as HttpResponseBase + # django-filter is optional try: import django_filters diff --git a/rest_framework/views.py b/rest_framework/views.py index c28d2835f..37bba7f02 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -4,11 +4,11 @@ Provides an APIView class that is the base of all views in REST framework. from __future__ import unicode_literals from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpResponse +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 +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 @@ -244,9 +244,10 @@ class APIView(View): Returns the final response object. """ # Make the error obvious if a proper response is not returned - assert isinstance(response, HttpResponse), ( - 'Expected a `Response` to be returned from the view, ' - 'but received a `%s`' % type(response) + assert isinstance(response, HttpResponseBase), ( + 'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` ' + 'to be returned from the view, but received a `%s`' + % type(response) ) if isinstance(response, Response): From fb6bcd9f06daaca51441c6b851d6411621d32c26 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 22:43:01 +0100 Subject: [PATCH 017/206] Update release notes, noting support for StreamingHttpResponse. Refs #939 --- 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 f49dd5c84..b08ac0583 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 * Added `trailing_slash` option to routers. +* Include support for `HttpStreamingResponse`. * Support wider range of default serializer validation when used with custom model fields. * Bugfix: Return error correctly when OAuth non-existent consumer occurs. * Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. From 8d83ff8e6c8513d0a88d6b1fecb34ed86f1e2085 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Jun 2013 23:12:16 +0100 Subject: [PATCH 018/206] Add decorator brackets back. Refs #941 --- 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 ad961636d..25d11bfb5 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -108,7 +108,7 @@ For example: queryset = User.objects.all() serializer_class = UserSerializer - @action + @action() def set_password(self, request, pk=None): user = self.get_object() serializer = PasswordSerializer(data=request.DATA) From 2bf5f6305030d5ebbd5a8a0fd5c31586c08a558d Mon Sep 17 00:00:00 2001 From: Igor Kalat Date: Sat, 22 Jun 2013 13:43:45 +0200 Subject: [PATCH 019/206] Make browsable API views play nice with utf-8 --- rest_framework/tests/test_utils.py | 36 ++++++++++++++++++++++++++++++ rest_framework/utils/formatting.py | 5 +++++ 2 files changed, 41 insertions(+) create mode 100644 rest_framework/tests/test_utils.py diff --git a/rest_framework/tests/test_utils.py b/rest_framework/tests/test_utils.py new file mode 100644 index 000000000..da508dbbe --- /dev/null +++ b/rest_framework/tests/test_utils.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase +from rest_framework.utils import formatting +import sys + + +class FormattingUnitTests(TestCase): + def setUp(self): + # test strings snatched from http://www.columbia.edu/~fdc/utf8/, + # http://winrus.com/utf8-jap.htm and memory + self.utf8_test_string = ( + 'zażółć gęślą jaźń' + 'Sîne klâwen durh die wolken sint geslagen' + 'Τη γλώσσα μου έδωσαν ελληνική' + 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' + 'На берегу пустынных волн' + ' てすと' + 'アイウエオカキクケコサシスセソタチツテ' + ) + self.non_utf8_test_string = ('The quick brown fox jumps over the lazy ' + 'dog') + + def test_for_ascii_support_in_remove_leading_indent(self): + if sys.version_info < (3, 0): + # only Python 2.x is affected, so we skip the test entirely + # if on Python 3.x + self.assertEqual(formatting._remove_leading_indent( + self.non_utf8_test_string), self.non_utf8_test_string) + + def test_for_utf8_support_in_remove_leading_indent(self): + if sys.version_info < (3, 0): + # only Python 2.x is affected, so we skip the test entirely + # if on Python 3.x + self.assertEqual(formatting._remove_leading_indent( + self.utf8_test_string), self.utf8_test_string.decode('utf-8')) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index ebadb3a67..a2a5609c0 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -24,6 +24,11 @@ def _remove_leading_indent(content): Remove leading indent from a block of text. Used when generating descriptions from docstrings. """ + try: + content = content.decode('utf-8') + except (AttributeError, UnicodeEncodeError): + pass # the string should keep the default 'ascii' encoding in + # Python 2.x or stay a unicode string in Python 3.x whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] From 13a3c993ab20e7af510d615a5eafaa87667b8efb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 11:30:27 +0100 Subject: [PATCH 020/206] Fix incorrect example --- docs/api-guide/generic-views.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index cd1bc7a1c..67853ed01 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -92,7 +92,8 @@ May be overridden to provide dynamic behavior such as returning a queryset that For example: def get_queryset(self): - return self.user.accounts.all() + user = self.request.user + return user.accounts.all() #### `get_object(self)` From 715bd47dfababd39be9b3295ada99f2107d7c00c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 17:56:42 +0100 Subject: [PATCH 021/206] Use AUTH_USER_MODEL consistently between various Django versions. Closes #946 --- rest_framework/authtoken/models.py | 4 ++-- rest_framework/compat.py | 10 ++-------- rest_framework/runtests/settings.py | 2 ++ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 52c45ad11..7601f5b79 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,7 +1,7 @@ import uuid import hmac from hashlib import sha1 -from rest_framework.compat import User +from rest_framework.compat import AUTH_USER_MODEL from django.conf import settings from django.db import models @@ -11,7 +11,7 @@ class Token(models.Model): The default authorization token model. """ key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField(User, related_name='auth_token') + user = models.OneToOneField(AUTH_USER_MODEL, related_name='auth_token') created = models.DateTimeField(auto_now_add=True) class Meta: diff --git a/rest_framework/compat.py b/rest_framework/compat.py index a19bd778b..69853730a 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -83,15 +83,9 @@ def get_concrete_model(model_cls): # Django 1.5 add support for custom auth user model if django.VERSION >= (1, 5): from django.conf import settings - if hasattr(settings, 'AUTH_USER_MODEL'): - User = settings.AUTH_USER_MODEL - else: - from django.contrib.auth.models import User + AUTH_USER_MODEL = settings.AUTH_USER_MODEL else: - try: - from django.contrib.auth.models import User - except ImportError: - raise ImportError("User model is not to be found.") + AUTH_USER_MODEL = 'auth.User' if django.VERSION >= (1, 5): diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 9dd7b545e..b3702d0bf 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -134,6 +134,8 @@ PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.CryptPasswordHasher', ) +AUTH_USER_MODEL = 'auth.User' + import django if django.VERSION < (1, 3): From c8b0e6c40f6bcf447aa539ff98b9985aa53032ce Mon Sep 17 00:00:00 2001 From: Igor Kalat Date: Wed, 26 Jun 2013 22:12:02 +0200 Subject: [PATCH 022/206] Refactored get_view_description, moved appropriate tests to test_description.py --- rest_framework/tests/test_description.py | 13 +++++---- rest_framework/tests/test_utils.py | 36 ------------------------ rest_framework/tests/views.py | 25 ++++++++++++++++ rest_framework/utils/formatting.py | 9 ++---- 4 files changed, 34 insertions(+), 49 deletions(-) delete mode 100644 rest_framework/tests/test_utils.py create mode 100644 rest_framework/tests/views.py diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index 52c1a34c1..bc86e1064 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework.views import APIView -from rest_framework.compat import apply_markdown +from rest_framework.compat import apply_markdown, smart_text from rest_framework.utils.formatting import get_view_name, get_view_description +from rest_framework.tests.views import ( + ViewWithNonASCIICharactersInDocstring, UTF8_TEST_DOCSTRING) # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring @@ -83,11 +85,10 @@ class TestViewNamesAndDescriptions(TestCase): Unicode in docstrings should be respected. """ - class MockView(APIView): - """Проверка""" - pass - - self.assertEqual(get_view_description(MockView), "Проверка") + self.assertEqual( + get_view_description(ViewWithNonASCIICharactersInDocstring), + smart_text(UTF8_TEST_DOCSTRING) + ) def test_view_description_can_be_empty(self): """ diff --git a/rest_framework/tests/test_utils.py b/rest_framework/tests/test_utils.py deleted file mode 100644 index da508dbbe..000000000 --- a/rest_framework/tests/test_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.test import TestCase -from rest_framework.utils import formatting -import sys - - -class FormattingUnitTests(TestCase): - def setUp(self): - # test strings snatched from http://www.columbia.edu/~fdc/utf8/, - # http://winrus.com/utf8-jap.htm and memory - self.utf8_test_string = ( - 'zażółć gęślą jaźń' - 'Sîne klâwen durh die wolken sint geslagen' - 'Τη γλώσσα μου έδωσαν ελληνική' - 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' - 'На берегу пустынных волн' - ' てすと' - 'アイウエオカキクケコサシスセソタチツテ' - ) - self.non_utf8_test_string = ('The quick brown fox jumps over the lazy ' - 'dog') - - def test_for_ascii_support_in_remove_leading_indent(self): - if sys.version_info < (3, 0): - # only Python 2.x is affected, so we skip the test entirely - # if on Python 3.x - self.assertEqual(formatting._remove_leading_indent( - self.non_utf8_test_string), self.non_utf8_test_string) - - def test_for_utf8_support_in_remove_leading_indent(self): - if sys.version_info < (3, 0): - # only Python 2.x is affected, so we skip the test entirely - # if on Python 3.x - self.assertEqual(formatting._remove_leading_indent( - self.utf8_test_string), self.utf8_test_string.decode('utf-8')) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py new file mode 100644 index 000000000..fc00cc0b5 --- /dev/null +++ b/rest_framework/tests/views.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from rest_framework.views import APIView + + +# test strings snatched from http://www.columbia.edu/~fdc/utf8/, +# http://winrus.com/utf8-jap.htm and memory +UTF8_TEST_DOCSTRING = ( + 'zażółć gęślą jaźń' + 'Sîne klâwen durh die wolken sint geslagen' + 'Τη γλώσσα μου έδωσαν ελληνική' + 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' + 'На берегу пустынных волн' + 'てすと' + 'アイウエオカキクケコサシスセソタチツテ' +) + + +# Apparently there is an issue where docstrings of imported view classes +# do not retain their encoding information even if a module has a proper +# encoding declaration at the top of its source file. Therefore for tests +# to catch unicode related errors, a mock view has to be declared in a separate +# module. +class ViewWithNonASCIICharactersInDocstring(APIView): + __doc__ = UTF8_TEST_DOCSTRING diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index a2a5609c0..4bec83877 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -5,7 +5,7 @@ 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 +from rest_framework.compat import apply_markdown, smart_text import re @@ -24,11 +24,6 @@ def _remove_leading_indent(content): Remove leading indent from a block of text. Used when generating descriptions from docstrings. """ - try: - content = content.decode('utf-8') - except (AttributeError, UnicodeEncodeError): - pass # the string should keep the default 'ascii' encoding in - # Python 2.x or stay a unicode string in Python 3.x whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] @@ -68,7 +63,7 @@ def get_view_description(cls, html=False): Return a description for an `APIView` class or `@api_view` function. """ description = cls.__doc__ or '' - description = _remove_leading_indent(description) + description = _remove_leading_indent(smart_text(description)) if html: return markup_description(description) return description From 69e5e3cc0db481e4fad7ac34bf28b73f4786e790 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 21:18:13 +0100 Subject: [PATCH 023/206] Use timezone aware datetimes with oauth2 provider, when supported. Closes #947. --- rest_framework/authentication.py | 9 ++++----- rest_framework/compat.py | 10 ++++++++++ rest_framework/serializers.py | 5 ++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index f659a172e..102980271 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -3,14 +3,13 @@ Provides various authentication policies. """ from __future__ import unicode_literals import base64 -from datetime import datetime from django.contrib.auth import authenticate 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 +from rest_framework.compat import oauth2_provider, provider_now from rest_framework.authtoken.models import Token @@ -320,9 +319,9 @@ class OAuth2Authentication(BaseAuthentication): try: token = oauth2_provider.models.AccessToken.objects.select_related('user') - # TODO: Change to timezone aware datetime when oauth2_provider add - # support to it. - token = token.get(token=access_token, expires__gt=datetime.now()) + # provider_now switches to timezone aware datetime when + # the oauth2_provider version supports to it. + token = token.get(token=access_token, expires__gt=provider_now()) except oauth2_provider.models.AccessToken.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 69853730a..b748dcc51 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -2,6 +2,7 @@ The `compat` module provides support for backwards compatibility with older versions of django/python, and compatibility wrappers around optional packages. """ + # flake8: noqa from __future__ import unicode_literals @@ -489,12 +490,21 @@ try: from provider.oauth2 import forms as oauth2_provider_forms from provider import scope as oauth2_provider_scope from provider import constants as oauth2_constants + from provider import __version__ as provider_version + 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 + else: + # Any other supported version does use timezone aware datetimes + from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None oauth2_provider_models = None oauth2_provider_forms = None oauth2_provider_scope = None oauth2_constants = None + provider_now = None # Handle lazy strings from django.utils.functional import Promise diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5a8fd89f0..023f7ccfb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -915,7 +915,10 @@ class HyperlinkedModelSerializer(ModelSerializer): view_name=self.opts.view_name, lookup_field=self.opts.lookup_field ) - fields.insert(0, 'url', url_field) + ret = self._dict_class() + ret['url'] = url_field + ret.update(fields) + fields = ret return fields From 494703fc8e916a9b7a318ec8bc7d774ef31de14e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 22:40:14 +0100 Subject: [PATCH 024/206] 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 b08ac0583..ce4df83c1 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`: * Added `trailing_slash` option to routers. * Include support for `HttpStreamingResponse`. * Support wider range of default serializer validation when used with custom model fields. +* OAuth2 provider usez timezone aware datetimes when supported. * Bugfix: Return error correctly when OAuth non-existent consumer occurs. * Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. * Bugfix: Fix `ScopedRateThrottle`. From 91b9fcb0ba2541b2752e2ab0706becad14bdda20 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 22:43:17 +0100 Subject: [PATCH 025/206] Minor test cleanup --- rest_framework/tests/test_description.py | 24 +++++++++++++++++++++-- rest_framework/tests/views.py | 25 ------------------------ 2 files changed, 22 insertions(+), 27 deletions(-) delete mode 100644 rest_framework/tests/views.py diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index bc86e1064..ea4b2c3ab 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -5,8 +5,6 @@ from django.test import TestCase from rest_framework.views import APIView from rest_framework.compat import apply_markdown, smart_text from rest_framework.utils.formatting import get_view_name, get_view_description -from rest_framework.tests.views import ( - ViewWithNonASCIICharactersInDocstring, UTF8_TEST_DOCSTRING) # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring @@ -51,6 +49,28 @@ MARKED_DOWN_gte_21 = """

an example docstring

hash style header

""" +# test strings snatched from http://www.columbia.edu/~fdc/utf8/, +# http://winrus.com/utf8-jap.htm and memory +UTF8_TEST_DOCSTRING = ( + 'zażółć gęślą jaźń' + 'Sîne klâwen durh die wolken sint geslagen' + 'Τη γλώσσα μου έδωσαν ελληνική' + 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' + 'На берегу пустынных волн' + 'てすと' + 'アイウエオカキクケコサシスセソタチツテ' +) + + +# Apparently there is an issue where docstrings of imported view classes +# do not retain their encoding information even if a module has a proper +# encoding declaration at the top of its source file. Therefore for tests +# to catch unicode related errors, a mock view has to be declared in a separate +# module. +class ViewWithNonASCIICharactersInDocstring(APIView): + __doc__ = UTF8_TEST_DOCSTRING + + class TestViewNamesAndDescriptions(TestCase): def test_view_name_uses_class_name(self): """ diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py deleted file mode 100644 index fc00cc0b5..000000000 --- a/rest_framework/tests/views.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- - -from rest_framework.views import APIView - - -# test strings snatched from http://www.columbia.edu/~fdc/utf8/, -# http://winrus.com/utf8-jap.htm and memory -UTF8_TEST_DOCSTRING = ( - 'zażółć gęślą jaźń' - 'Sîne klâwen durh die wolken sint geslagen' - 'Τη γλώσσα μου έδωσαν ελληνική' - 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' - 'На берегу пустынных волн' - 'てすと' - 'アイウエオカキクケコサシスセソタチツテ' -) - - -# Apparently there is an issue where docstrings of imported view classes -# do not retain their encoding information even if a module has a proper -# encoding declaration at the top of its source file. Therefore for tests -# to catch unicode related errors, a mock view has to be declared in a separate -# module. -class ViewWithNonASCIICharactersInDocstring(APIView): - __doc__ = UTF8_TEST_DOCSTRING From cb83bc373f8044ec21f5affabda0540ed0876357 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 22:44:44 +0100 Subject: [PATCH 026/206] Added @trwired for fix #943. Thanks :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index a7c09b5bc..94760c74b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -143,6 +143,7 @@ The following people have helped make REST framework great. * Ethan Freman - [mindlace] * David Sanders - [davesque] * Philip Douglas - [freakydug] +* Igor Kalat - [trwired] Many thanks to everyone who's contributed to the project. @@ -322,3 +323,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mindlace]: https://github.com/mindlace [davesque]: https://github.com/davesque [freakydug]: https://github.com/freakydug +[trwired]: https://github.com/trwired From af2fdc03a6f4cafe6e2f19b2adcf59c8918088f2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 22:45:39 +0100 Subject: [PATCH 027/206] 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 ce4df83c1..4fecbf1f2 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`: * Added `trailing_slash` option to routers. * Include support for `HttpStreamingResponse`. * Support wider range of default serializer validation when used with custom model fields. +* UTF-8 Support for browsable API descriptions. * OAuth2 provider usez timezone aware datetimes when supported. * Bugfix: Return error correctly when OAuth non-existent consumer occurs. * Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. From c127e63c32b2fb93d1a9422943005c1f6cc5328b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Wed, 26 Jun 2013 23:00:42 +0100 Subject: [PATCH 028/206] Raise exception when attempting to dynamically route to a method that is already routed to. Fixes #940 --- rest_framework/routers.py | 14 ++++++++++++++ rest_framework/tests/test_routers.py | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index c222f5046..930011d39 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -15,7 +15,9 @@ For example, you might have a `urls.py` that looks something like this: """ from __future__ import unicode_literals +import itertools from collections import namedtuple +from django.core.exceptions import ImproperlyConfigured from rest_framework import views from rest_framework.compat import patterns, url from rest_framework.response import Response @@ -38,6 +40,13 @@ def replace_methodname(format_string, methodname): return ret +def flatten(list_of_lists): + """ + Takes an iterable of iterables, returns a single iterable containing all items + """ + return itertools.chain(*list_of_lists) + + class BaseRouter(object): def __init__(self): self.registry = [] @@ -130,12 +139,17 @@ class SimpleRouter(BaseRouter): Returns a list of the Route namedtuple. """ + known_actions = flatten([route.mapping.values() for route in self.routes]) + # Determine any `@action` or `@link` decorated methods on the viewset dynamic_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) httpmethods = getattr(attr, 'bind_to_methods', None) 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)) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index fe0711fa2..d375f4a8c 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.db import models from django.test import TestCase from django.test.client import RequestFactory +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 @@ -191,3 +192,24 @@ class TestActionKeywordArgs(TestCase): response.data, {'permission_classes': [permissions.AllowAny]} ) + +class TestActionAppliedToExistingRoute(TestCase): + """ + Ensure `@action` 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() + def retrieve(self, request, *args, **kwargs): + return Response({ + 'hello': 'world' + }) + + self.router = SimpleRouter() + self.router.register(r'test', TestViewSet, base_name='test') + + with self.assertRaises(ImproperlyConfigured): + self.router.urls From 4d22a65e78432a2aa70ddc80395a014a7c9e299e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 26 Jun 2013 23:26:35 +0100 Subject: [PATCH 029/206] Fix sidebar styling when browser window is too small --- docs/css/default.css | 4 ++++ docs/template.html | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/css/default.css b/docs/css/default.css index a4f05daa8..af6a9cc03 100644 --- a/docs/css/default.css +++ b/docs/css/default.css @@ -303,3 +303,7 @@ table { border-color: white; margin-bottom: 0.6em; } + +.side-nav { + overflow-y: scroll; +} diff --git a/docs/template.html b/docs/template.html index 53656e7d4..14ecc9c7a 100644 --- a/docs/template.html +++ b/docs/template.html @@ -198,5 +198,14 @@ $('.dropdown-menu').on('click touchstart', function(event) { event.stopPropagation(); }); + + // Dynamically force sidenav to no higher than browser window + $('.side-nav').css('max-height', window.innerHeight - 125); + + $(function(){ + $(window).resize(function(){ + $('.side-nav').css('max-height', window.innerHeight - 125); + }); + }); From 96f41fd12d376833a5822918cedcec5e74d59d02 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 11:58:34 +0100 Subject: [PATCH 030/206] Use imported views to expose python 2.6 bug. Refs #943 --- rest_framework/tests/description.py | 26 ++++++++++++++++++++++++ rest_framework/tests/test_description.py | 26 +++--------------------- 2 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 rest_framework/tests/description.py diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py new file mode 100644 index 000000000..b46d7f54d --- /dev/null +++ b/rest_framework/tests/description.py @@ -0,0 +1,26 @@ +# -- coding: utf-8 -- + +# Apparently there is a python 2.6 issue where docstrings of imported view classes +# do not retain their encoding information even if a module has a proper +# encoding declaration at the top of its source file. Therefore for tests +# to catch unicode related errors, a mock view has to be declared in a separate +# module. + +from rest_framework.views import APIView + + +# test strings snatched from http://www.columbia.edu/~fdc/utf8/, +# http://winrus.com/utf8-jap.htm and memory +UTF8_TEST_DOCSTRING = ( + 'zażółć gęślą jaźń' + 'Sîne klâwen durh die wolken sint geslagen' + 'Τη γλώσσα μου έδωσαν ελληνική' + 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' + 'На берегу пустынных волн' + 'てすと' + 'アイウエオカキクケコサシスセソタチツテ' +) + + +class ViewWithNonASCIICharactersInDocstring(APIView): + __doc__ = UTF8_TEST_DOCSTRING diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py index ea4b2c3ab..8019f5eca 100644 --- a/rest_framework/tests/test_description.py +++ b/rest_framework/tests/test_description.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals from django.test import TestCase -from rest_framework.views import APIView 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. @@ -49,28 +51,6 @@ MARKED_DOWN_gte_21 = """

an example docstring

hash style header

""" -# test strings snatched from http://www.columbia.edu/~fdc/utf8/, -# http://winrus.com/utf8-jap.htm and memory -UTF8_TEST_DOCSTRING = ( - 'zażółć gęślą jaźń' - 'Sîne klâwen durh die wolken sint geslagen' - 'Τη γλώσσα μου έδωσαν ελληνική' - 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' - 'На берегу пустынных волн' - 'てすと' - 'アイウエオカキクケコサシスセソタチツテ' -) - - -# Apparently there is an issue where docstrings of imported view classes -# do not retain their encoding information even if a module has a proper -# encoding declaration at the top of its source file. Therefore for tests -# to catch unicode related errors, a mock view has to be declared in a separate -# module. -class ViewWithNonASCIICharactersInDocstring(APIView): - __doc__ = UTF8_TEST_DOCSTRING - - class TestViewNamesAndDescriptions(TestCase): def test_view_name_uses_class_name(self): """ From 124ae8c2c88d48b67fbaee77e337e8a6f37d1b70 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 12:58:38 +0100 Subject: [PATCH 031/206] Tweak styling for max-height of sidenav --- docs/template.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/template.html b/docs/template.html index 14ecc9c7a..217710250 100644 --- a/docs/template.html +++ b/docs/template.html @@ -200,11 +200,11 @@ }); // Dynamically force sidenav to no higher than browser window - $('.side-nav').css('max-height', window.innerHeight - 125); + $('.side-nav').css('max-height', window.innerHeight - 130); $(function(){ $(window).resize(function(){ - $('.side-nav').css('max-height', window.innerHeight - 125); + $('.side-nav').css('max-height', window.innerHeight - 130); }); }); From 7ba2f44a0f0e5ed7bac0fbdbb0112bbfe43f6d24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 13:00:05 +0100 Subject: [PATCH 032/206] Version 2.3.6 --- docs/topics/release-notes.md | 6 ++++-- rest_framework/__init__.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4fecbf1f2..d379ab74f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,13 +40,15 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.6 + +**Date**: 27th June 2013 * Added `trailing_slash` option to routers. * Include support for `HttpStreamingResponse`. * Support wider range of default serializer validation when used with custom model fields. * UTF-8 Support for browsable API descriptions. -* OAuth2 provider usez timezone aware datetimes when supported. +* OAuth2 provider uses timezone aware datetimes when supported. * Bugfix: Return error correctly when OAuth non-existent consumer occurs. * Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. * Bugfix: Fix `ScopedRateThrottle`. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 0a2101863..776618ac3 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.5' +__version__ = '2.3.6' VERSION = __version__ # synonym From 1f6a59d76da286e7a89e8e41317beb16a4aab7c7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Jun 2013 13:41:42 +0100 Subject: [PATCH 033/206] Moar hyperlinks --- README.md | 23 ++++++++++++++++++----- docs/index.md | 16 +++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 12ed09f9f..62883e32e 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b Some reasons you might want to use REST framework: -* The Web browseable API is a huge useability win for your developers. -* Authentication policies including OAuth1a and OAuth2 out of the box. -* Serialization that supports both ORM and non-ORM data sources. -* Customizable all the way down - just use regular function-based views if you don't need the more powerful features. -* Extensive documentation, and great community support. +* The [Web browseable API][sandbox] is a huge useability win for your developers. +* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. +* [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]. There is a live example API for testing purposes, [available here][sandbox]. @@ -139,6 +139,19 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [sandbox]: http://restframework.herokuapp.com/ + +[index]: http://django-rest-framework.org/ +[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication +[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication +[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers +[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer +[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views +[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html +[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html +[routers]: http://django-rest-framework.org/api-guide/routers.html +[serializers]: http://django-rest-framework.org/api-guide/serializers.html +[authentication]: http://django-rest-framework.org/api-guide/authentication.html + [rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [image]: http://django-rest-framework.org/img/quickstart.png diff --git a/docs/index.md b/docs/index.md index b04e23465..de4b01c61 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,11 +15,11 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b Some reasons you might want to use REST framework: -* The Web browseable API is a huge usability win for your developers. -* Authentication policies including OAuth1a and OAuth2 out of the box. -* Serialization that supports both ORM and non-ORM data sources. -* Customizable all the way down - just use regular function-based views if you don't need the more powerful features. -* Extensive documentation, and great community support. +* The [Web browseable API][sandbox] is a huge usability win for your developers. +* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. +* [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]. There is a live example API for testing purposes, [available here][sandbox]. @@ -250,6 +250,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider [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 [sandbox]: http://restframework.herokuapp.com/ [quickstart]: tutorial/quickstart.md From f5f23793e34324552f323725fa25f09b34380acc Mon Sep 17 00:00:00 2001 From: Rudolf Olah Date: Thu, 27 Jun 2013 16:30:24 -0400 Subject: [PATCH 034/206] #955 updated documentation for overriding `routes` attribute in Router sub-classes --- docs/api-guide/routers.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index b74b6e13b..feff0fbfe 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -98,7 +98,7 @@ As with `SimpleRouter` the trailing slashs on the URL routes can be removed by s 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. -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 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. ## Example @@ -109,10 +109,18 @@ The following example will only route to the `list` and `retrieve` actions, and A router for read-only APIs, which doesn't use trailing suffixes. """ routes = [ - (r'^{prefix}$', {'get': 'list'}, '{basename}-list'), - (r'^{prefix}/{lookup}$', {'get': 'retrieve'}, '{basename}-detail') + Route(url=r'^{prefix}$', + mapping={'get': 'list'}, + name='{basename}-list', + initkwargs={}), + Route(url=r'^{prefix}/{lookup}$', + mapping={'get': 'retrieve'}, + name='{basename}-detail', + initkwargs={}) ] +The `SimpleRouter` class provides another example of setting the `.routes` attribute. + ## 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. From 4ee9cdc7aff30fc3f45e78292da77b5989bb0e23 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Jun 2013 09:35:52 +0100 Subject: [PATCH 035/206] Fix compat datetime import when oauth2 provide does not support timezone aware datetimes --- .gitignore | 1 + rest_framework/compat.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2255cd9aa..ae73f8379 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .* html/ +htmlcov/ coverage/ build/ dist/ diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b748dcc51..cb1228465 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -494,7 +494,8 @@ 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 From 7224b20d58ceee22abc987980ab646ab8cb2d8dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Jun 2013 17:17:39 +0100 Subject: [PATCH 036/206] Added APIRequestFactory --- rest_framework/compat.py | 38 +++++++++++- rest_framework/renderers.py | 11 ++++ rest_framework/response.py | 2 +- rest_framework/test.py | 48 +++++++++++++++ rest_framework/tests/test_authentication.py | 6 +- rest_framework/tests/test_decorators.py | 11 ++-- rest_framework/tests/test_filters.py | 4 +- rest_framework/tests/test_generics.py | 60 ++++++++----------- .../tests/test_hyperlinkedserializers.py | 11 ++-- rest_framework/tests/test_negotiation.py | 4 +- rest_framework/tests/test_pagination.py | 6 +- rest_framework/tests/test_permissions.py | 35 +++++------ .../tests/test_relations_hyperlink.py | 4 +- rest_framework/tests/test_renderers.py | 8 +-- rest_framework/tests/test_request.py | 15 +---- rest_framework/tests/test_reverse.py | 4 +- rest_framework/tests/test_routers.py | 5 +- rest_framework/tests/test_throttling.py | 6 +- rest_framework/tests/test_urlpatterns.py | 4 +- rest_framework/tests/test_validation.py | 8 +-- rest_framework/tests/test_views.py | 6 +- 21 files changed, 180 insertions(+), 116 deletions(-) create mode 100644 rest_framework/test.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index cb1228465..6f7447add 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -8,6 +8,7 @@ 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 included `six`. try: @@ -83,7 +84,6 @@ def get_concrete_model(model_cls): # Django 1.5 add support for custom auth user model if django.VERSION >= (1, 5): - from django.conf import settings AUTH_USER_MODEL = settings.AUTH_USER_MODEL else: AUTH_USER_MODEL = 'auth.User' @@ -436,6 +436,42 @@ except ImportError: return force_text(url) +# RequestFactory only provide `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 + 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): + parsed = urlparse.urlparse(path) + data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) + r = { + 'PATH_INFO': self._get_path(parsed), + 'QUERY_STRING': force_text(parsed[4]), + 'REQUEST_METHOD': str(method), + } + if data: + r.update({ + 'CONTENT_LENGTH': len(data), + 'CONTENT_TYPE': str(content_type), + 'wsgi.input': FakePayload(data), + }) + elif django.VERSION <= (1, 4): + # For 1.3 we need an empty WSGI payload + r.update({ + 'wsgi.input': FakePayload('') + }) + r.update(extra) + return self.request(**r) + # Markdown is optional try: import markdown diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8b2428ad8..d7a7ef297 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -14,6 +14,7 @@ from django import forms from django.core.exceptions import ImproperlyConfigured from django.http.multipartparser import parse_header from django.template import RequestContext, loader, Template +from django.test.client import encode_multipart from django.utils.xmlutils import SimplerXMLGenerator from rest_framework.compat import StringIO from rest_framework.compat import six @@ -571,3 +572,13 @@ class BrowsableAPIRenderer(BaseRenderer): response.status_code = status.HTTP_200_OK return ret + + +class MultiPartRenderer(BaseRenderer): + media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' + format = 'form' + charset = 'utf-8' + BOUNDARY = 'BoUnDaRyStRiNg' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return encode_multipart(self.BOUNDARY, data) diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3e..c4b2aaa66 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -50,7 +50,7 @@ class Response(SimpleTemplateResponse): charset = renderer.charset content_type = self.content_type - if content_type is None and charset is not None: + if content_type is None and charset is not None and ';' not in media_type: content_type = "{0}; charset={1}".format(media_type, charset) elif content_type is None: content_type = media_type diff --git a/rest_framework/test.py b/rest_framework/test.py new file mode 100644 index 000000000..92281cafc --- /dev/null +++ b/rest_framework/test.py @@ -0,0 +1,48 @@ +from rest_framework.compat import six, RequestFactory +from rest_framework.renderers import JSONRenderer, MultiPartRenderer + + +class APIRequestFactory(RequestFactory): + renderer_classes = { + 'json': JSONRenderer, + 'form': MultiPartRenderer + } + default_format = 'form' + + def __init__(self, format=None, **defaults): + self.format = format or self.default_format + super(APIRequestFactory, self).__init__(**defaults) + + def _encode_data(self, data, format, content_type): + if not data: + return ('', None) + + format = format or self.format + + if content_type is None and data is not None: + renderer = self.renderer_classes[format]() + data = renderer.render(data) + # Determine the content-type header + if ';' in renderer.media_type: + content_type = renderer.media_type + else: + content_type = "{0}; charset={1}".format( + renderer.media_type, renderer.charset + ) + # Coerce text to bytes if required. + if isinstance(data, six.text_type): + data = bytes(data.encode(renderer.charset)) + + return data, content_type + + def post(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('POST', path, data, content_type, **extra) + + def put(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('PUT', path, data, content_type, **extra) + + def patch(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('PATCH', path, data, content_type, **extra) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 6a50be064..f2c51c68f 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -21,14 +21,14 @@ 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.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -import json import base64 import time import datetime +import json -factory = RequestFactory() +factory = APIRequestFactory() class MockView(APIView): diff --git a/rest_framework/tests/test_decorators.py b/rest_framework/tests/test_decorators.py index 1016fed3f..195f0ba3e 100644 --- a/rest_framework/tests/test_decorators.py +++ b/rest_framework/tests/test_decorators.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework import status +from rest_framework.authentication import BasicAuthentication +from rest_framework.parsers import JSONParser +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.renderers import JSONRenderer -from rest_framework.parsers import JSONParser -from rest_framework.authentication import BasicAuthentication +from rest_framework.test import APIRequestFactory from rest_framework.throttling import UserRateThrottle -from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.decorators import ( api_view, @@ -17,13 +18,11 @@ from rest_framework.decorators import ( permission_classes, ) -from rest_framework.tests.utils import RequestFactory - class DecoratorTestCase(TestCase): def setUp(self): - self.factory = RequestFactory() + self.factory = APIRequestFactory() def _finalize_response(self, request, response, *args, **kwargs): response.request = request diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index aaed62478..c9d9e7ffa 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -4,13 +4,13 @@ from decimal import Decimal from django.db import models from django.core.urlresolvers import reverse from django.test import TestCase -from django.test.client import RequestFactory 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.test import APIRequestFactory from rest_framework.tests.models import BasicModel -factory = RequestFactory() +factory = APIRequestFactory() class FilterableItem(models.Model): diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 37734195a..1550880b5 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -3,12 +3,11 @@ from django.db import models from django.shortcuts import get_object_or_404 from django.test import TestCase from rest_framework import generics, renderers, serializers, status -from rest_framework.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel from rest_framework.compat import six -import json -factory = RequestFactory() +factory = APIRequestFactory() class RootView(generics.ListCreateAPIView): @@ -71,9 +70,8 @@ class TestRootView(TestCase): """ POST requests to ListCreateAPIView should create a new object. """ - content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -85,9 +83,8 @@ class TestRootView(TestCase): """ PUT requests to ListCreateAPIView should not be allowed """ - content = {'text': 'foobar'} - request = factory.put('/', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.put('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) @@ -148,9 +145,8 @@ class TestRootView(TestCase): """ POST requests to create a new object should not be able to set the id. """ - content = {'id': 999, 'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + data = {'id': 999, 'text': 'foobar'} + request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -189,9 +185,8 @@ class TestInstanceView(TestCase): """ POST requests to RetrieveUpdateDestroyAPIView should not be allowed """ - content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.post('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) @@ -201,9 +196,8 @@ class TestInstanceView(TestCase): """ PUT requests to RetrieveUpdateDestroyAPIView should update an object. """ - content = {'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.put('/1', data, format='json') with self.assertNumQueries(2): response = self.view(request, pk='1').render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -215,9 +209,8 @@ class TestInstanceView(TestCase): """ PATCH requests to RetrieveUpdateDestroyAPIView should update an object. """ - content = {'text': 'foobar'} - request = factory.patch('/1', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.patch('/1', data, format='json') with self.assertNumQueries(2): response = self.view(request, pk=1).render() @@ -293,9 +286,8 @@ class TestInstanceView(TestCase): """ PUT requests to create a new object should not be able to set the id. """ - content = {'id': 999, 'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + data = {'id': 999, 'text': 'foobar'} + request = factory.put('/1', data, format='json') with self.assertNumQueries(2): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -309,9 +301,8 @@ class TestInstanceView(TestCase): if it does not currently exist. """ self.objects.get(id=1).delete() - content = {'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.put('/1', data, format='json') with self.assertNumQueries(3): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -324,10 +315,9 @@ class TestInstanceView(TestCase): PUT requests to RetrieveUpdateDestroyAPIView should create an object at the requested url if it doesn't exist. """ - content = {'text': 'foobar'} + data = {'text': 'foobar'} # pk fields can not be created on demand, only the database can set the pk for a new object - request = factory.put('/5', json.dumps(content), - content_type='application/json') + request = factory.put('/5', data, format='json') with self.assertNumQueries(3): response = self.view(request, pk=5).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -339,9 +329,8 @@ class TestInstanceView(TestCase): PUT requests to RetrieveUpdateDestroyAPIView should create an object at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. """ - content = {'text': 'foobar'} - request = factory.put('/test_slug', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.put('/test_slug', data, format='json') with self.assertNumQueries(2): response = self.slug_based_view(request, slug='test_slug').render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -415,9 +404,8 @@ class TestCreateModelWithAutoNowAddField(TestCase): https://github.com/tomchristie/django-rest-framework/issues/285 """ - content = {'email': 'foobar@example.com', 'content': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + data = {'email': 'foobar@example.com', 'content': 'foobar'} + request = factory.post('/', data, format='json') response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) created = self.objects.get(id=1) diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py index 129600cb4..61e613d75 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -1,12 +1,15 @@ from __future__ import unicode_literals import json from django.test import TestCase -from django.test.client import RequestFactory from rest_framework import generics, status, serializers from rest_framework.compat import patterns, url -from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel +from rest_framework.test import APIRequestFactory +from rest_framework.tests.models import ( + Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, + Album, Photo, OptionalRelationModel +) -factory = RequestFactory() +factory = APIRequestFactory() class BlogPostCommentSerializer(serializers.ModelSerializer): @@ -21,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(), slug_field='title', slug_url_kwarg='title') + album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title') def restore_object(self, attrs, instance=None): return Photo(**attrs) diff --git a/rest_framework/tests/test_negotiation.py b/rest_framework/tests/test_negotiation.py index 7f84827f0..04b89eb60 100644 --- a/rest_framework/tests/test_negotiation.py +++ b/rest_framework/tests/test_negotiation.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory from rest_framework.negotiation import DefaultContentNegotiation from rest_framework.request import Request from rest_framework.renderers import BaseRenderer +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() class MockJSONRenderer(BaseRenderer): diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index e538a78e5..85d4640ea 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -4,13 +4,13 @@ from decimal import Decimal from django.db import models from django.core.paginator import Paginator from django.test import TestCase -from django.test.client import RequestFactory from django.utils import unittest from rest_framework import generics, status, pagination, filters, serializers from rest_framework.compat import django_filters +from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel -factory = RequestFactory() +factory = APIRequestFactory() class FilterableItem(models.Model): @@ -369,7 +369,7 @@ class TestCustomPaginationSerializer(TestCase): self.page = paginator.page(1) def test_custom_pagination_serializer(self): - request = RequestFactory().get('/foobar') + request = APIRequestFactory().get('/foobar') serializer = CustomPaginationSerializer( instance=self.page, context={'request': request} diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index 6caaf65b0..e2cca3808 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -3,11 +3,10 @@ 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.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory import base64 -import json -factory = RequestFactory() +factory = APIRequestFactory() class BasicModel(models.Model): @@ -56,15 +55,13 @@ class ModelPermissionsIntegrationTests(TestCase): BasicModel(text='foo').save() def test_has_create_permissions(self): - request = factory.post('/', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.post('/', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.permitted_credentials) response = root_view(request, pk=1) self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_has_put_permissions(self): - request = factory.put('/1', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.put('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.permitted_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -75,15 +72,13 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) def test_does_not_have_create_permissions(self): - request = factory.post('/', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.post('/', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.disallowed_credentials) response = root_view(request, pk=1) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_does_not_have_put_permissions(self): - request = factory.put('/1', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.put('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.disallowed_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -95,28 +90,26 @@ class ModelPermissionsIntegrationTests(TestCase): def test_has_put_as_create_permissions(self): # User only has update permissions - should be able to update an entity. - request = factory.put('/1', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.put('/1', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.updateonly_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) # But if PUTing to a new entity, permission should be denied. - request = factory.put('/2', json.dumps({'text': 'foobar'}), - content_type='application/json', + request = factory.put('/2', {'text': 'foobar'}, format='json', HTTP_AUTHORIZATION=self.updateonly_credentials) response = instance_view(request, pk='2') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_options_permitted(self): - request = factory.options('/', content_type='application/json', + request = factory.options('/', HTTP_AUTHORIZATION=self.permitted_credentials) response = root_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIn('actions', response.data) self.assertEqual(list(response.data['actions'].keys()), ['POST']) - request = factory.options('/1', content_type='application/json', + request = factory.options('/1', HTTP_AUTHORIZATION=self.permitted_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -124,26 +117,26 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(list(response.data['actions'].keys()), ['PUT']) def test_options_disallowed(self): - request = factory.options('/', content_type='application/json', + request = factory.options('/', HTTP_AUTHORIZATION=self.disallowed_credentials) response = root_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertNotIn('actions', response.data) - request = factory.options('/1', content_type='application/json', + request = factory.options('/1', HTTP_AUTHORIZATION=self.disallowed_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertNotIn('actions', response.data) def test_options_updateonly(self): - request = factory.options('/', content_type='application/json', + request = factory.options('/', HTTP_AUTHORIZATION=self.updateonly_credentials) response = root_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertNotIn('actions', response.data) - request = factory.options('/1', content_type='application/json', + request = factory.options('/1', HTTP_AUTHORIZATION=self.updateonly_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/tests/test_relations_hyperlink.py b/rest_framework/tests/test_relations_hyperlink.py index 2ca7f4f2b..3c4d39af6 100644 --- a/rest_framework/tests/test_relations_hyperlink.py +++ b/rest_framework/tests/test_relations_hyperlink.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory 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, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource ) -factory = RequestFactory() +factory = APIRequestFactory() request = factory.get('/') # Just to ensure we have a request in the serializer context diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 95b597411..df6f4aa63 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -4,19 +4,17 @@ from __future__ import unicode_literals from decimal import Decimal from django.core.cache import cache from django.test import TestCase -from django.test.client import RequestFactory 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 +from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings -from rest_framework.compat import StringIO -from rest_framework.compat import six +from rest_framework.test import APIRequestFactory import datetime import pickle import re @@ -121,7 +119,7 @@ class POSTDeniedView(APIView): class DocumentingRendererTests(TestCase): def test_only_permitted_forms_are_displayed(self): view = POSTDeniedView.as_view() - request = RequestFactory().get('/') + request = APIRequestFactory().get('/') response = view(request).render() self.assertNotContains(response, '>POST<') self.assertContains(response, '>PUT<') diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index a5c5e84ce..8d64d79f2 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -6,7 +6,6 @@ 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, Client -from django.test.client import RequestFactory from rest_framework import status from rest_framework.authentication import SessionAuthentication from rest_framework.compat import patterns @@ -19,12 +18,13 @@ from rest_framework.parsers import ( from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings +from rest_framework.test import APIRequestFactory from rest_framework.views import APIView from rest_framework.compat import six import json -factory = RequestFactory() +factory = APIRequestFactory() class PlainTextParser(BaseParser): @@ -116,16 +116,7 @@ class TestContentParsing(TestCase): Ensure request.DATA returns content for PUT request with form content. """ data = {'qwerty': 'uiop'} - - from django import VERSION - - if VERSION >= (1, 5): - from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart - request = Request(factory.put('/', encode_multipart(BOUNDARY, data), - content_type=MULTIPART_CONTENT)) - else: - request = Request(factory.put('/', data)) - + request = Request(factory.put('/', data)) request.parsers = (FormParser(), MultiPartParser()) self.assertEqual(list(request.DATA.items()), list(data.items())) diff --git a/rest_framework/tests/test_reverse.py b/rest_framework/tests/test_reverse.py index 93ef56377..690a30b11 100644 --- a/rest_framework/tests/test_reverse.py +++ b/rest_framework/tests/test_reverse.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory from rest_framework.compat import patterns, url from rest_framework.reverse import reverse +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() def null_view(request): diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index d375f4a8c..5fcccb741 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals from django.db import models from django.test import TestCase -from django.test.client import RequestFactory 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.response import Response from rest_framework.routers import SimpleRouter, DefaultRouter +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() urlpatterns = patterns('',) @@ -193,6 +193,7 @@ class TestActionKeywordArgs(TestCase): {'permission_classes': [permissions.AllowAny]} ) + class TestActionAppliedToExistingRoute(TestCase): """ Ensure `@action` decorator raises an except when applied diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py index d35d37092..19bc691ae 100644 --- a/rest_framework/tests/test_throttling.py +++ b/rest_framework/tests/test_throttling.py @@ -5,7 +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 django.test.client import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.views import APIView from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle from rest_framework.response import Response @@ -41,7 +41,7 @@ class ThrottlingTests(TestCase): Reset the cache so that no throttles will be active """ cache.clear() - self.factory = RequestFactory() + self.factory = APIRequestFactory() def test_requests_are_throttled(self): """ @@ -173,7 +173,7 @@ class ScopedRateThrottleTests(TestCase): return Response('y') self.throttle_class = XYScopedRateThrottle - self.factory = RequestFactory() + self.factory = APIRequestFactory() self.x_view = XView.as_view() self.y_view = YView.as_view() self.unscoped_view = UnscopedView.as_view() diff --git a/rest_framework/tests/test_urlpatterns.py b/rest_framework/tests/test_urlpatterns.py index 29ed4a961..8132ec4c8 100644 --- a/rest_framework/tests/test_urlpatterns.py +++ b/rest_framework/tests/test_urlpatterns.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from collections import namedtuple from django.core import urlresolvers from django.test import TestCase -from django.test.client import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.compat import patterns, url, include from rest_framework.urlpatterns import format_suffix_patterns @@ -20,7 +20,7 @@ class FormatSuffixTests(TestCase): Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters. """ def _resolve_urlpatterns(self, urlpatterns, test_paths): - factory = RequestFactory() + factory = APIRequestFactory() try: urlpatterns = format_suffix_patterns(urlpatterns) except Exception: diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index a6ec0e993..ebfdff9cd 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -2,10 +2,9 @@ from __future__ import unicode_literals from django.db import models from django.test import TestCase from rest_framework import generics, serializers, status -from rest_framework.tests.utils import RequestFactory -import json +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() # Regression for #666 @@ -33,8 +32,7 @@ class TestPreSaveValidationExclusions(TestCase): validation on read only fields. """ obj = ValidationModel.objects.create(blank_validated_field='') - request = factory.put('/', json.dumps({}), - content_type='application/json') + request = factory.put('/', {}, format='json') view = UpdateValidationModel().as_view() response = view(request, pk=obj.pk).render() self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py index 2767d24c8..c0bec5aed 100644 --- a/rest_framework/tests/test_views.py +++ b/rest_framework/tests/test_views.py @@ -1,17 +1,15 @@ from __future__ import unicode_literals import copy - from django.test import TestCase -from django.test.client import RequestFactory - from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.settings import api_settings +from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -factory = RequestFactory() +factory = APIRequestFactory() class BasicView(APIView): From f585480ee10f4b5e61db4ac343b1d2af25d2de97 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Jun 2013 17:50:30 +0100 Subject: [PATCH 037/206] Added APIClient --- rest_framework/test.py | 83 +++++++++++++++++---- rest_framework/tests/test_authentication.py | 44 +++++------ rest_framework/tests/test_request.py | 6 +- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 92281cafc..9fce2c08b 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,39 +1,54 @@ -from rest_framework.compat import six, RequestFactory +# Note that we use `DjangoRequestFactory` and `DjangoClient` names in order +# to make it harder for the user to import the wrong thing without realizing. +from django.conf import settings +from django.test.client import Client as DjangoClient +from rest_framework.compat import RequestFactory as DjangoRequestFactory +from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.renderers import JSONRenderer, MultiPartRenderer -class APIRequestFactory(RequestFactory): +class APIRequestFactory(DjangoRequestFactory): renderer_classes = { 'json': JSONRenderer, 'form': MultiPartRenderer } default_format = 'form' - def __init__(self, format=None, **defaults): - self.format = format or self.default_format - super(APIRequestFactory, self).__init__(**defaults) + def _encode_data(self, data, format=None, content_type=None): + """ + Encode the data returning a two tuple of (bytes, content_type) + """ - def _encode_data(self, data, format, content_type): if not data: return ('', None) - format = format or self.format + assert format is None or content_type is None, ( + 'You may not set both `format` and `content_type`.' + ) - if content_type is None and data is not None: + if content_type: + # Content type specified explicitly, treat data as a raw bytestring + ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) + + else: + # Use format and render the data into a bytestring + format = format or self.default_format renderer = self.renderer_classes[format]() - data = renderer.render(data) - # Determine the content-type header + ret = renderer.render(data) + + # Determine the content-type header from the renderer if ';' in renderer.media_type: content_type = renderer.media_type else: content_type = "{0}; charset={1}".format( renderer.media_type, renderer.charset ) - # Coerce text to bytes if required. - if isinstance(data, six.text_type): - data = bytes(data.encode(renderer.charset)) - return data, content_type + # Coerce text to bytes if required. + if isinstance(ret, six.text_type): + ret = bytes(ret.encode(renderer.charset)) + + return ret, content_type def post(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) @@ -46,3 +61,43 @@ class APIRequestFactory(RequestFactory): def patch(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) return self.generic('PATCH', path, data, content_type, **extra) + + def delete(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('DELETE', path, data, content_type, **extra) + + def options(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('OPTIONS', path, data, content_type, **extra) + + +class APIClient(APIRequestFactory, DjangoClient): + def post(self, path, data=None, format=None, content_type=None, follow=False, **extra): + response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def put(self, path, data=None, format=None, content_type=None, follow=False, **extra): + response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def patch(self, path, data=None, format=None, content_type=None, follow=False, **extra): + response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def delete(self, path, data=None, format=None, content_type=None, follow=False, **extra): + response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def options(self, path, data=None, format=None, content_type=None, follow=False, **extra): + response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index f2c51c68f..a44813b69 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse -from django.test import Client, TestCase +from django.test import TestCase from django.utils import unittest from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions @@ -21,12 +21,11 @@ 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 +from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView import base64 import time import datetime -import json factory = APIRequestFactory() @@ -68,7 +67,7 @@ class BasicAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -87,7 +86,7 @@ class BasicAuthTests(TestCase): credentials = ('%s:%s' % (self.username, self.password)) base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) auth = 'Basic %s' % base64_credentials - response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) + response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_post_form_failing_basic_auth(self): @@ -97,7 +96,7 @@ class BasicAuthTests(TestCase): def test_post_json_failing_basic_auth(self): """Ensure POSTing json over basic auth without correct credentials fails""" - response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json') + response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json') self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"') @@ -107,8 +106,8 @@ class SessionAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) - self.non_csrf_client = Client(enforce_csrf_checks=False) + self.csrf_client = APIClient(enforce_csrf_checks=True) + self.non_csrf_client = APIClient(enforce_csrf_checks=False) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -154,7 +153,7 @@ class TokenAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -172,7 +171,7 @@ class TokenAuthTests(TestCase): def test_post_json_passing_token_auth(self): """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF""" auth = "Token " + self.key - response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) + response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_post_form_failing_token_auth(self): @@ -182,7 +181,7 @@ class TokenAuthTests(TestCase): def test_post_json_failing_token_auth(self): """Ensure POSTing json over token auth without correct credentials fails""" - response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json') + response = self.csrf_client.post('/token/', {'example': 'example'}, format='json') self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_token_has_auto_assigned_key_if_none_provided(self): @@ -193,33 +192,33 @@ class TokenAuthTests(TestCase): def test_token_login_json(self): """Ensure token login view using JSON POST works.""" - client = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username, 'password': self.password}), 'application/json') + {'username': self.username, 'password': self.password}, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + self.assertEqual(response.data['token'], self.key) def test_token_login_json_bad_creds(self): """Ensure token login view using JSON POST fails if bad credentials are used.""" - client = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + {'username': self.username, 'password': "badpass"}, format='json') self.assertEqual(response.status_code, 400) def test_token_login_json_missing_fields(self): """Ensure token login view using JSON POST fails if missing fields.""" - client = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username}), 'application/json') + {'username': self.username}, format='json') self.assertEqual(response.status_code, 400) def test_token_login_form(self): """Ensure token login view using form POST works.""" - client = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + self.assertEqual(response.data['token'], self.key) class IncorrectCredentialsTests(TestCase): @@ -256,7 +255,7 @@ class OAuthTests(TestCase): self.consts = consts - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -470,12 +469,13 @@ class OAuthTests(TestCase): response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) + class OAuth2Tests(TestCase): """OAuth 2.0 authentication""" urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 8d64d79f2..969d8024a 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -5,7 +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.test import TestCase, Client +from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication from rest_framework.compat import patterns @@ -18,7 +18,7 @@ from rest_framework.parsers import ( from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings -from rest_framework.test import APIRequestFactory +from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework.compat import six import json @@ -248,7 +248,7 @@ class TestContentParsingWithAuthentication(TestCase): urls = 'rest_framework.tests.test_request' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' From 90bc07f3f160485001ea329e5f69f7e521d14ec9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:05:08 +0100 Subject: [PATCH 038/206] Addeded 'APITestClient.credentials()' --- rest_framework/test.py | 29 +++++++++++++++++++++++++ rest_framework/tests/test_testing.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 rest_framework/tests/test_testing.py diff --git a/rest_framework/test.py b/rest_framework/test.py index 9fce2c08b..8115fa0d2 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,5 +1,8 @@ +# -- coding: utf-8 -- + # Note that we use `DjangoRequestFactory` and `DjangoClient` names in order # to make it harder for the user to import the wrong thing without realizing. +from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient from rest_framework.compat import RequestFactory as DjangoRequestFactory @@ -72,31 +75,57 @@ class APIRequestFactory(DjangoRequestFactory): class APIClient(APIRequestFactory, DjangoClient): + def __init__(self, *args, **kwargs): + self._credentials = {} + super(APIClient, self).__init__(*args, **kwargs) + + def credentials(self, **kwargs): + self._credentials = kwargs + + def get(self, path, data={}, follow=False, **extra): + extra.update(self._credentials) + response = super(APIClient, self).get(path, data=data, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def head(self, path, data={}, follow=False, **extra): + extra.update(self._credentials) + response = super(APIClient, self).head(path, data=data, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + def post(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response def put(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response def patch(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response def delete(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response def options(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py new file mode 100644 index 000000000..71dacd38e --- /dev/null +++ b/rest_framework/tests/test_testing.py @@ -0,0 +1,32 @@ +# -- coding: utf-8 -- + +from __future__ import unicode_literals +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 + + +@api_view(['GET']) +def mirror(request): + return Response({ + 'auth': request.META.get('HTTP_AUTHORIZATION', b'') + }) + + +urlpatterns = patterns('', + url(r'^view/$', mirror), +) + + +class CheckTestClient(TestCase): + urls = 'rest_framework.tests.test_testing' + + def setUp(self): + self.client = APIClient() + + def test_credentials(self): + self.client.credentials(HTTP_AUTHORIZATION='example') + response = self.client.get('/view/') + self.assertEqual(response.data['auth'], 'example') From f7db06953bd8ad7f5e0211f49a04e8d5bb634380 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:06:11 +0100 Subject: [PATCH 039/206] Remove unneeded tests.utils, superseeded by APIRequestFactory, APIClient --- rest_framework/tests/utils.py | 40 ----------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 rest_framework/tests/utils.py diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py deleted file mode 100644 index 8c87917d9..000000000 --- a/rest_framework/tests/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import unicode_literals -from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory -from django.test.client import MULTIPART_CONTENT -from rest_framework.compat import urlparse - - -class RequestFactory(_RequestFactory): - - def __init__(self, **defaults): - super(RequestFactory, self).__init__(**defaults) - - def patch(self, path, data={}, content_type=MULTIPART_CONTENT, - **extra): - "Construct a PATCH request." - - patch_data = self._encode_data(data, content_type) - - parsed = urlparse.urlparse(path) - r = { - 'CONTENT_LENGTH': len(patch_data), - 'CONTENT_TYPE': content_type, - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'PATCH', - 'wsgi.input': FakePayload(patch_data), - } - r.update(extra) - return self.request(**r) - - -class Client(_Client, RequestFactory): - def patch(self, path, data={}, content_type=MULTIPART_CONTENT, - follow=False, **extra): - """ - Send a resource to the server using PATCH. - """ - response = super(Client, self).patch(path, data=data, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response From 35022ca9213939a2f40c82facffa908a818efe0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:14:05 +0100 Subject: [PATCH 040/206] Refactor SessionAuthentication slightly --- rest_framework/authentication.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 102980271..b42162dd9 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -26,6 +26,12 @@ def get_authorization_header(request): return auth +class CSRFCheck(CsrfViewMiddleware): + def _reject(self, request, reason): + # Return the failure reason instead of an HttpResponse + return reason + + class BaseAuthentication(object): """ All authentication classes should extend BaseAuthentication. @@ -110,20 +116,20 @@ class SessionAuthentication(BaseAuthentication): if not user or not user.is_active: return None - # Enforce CSRF validation for session based authentication. - class CSRFCheck(CsrfViewMiddleware): - def _reject(self, request, reason): - # Return the failure reason instead of an HttpResponse - return reason - - reason = CSRFCheck().process_view(http_request, None, (), {}) - if reason: - # CSRF failed, bail with explicit error message - raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) + self.enforce_csrf(http_request) # CSRF passed with authenticated user return (user, None) + def enforce_csrf(self, request): + """ + Enforce CSRF validation for session based authentication. + """ + reason = CSRFCheck().process_view(request, None, (), {}) + if reason: + # CSRF failed, bail with explicit error message + raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) + class TokenAuthentication(BaseAuthentication): """ From 664f8c63655770cd90bdbd510b315bcd045b380a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 21:02:58 +0100 Subject: [PATCH 041/206] Added APIClient.authenticate() --- rest_framework/renderers.py | 2 +- rest_framework/request.py | 20 +++++++++++++ rest_framework/test.py | 39 +++++++++++++++++++++++--- rest_framework/tests/test_testing.py | 42 ++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index d7a7ef297..3a03ca332 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -576,7 +576,7 @@ class BrowsableAPIRenderer(BaseRenderer): class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' - format = 'form' + format = 'multipart' charset = 'utf-8' BOUNDARY = 'BoUnDaRyStRiNg' diff --git a/rest_framework/request.py b/rest_framework/request.py index 0d88ebc7e..919716f49 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -64,6 +64,20 @@ def clone_request(request, method): return ret +class ForcedAuthentication(object): + """ + This authentication class is used if the test client or request factory + forcibly authenticated the request. + """ + + def __init__(self, force_user, force_token): + self.force_user = force_user + self.force_token = force_token + + def authenticate(self, request): + return (self.force_user, self.force_token) + + class Request(object): """ Wrapper allowing to enhance a standard `HttpRequest` instance. @@ -98,6 +112,12 @@ class Request(object): self.parser_context['request'] = self self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET + force_user = getattr(request, '_force_auth_user', None) + force_token = getattr(request, '_force_auth_token', None) + if (force_user is not None or force_token is not None): + forced_auth = ForcedAuthentication(force_user, force_token) + self.authenticators = (forced_auth,) + def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() diff --git a/rest_framework/test.py b/rest_framework/test.py index 8115fa0d2..08de2297b 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient +from django.test.client import ClientHandler from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.renderers import JSONRenderer, MultiPartRenderer @@ -13,9 +14,9 @@ from rest_framework.renderers import JSONRenderer, MultiPartRenderer class APIRequestFactory(DjangoRequestFactory): renderer_classes = { 'json': JSONRenderer, - 'form': MultiPartRenderer + 'multipart': MultiPartRenderer } - default_format = 'form' + default_format = 'multipart' def _encode_data(self, data, format=None, content_type=None): """ @@ -74,14 +75,44 @@ class APIRequestFactory(DjangoRequestFactory): return self.generic('OPTIONS', path, data, content_type, **extra) -class APIClient(APIRequestFactory, DjangoClient): +class ForceAuthClientHandler(ClientHandler): + """ + A patched version of ClientHandler that can enforce authentication + on the outgoing requests. + """ + def __init__(self, *args, **kwargs): + self._force_auth_user = None + self._force_auth_token = None + super(ForceAuthClientHandler, self).__init__(*args, **kwargs) + + def force_authenticate(self, user=None, token=None): + self._force_auth_user = user + self._force_auth_token = token + + def get_response(self, request): + # This is the simplest place we can hook into to patch the + # request object. + request._force_auth_user = self._force_auth_user + request._force_auth_token = self._force_auth_token + return super(ForceAuthClientHandler, self).get_response(request) + + +class APIClient(APIRequestFactory, DjangoClient): + def __init__(self, enforce_csrf_checks=False, **defaults): + # Note that our super call skips Client.__init__ + # since we don't need to instantiate a regular ClientHandler + super(DjangoClient, self).__init__(**defaults) + self.handler = ForceAuthClientHandler(enforce_csrf_checks) + self.exc_info = None self._credentials = {} - super(APIClient, self).__init__(*args, **kwargs) def credentials(self, **kwargs): self._credentials = kwargs + def authenticate(self, user=None, token=None): + self.handler.force_authenticate(user, token) + def get(self, path, data={}, follow=False, **extra): extra.update(self._credentials) response = super(APIClient, self).get(path, data=data, **extra) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 71dacd38e..a8398b9a4 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -1,6 +1,7 @@ # -- coding: utf-8 -- from __future__ import unicode_literals +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 @@ -8,10 +9,11 @@ from rest_framework.response import Response from rest_framework.test import APIClient -@api_view(['GET']) +@api_view(['GET', 'POST']) def mirror(request): return Response({ - 'auth': request.META.get('HTTP_AUTHORIZATION', b'') + 'auth': request.META.get('HTTP_AUTHORIZATION', b''), + 'user': request.user.username }) @@ -27,6 +29,40 @@ class CheckTestClient(TestCase): self.client = APIClient() def test_credentials(self): + """ + Setting `.credentials()` adds the required headers to each request. + """ self.client.credentials(HTTP_AUTHORIZATION='example') + for _ in range(0, 3): + response = self.client.get('/view/') + self.assertEqual(response.data['auth'], 'example') + + def test_authenticate(self): + """ + Setting `.authenticate()` forcibly authenticates each request. + """ + user = User.objects.create_user('example', 'example@example.com') + self.client.authenticate(user) response = self.client.get('/view/') - self.assertEqual(response.data['auth'], 'example') + self.assertEqual(response.data['user'], 'example') + + def test_csrf_exempt_by_default(self): + """ + By default, the test client is CSRF exempt. + """ + User.objects.create_user('example', 'example@example.com', 'password') + self.client.login(username='example', password='password') + response = self.client.post('/view/') + self.assertEqual(response.status_code, 200) + + def test_explicitly_enforce_csrf_checks(self): + """ + The test client can enforce CSRF checks. + """ + client = APIClient(enforce_csrf_checks=True) + User.objects.create_user('example', 'example@example.com', 'password') + client.login(username='example', password='password') + response = client.post('/view/') + expected = {'detail': 'CSRF Failed: CSRF cookie not set.'} + self.assertEqual(response.status_code, 403) + self.assertEqual(response.data, expected) From ab799ccc3ee473de61ec35c6f745c6952752c522 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 21:34:47 +0100 Subject: [PATCH 042/206] Simplify APIClient implementation --- rest_framework/authentication.py | 6 +-- rest_framework/test.py | 66 +++++++------------------------- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index b42162dd9..cf001a24d 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -109,14 +109,14 @@ class SessionAuthentication(BaseAuthentication): """ # Get the underlying HttpRequest object - http_request = request._request - user = getattr(http_request, 'user', None) + request = request._request + user = getattr(request, 'user', None) # Unauthenticated, CSRF validation not required if not user or not user.is_active: return None - self.enforce_csrf(http_request) + self.enforce_csrf(request) # CSRF passed with authenticated user return (user, None) diff --git a/rest_framework/test.py b/rest_framework/test.py index 08de2297b..2e9cfe096 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -86,10 +86,6 @@ class ForceAuthClientHandler(ClientHandler): self._force_auth_token = None super(ForceAuthClientHandler, self).__init__(*args, **kwargs) - def force_authenticate(self, user=None, token=None): - self._force_auth_user = user - self._force_auth_token = token - def get_response(self, request): # This is the simplest place we can hook into to patch the # request object. @@ -108,56 +104,20 @@ class APIClient(APIRequestFactory, DjangoClient): self._credentials = {} def credentials(self, **kwargs): + """ + Sets headers that will be used on every outgoing request. + """ self._credentials = kwargs def authenticate(self, user=None, token=None): - self.handler.force_authenticate(user, token) + """ + Forcibly authenticates outgoing requests with the given + user and/or token. + """ + self.handler._force_auth_user = user + self.handler._force_auth_token = token - def get(self, path, data={}, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).get(path, data=data, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def head(self, path, data={}, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).head(path, data=data, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def post(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def put(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def patch(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def delete(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def options(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response + def request(self, **request): + # Ensure that any credentials set get added to every request. + request.update(self._credentials) + return super(APIClient, self).request(**request) From c9485c783a555516e41068996258f4c5e383523b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 22:53:15 +0100 Subject: [PATCH 043/206] Rename to force_authenticate --- rest_framework/test.py | 2 +- rest_framework/tests/test_testing.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 2e9cfe096..2f658a56c 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -109,7 +109,7 @@ class APIClient(APIRequestFactory, DjangoClient): """ self._credentials = kwargs - def authenticate(self, user=None, token=None): + def force_authenticate(self, user=None, token=None): """ Forcibly authenticates outgoing requests with the given user and/or token. diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index a8398b9a4..3706f38c2 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -37,12 +37,12 @@ class CheckTestClient(TestCase): response = self.client.get('/view/') self.assertEqual(response.data['auth'], 'example') - def test_authenticate(self): + def test_force_authenticate(self): """ - Setting `.authenticate()` forcibly authenticates each request. + Setting `.force_authenticate()` forcibly authenticates each request. """ user = User.objects.create_user('example', 'example@example.com') - self.client.authenticate(user) + self.client.force_authenticate(user) response = self.client.get('/view/') self.assertEqual(response.data['user'], 'example') From d31d7c18676b6292e8dc688b61913d572eccde91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 22:53:27 +0100 Subject: [PATCH 044/206] First pass at testing docs --- docs/api-guide/testing.md | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/api-guide/testing.md diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md new file mode 100644 index 000000000..f3a092a9d --- /dev/null +++ b/docs/api-guide/testing.md @@ -0,0 +1,97 @@ + + +# Testing + +> Code without tests is broken as designed +> +> — [Jacob Kaplan-Moss][cite] + +REST framework includes a few helper classes that extend Django's existing test framework, and improve support for making API requests. + +# APIRequestFactory + +Extends Django's existing `RequestFactory`. + +**TODO**: Document making requests. Note difference on form PUT requests. Document configuration. + +# APIClient + +Extends Django's existing `Client`. + +### .login(**kwargs) + +The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`. + + # Make all requests in the context of a logged in session. + >>> client = APIClient() + >>> client.login(username='lauren', password='secret') + +To logout, call the `logout` method as usual. + + # Log out + >>> client.logout() + +The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API. + +### .credentials(**kwargs) + +The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. + + # Include an appropriate `Authorization:` header on all requests. + >>> token = Token.objects.get(username='lauren') + >>> client = APIClient() + >>> client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) + +Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments. + + # Stop including any credentials + >>> client.credentials() + +The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes. + +### .force_authenticate(user=None, token=None) + +Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated. + +This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests. + + >>> user = User.objects.get(username='lauren') + >>> client = APIClient() + >>> client.force_authenticate(user=user) + +To unauthenticate subsequant requests, call `force_authenticate` setting the user and/or token to `None`. + + >>> client.force_authenticate(user=None) + +### Making requests + +**TODO**: Document requests similarly to `APIRequestFactory` + +# Testing responses + +### Using request.data + +When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. + +For example, it's easier to inspect `request.data`: + + response = self.client.get('/users/4/') + self.assertEqual(response.data, {'id': 4, 'username': 'lauren'}) + +Instead of inspecting the result of parsing `request.content`: + + response = self.client.get('/users/4/') + self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) + +### Rendering responses + +If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response. + + view = UserDetail.as_view() + request = factory.get('/users/4') + response = view(request, pk='4') + response.render() # Cannot access `response.content` without this. + self.assertEqual(response.content, '{"username": "lauren", "id": 4}') + + +[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper \ No newline at end of file From 0a722de171b0e80ac26d8c77b8051a4170bdb4c6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 13:59:05 +0100 Subject: [PATCH 045/206] Complete testing docs --- docs/api-guide/renderers.md | 11 +++ docs/api-guide/settings.md | 27 +++++ docs/api-guide/testing.md | 143 +++++++++++++++++++++++++-- rest_framework/response.py | 2 +- rest_framework/settings.py | 8 ++ rest_framework/test.py | 70 ++++++++----- rest_framework/tests/test_testing.py | 55 ++++++++++- 7 files changed, 274 insertions(+), 42 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b627c9306..869bdc16a 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -217,6 +217,16 @@ Renders data into HTML for the Browsable API. This renderer will determine whic **.charset**: `utf-8` +## MultiPartRenderer + +This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing]. + +**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg` + +**.format**: `'.multipart'` + +**.charset**: `utf-8` + --- # Custom renderers @@ -373,6 +383,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 +[testing]: testing.md [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [application/vnd.github+json]: http://developer.github.com/v3/media/ diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 4a5164c9b..7b114983d 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -149,6 +149,33 @@ Default: `None` --- +## Test settings + +*The following settings control the behavior of APIRequestFactory and APIClient* + +#### TEST_REQUEST_DEFAULT_FORMAT + +The default format that should be used when making test requests. + +This should match up with the format of one of the renderer classes in the `TEST_REQUEST_RENDERER_CLASSES` setting. + +Default: `'multipart'` + +#### TEST_REQUEST_RENDERER_CLASSES + +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')` + +Default: + + ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer' + ) + +--- + ## Browser overrides *The following settings provide URL or form-based overrides of the default browser behavior.* diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index f3a092a9d..293ee7019 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -10,13 +10,100 @@ REST framework includes a few helper classes that extend Django's existing test # APIRequestFactory -Extends Django's existing `RequestFactory`. +Extends [Django's existing `RequestFactory` class][requestfactory]. -**TODO**: Document making requests. Note difference on form PUT requests. Document configuration. +## Creating test requests + +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. + +### Using the format arguments + +Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: + + factory = APIRequestFactory() + request = factory.post('/notes/', {'title': 'new idea'}, format='json') + +By default the available formats are `'multipart'` and `'json'`. For compatibility with Django's existing `RequestFactory` the default format is `'multipart'`. + +To support a wider set of request formats, or change the default format, [see the configuration section][configuration]. + +If you need to explictly encode the request body, you can do so by explicitly setting the `content_type` flag. For example: + + request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') + +### PUT and PATCH with form data + +One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`. + +For example, using `APIRequestFactory`, you can make a form PUT request like so: + + factory = APIRequestFactory() + request = factory.put('/notes/547/', {'title': 'remember to email dave'}) + +Using Django's `Factory`, you'd need to explicitly encode the data yourself: + + factory = RequestFactory() + data = {'title': 'remember to email dave'} + content = encode_multipart('BoUnDaRyStRiNg', data) + content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' + request = factory.put('/notes/547/', content, content_type=content_type) + +## Forcing authentication + +When testing views directly using a request factory, it's often convenient to be able to directly authenticate the request, rather than having to construct the correct authentication credentials. + +To forcibly authenticate a request, use the `force_authenticate()` method. + + factory = APIRequestFactory() + user = User.objects.get(username='olivia') + view = AccountDetail.as_view() + + # Make an authenticated request to the view... + request = factory.get('/accounts/django-superstars/') + force_authenticate(request, user=user) + response = view(request) + +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. + +--- + +**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. + +This means that setting attributes directly on the request object may not always have the effect you expect. For example, setting `.token` directly will have no effect, and setting `.user` directly will only work if session authentication is being used. + + # Request will only authenticate if `SessionAuthentication` is in use. + request = factory.get('/accounts/django-superstars/') + request.user = user + response = view(request) + +--- + +## Forcing CSRF validation + +By default, requests created with `APIRequestFactory` will not have CSRF validation applied when passed to a REST framework view. If you need to explicitly turn CSRF validation on, you can do so by setting the `enforce_csrf_checks` flag when instantiating the factory. + + factory = APIRequestFactory(enforce_csrf_checks=True) + +--- + +**Note**: It's worth noting that Django's standard `RequestFactory` doesn't need to include this option, because when using regular Django the CSRF validation takes place in middleware, which is not run when testing views directly. When using REST framework, CSRF validation takes place inside the view, so the request factory needs to disable view-level CSRF checks. + +--- # APIClient -Extends Django's existing `Client`. +Extends [Django's existing `Client` class][client]. + +## Making requests + +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: + + client = APIClient() + client.post('/notes/', {'title': 'new idea'}, format='json') + +To support a wider set of request formats, or change the default format, [see the configuration section][configuration]. + +## Authenticating ### .login(**kwargs) @@ -59,17 +146,23 @@ This can be a useful shortcut if you're testing the API but don't want to have t >>> client = APIClient() >>> client.force_authenticate(user=user) -To unauthenticate subsequant requests, call `force_authenticate` setting the user and/or token to `None`. +To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`. >>> client.force_authenticate(user=None) -### Making requests +## CSRF validation -**TODO**: Document requests similarly to `APIRequestFactory` +By default CSRF validation is not applied when using `APIClient`. If you need to explicitly enable CSRF validation, you can do so by setting the `enforce_csrf_checks` flag when instantiating the client. + + client = APIClient(enforce_csrf_checks=True) + +As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling `login()`. + +--- # Testing responses -### Using request.data +## Checking the response data When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. @@ -83,7 +176,7 @@ Instead of inspecting the result of parsing `request.content`: response = self.client.get('/users/4/') self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) -### Rendering responses +## Rendering responses If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response. @@ -92,6 +185,36 @@ If you're testing views directly using `APIRequestFactory`, the responses that a response = view(request, pk='4') response.render() # Cannot access `response.content` without this. self.assertEqual(response.content, '{"username": "lauren", "id": 4}') - -[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper \ No newline at end of file +--- + +# Configuration + +## Setting the default format + +The default format used to make test requests may be set using the `TEST_REQUEST_DEFAULT_FORMAT` setting key. For example, to always use JSON for test requests by default instead of standard multipart form requests, set the following in your `settings.py` file: + + REST_FRAMEWORK = { + ... + 'TEST_REQUEST_DEFAULT_FORMAT': 'json' + } + +## Setting the available formats + +If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting. + +For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file. + + REST_FRAMEWORK = { + ... + 'TEST_REQUEST_RENDERER_CLASSES': ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.YAMLRenderer' + ) + } + +[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper +[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client +[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory +[configuration]: #configuration diff --git a/rest_framework/response.py b/rest_framework/response.py index c4b2aaa66..5877c8a3e 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -50,7 +50,7 @@ class Response(SimpleTemplateResponse): charset = renderer.charset content_type = self.content_type - if content_type is None and charset is not None and ';' not in media_type: + if content_type is None and charset is not None: content_type = "{0}; charset={1}".format(media_type, charset) elif content_type is None: content_type = media_type diff --git a/rest_framework/settings.py b/rest_framework/settings.py index beb511aca..8fd177d58 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -73,6 +73,13 @@ DEFAULTS = { 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # Testing + 'TEST_REQUEST_RENDERER_CLASSES': ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer' + ), + 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', + # Browser enhancements 'FORM_METHOD_OVERRIDE': '_method', 'FORM_CONTENT_OVERRIDE': '_content', @@ -115,6 +122,7 @@ IMPORT_STRINGS = ( 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_FILTER_BACKENDS', 'FILTER_BACKEND', + 'TEST_REQUEST_RENDERER_CLASSES', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', ) diff --git a/rest_framework/test.py b/rest_framework/test.py index 2f658a56c..29d017ee4 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,22 +1,31 @@ # -- coding: utf-8 -- -# Note that we use `DjangoRequestFactory` and `DjangoClient` names in order +# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order # to make it harder for the user to import the wrong thing without realizing. from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient from django.test.client import ClientHandler +from rest_framework.settings import api_settings from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import force_bytes_or_smart_bytes, six -from rest_framework.renderers import JSONRenderer, MultiPartRenderer + + +def force_authenticate(request, user=None, token=None): + request._force_auth_user = user + request._force_auth_token = token class APIRequestFactory(DjangoRequestFactory): - renderer_classes = { - 'json': JSONRenderer, - 'multipart': MultiPartRenderer - } - default_format = 'multipart' + renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES + default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT + + def __init__(self, enforce_csrf_checks=False, **defaults): + self.enforce_csrf_checks = enforce_csrf_checks + self.renderer_classes = {} + for cls in self.renderer_classes_list: + self.renderer_classes[cls.format] = cls + super(APIRequestFactory, self).__init__(**defaults) def _encode_data(self, data, format=None, content_type=None): """ @@ -35,18 +44,24 @@ class APIRequestFactory(DjangoRequestFactory): ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) else: - # Use format and render the data into a bytestring format = format or self.default_format + + assert format in self.renderer_classes, ("Invalid format '{0}'. " + "Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES " + "to enable extra request formats.".format( + format, + ', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()]) + ) + ) + + # Use format and render the data into a bytestring renderer = self.renderer_classes[format]() ret = renderer.render(data) # Determine the content-type header from the renderer - if ';' in renderer.media_type: - content_type = renderer.media_type - else: - content_type = "{0}; charset={1}".format( - renderer.media_type, renderer.charset - ) + content_type = "{0}; charset={1}".format( + renderer.media_type, renderer.charset + ) # Coerce text to bytes if required. if isinstance(ret, six.text_type): @@ -74,6 +89,11 @@ class APIRequestFactory(DjangoRequestFactory): data, content_type = self._encode_data(data, format, content_type) return self.generic('OPTIONS', path, data, content_type, **extra) + def request(self, **kwargs): + request = super(APIRequestFactory, self).request(**kwargs) + request._dont_enforce_csrf_checks = not self.enforce_csrf_checks + return request + class ForceAuthClientHandler(ClientHandler): """ @@ -82,25 +102,21 @@ class ForceAuthClientHandler(ClientHandler): """ def __init__(self, *args, **kwargs): - self._force_auth_user = None - self._force_auth_token = None + self._force_user = None + self._force_token = None super(ForceAuthClientHandler, self).__init__(*args, **kwargs) def get_response(self, request): # This is the simplest place we can hook into to patch the # request object. - request._force_auth_user = self._force_auth_user - request._force_auth_token = self._force_auth_token + force_authenticate(request, self._force_user, self._force_token) return super(ForceAuthClientHandler, self).get_response(request) class APIClient(APIRequestFactory, DjangoClient): def __init__(self, enforce_csrf_checks=False, **defaults): - # Note that our super call skips Client.__init__ - # since we don't need to instantiate a regular ClientHandler - super(DjangoClient, self).__init__(**defaults) + super(APIClient, self).__init__(**defaults) self.handler = ForceAuthClientHandler(enforce_csrf_checks) - self.exc_info = None self._credentials = {} def credentials(self, **kwargs): @@ -114,10 +130,10 @@ class APIClient(APIRequestFactory, DjangoClient): Forcibly authenticates outgoing requests with the given user and/or token. """ - self.handler._force_auth_user = user - self.handler._force_auth_token = token + self.handler._force_user = user + self.handler._force_token = token - def request(self, **request): + def request(self, **kwargs): # Ensure that any credentials set get added to every request. - request.update(self._credentials) - return super(APIClient, self).request(**request) + kwargs.update(self._credentials) + return super(APIClient, self).request(**kwargs) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 3706f38c2..49d45fc29 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -6,11 +6,11 @@ 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 +from rest_framework.test import APIClient, APIRequestFactory, force_authenticate @api_view(['GET', 'POST']) -def mirror(request): +def view(request): return Response({ 'auth': request.META.get('HTTP_AUTHORIZATION', b''), 'user': request.user.username @@ -18,11 +18,11 @@ def mirror(request): urlpatterns = patterns('', - url(r'^view/$', mirror), + url(r'^view/$', view), ) -class CheckTestClient(TestCase): +class TestAPITestClient(TestCase): urls = 'rest_framework.tests.test_testing' def setUp(self): @@ -66,3 +66,50 @@ class CheckTestClient(TestCase): expected = {'detail': 'CSRF Failed: CSRF cookie not set.'} self.assertEqual(response.status_code, 403) self.assertEqual(response.data, expected) + + +class TestAPIRequestFactory(TestCase): + def test_csrf_exempt_by_default(self): + """ + By default, the test client is CSRF exempt. + """ + user = User.objects.create_user('example', 'example@example.com', 'password') + factory = APIRequestFactory() + request = factory.post('/view/') + request.user = user + response = view(request) + self.assertEqual(response.status_code, 200) + + def test_explicitly_enforce_csrf_checks(self): + """ + The test client can enforce CSRF checks. + """ + user = User.objects.create_user('example', 'example@example.com', 'password') + factory = APIRequestFactory(enforce_csrf_checks=True) + request = factory.post('/view/') + request.user = user + response = view(request) + expected = {'detail': 'CSRF Failed: CSRF cookie not set.'} + self.assertEqual(response.status_code, 403) + self.assertEqual(response.data, expected) + + def test_invalid_format(self): + """ + Attempting to use a format that is not configured will raise an + assertion error. + """ + factory = APIRequestFactory() + self.assertRaises(AssertionError, factory.post, + path='/view/', data={'example': 1}, format='xml' + ) + + def test_force_authenticate(self): + """ + Setting `force_authenticate()` forcibly authenticates the request. + """ + user = User.objects.create_user('example', 'example@example.com') + factory = APIRequestFactory() + request = factory.get('/view') + force_authenticate(request, user=user) + response = view(request) + self.assertEqual(response.data['user'], 'example') From 5427d90fa48398684948067530cd8083f785c248 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 17:22:11 +0100 Subject: [PATCH 046/206] Remove console style from code blocks --- docs/api-guide/testing.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 293ee7019..a48aff00e 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -16,7 +16,7 @@ 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. -### Using the format arguments +#### Using the format arguments Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: @@ -31,7 +31,7 @@ If you need to explictly encode the request body, you can do so by explicitly se request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') -### PUT and PATCH with form data +#### PUT and PATCH with form data One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`. @@ -105,50 +105,50 @@ To support a wider set of request formats, or change the default format, [see th ## Authenticating -### .login(**kwargs) +#### .login(**kwargs) The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`. # Make all requests in the context of a logged in session. - >>> client = APIClient() - >>> client.login(username='lauren', password='secret') + client = APIClient() + client.login(username='lauren', password='secret') To logout, call the `logout` method as usual. # Log out - >>> client.logout() + client.logout() The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API. -### .credentials(**kwargs) +#### .credentials(**kwargs) The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. # Include an appropriate `Authorization:` header on all requests. - >>> token = Token.objects.get(username='lauren') - >>> client = APIClient() - >>> client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) + token = Token.objects.get(username='lauren') + client = APIClient() + client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments. # Stop including any credentials - >>> client.credentials() + client.credentials() The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes. -### .force_authenticate(user=None, token=None) +#### .force_authenticate(user=None, token=None) Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated. This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests. - >>> user = User.objects.get(username='lauren') - >>> client = APIClient() - >>> client.force_authenticate(user=user) + user = User.objects.get(username='lauren') + client = APIClient() + client.force_authenticate(user=user) To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`. - >>> client.force_authenticate(user=None) + client.force_authenticate(user=None) ## CSRF validation From 53dc98eefb5644e60495ca86c7424e669d0a86f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 17:22:42 +0100 Subject: [PATCH 047/206] Added Django OAuth2 Consumer package --- docs/api-guide/authentication.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 8cf995b38..22c3297ce 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -355,6 +355,10 @@ HTTP digest authentication is a widely implemented scheme that was intended to r The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excelllent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support. +## Django OAuth2 Consumer + +The [Django Oauth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth2 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API. + [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -376,3 +380,6 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit [evonove]: https://github.com/evonove/ [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 From 8274ff7d9c692d27f926af7610a5a547ced09a2e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 17:27:23 +0100 Subject: [PATCH 048/206] Capitalization on OAuth --- 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 22c3297ce..768f156b3 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -357,7 +357,7 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp ## Django OAuth2 Consumer -The [Django Oauth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth2 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API. +The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth2 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API. [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 From 8d410c4671fd4596089883e360f5d3e8f9e0f62b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 17:32:06 +0100 Subject: [PATCH 049/206] Tweak text --- 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 768f156b3..390fba8cb 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -357,7 +357,7 @@ The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 supp ## Django OAuth2 Consumer -The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth2 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API. +The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API. [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 From e7529b4072274797daf5b886dbac4c0e65a65674 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Jul 2013 16:22:22 +0100 Subject: [PATCH 050/206] Fix broken link by hacking around md->html translating --- 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 390fba8cb..5d6e0d91d 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -382,4 +382,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/markdown/integrations.md# From e460180a4dd86ff74cc786aabfeeba1c31d17413 Mon Sep 17 00:00:00 2001 From: Rudolf Olah Date: Tue, 2 Jul 2013 13:20:25 -0400 Subject: [PATCH 051/206] #955 updated router docs with more information on the `Route` named tuple and its parameters. --- 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 feff0fbfe..1fb15fae3 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -100,6 +100,18 @@ Implementing a custom router isn't something you'd need to do very often, but it 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. +The arguments to the `Route` named tuple are: + +* `url`: The URL to be routed. There are format arguments available, defined in `SimpleRouter.get_urls`: + * `prefix` - The URL prefix to use for this set of routes. + * `lookup` - The lookup field used to match against a single instance. + * `trailing_slash` - the value of `.trailing_slash`. +* `mapping`: Mapping of HTTP method names to the object's methods +* `name`: The name of the URL as used in `reverse` calls. There are format arguments available, defined in `SimpleRouter.get_urls`: + * `basename` - The base to use for the URL names that are created. +* `initkwargs`: Any additional arguments to the view. + * `suffix` - reserved for identifying the viewset type, used when generating the breadcrumb links, e.g. `AccountViewSet` becomes `Account List` when `suffix='List'`. + ## Example The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention. From e969c96aa020047becd7a759a80e2fdc46b21170 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Jul 2013 22:08:40 +0100 Subject: [PATCH 052/206] Added @omouse for work on #955 - thanks! :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 94760c74b..e6fb9134e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -144,6 +144,7 @@ The following people have helped make REST framework great. * David Sanders - [davesque] * Philip Douglas - [freakydug] * Igor Kalat - [trwired] +* Rudolf Olah - [omouse] Many thanks to everyone who's contributed to the project. @@ -324,3 +325,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [davesque]: https://github.com/davesque [freakydug]: https://github.com/freakydug [trwired]: https://github.com/trwired +[omouse]: https://github.com/omouse From 03e0ce35fe497d52d1a332e98c44e42acbc8af75 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Jul 2013 22:15:46 +0100 Subject: [PATCH 053/206] Added 'Documenting your API' section. Closes #616 --- docs/img/apiary.png | Bin 0 -> 55554 bytes docs/img/django-rest-swagger.png | Bin 0 -> 76945 bytes docs/img/rest-framework-docs.png | Bin 0 -> 76612 bytes docs/img/self-describing.png | Bin 0 -> 53953 bytes docs/index.md | 2 + docs/template.html | 1 + docs/topics/documenting-your-api.md | 108 +++++++++++++++++++++++++ docs/topics/rest-hypermedia-hateoas.md | 3 +- mkdocs.py | 1 + 9 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 docs/img/apiary.png create mode 100644 docs/img/django-rest-swagger.png create mode 100644 docs/img/rest-framework-docs.png create mode 100644 docs/img/self-describing.png create mode 100644 docs/topics/documenting-your-api.md diff --git a/docs/img/apiary.png b/docs/img/apiary.png new file mode 100644 index 0000000000000000000000000000000000000000..923d384ebb66d833b13b9971cda69eb718651c1d GIT binary patch literal 55554 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 literal 0 HcmV?d00001 diff --git a/docs/img/django-rest-swagger.png b/docs/img/django-rest-swagger.png new file mode 100644 index 0000000000000000000000000000000000000000..96a6b23800d2f7dec53631e0b017e2f383bda659 GIT binary patch literal 76945 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%0D(6%`>FHckpDt$oGc4=IC`V= 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 literal 0 HcmV?d00001 diff --git a/docs/img/rest-framework-docs.png b/docs/img/rest-framework-docs.png new file mode 100644 index 0000000000000000000000000000000000000000..736a0095500cce86611e6fa80ddabbdedb878f4e GIT binary patch literal 76612 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

yMZfoSE<{aD_|fOb~6MCO4H4^wZO- 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�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 z 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 Q8{nT1pEz&HHw~}<2TrG-+W-In literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index de4b01c61..99cd6b882 100644 --- a/docs/index.md +++ b/docs/index.md @@ -170,6 +170,7 @@ The API guide is your complete reference manual to all the functionality provide General guides to using REST framework. +* [Documenting your API][documenting-your-api] * [AJAX, CSRF & CORS][ajax-csrf-cors] * [Browser enhancements][browser-enhancements] * [The Browsable API][browsableapi] @@ -289,6 +290,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [status]: api-guide/status-codes.md [settings]: api-guide/settings.md +[documenting-your-api]: topics/documenting-your-api.md [ajax-csrf-cors]: topics/ajax-csrf-cors.md [browser-enhancements]: topics/browser-enhancements.md [browsableapi]: topics/browsable-api.md diff --git a/docs/template.html b/docs/template.html index 217710250..27bc10622 100644 --- a/docs/template.html +++ b/docs/template.html @@ -95,6 +95,7 @@

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 127/206] 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 128/206] 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 129/206] 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 130/206] 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 131/206] 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 132/206] 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 133/206] 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 134/206] 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 135/206] 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 28ff6fb1ec02b7a04c4a0db54885f3735b6dd43f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Aug 2013 21:44:47 +0100 Subject: [PATCH 136/206] 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 137/206] 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 138/206] 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 139/206] 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 140/206] 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 141/206] 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 2bcad32dcb57ae9419f6a901e081f0dcdc1a6f87 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Aug 2013 21:22:12 +0100 Subject: [PATCH 142/206] 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 ec5955101b4b15b828ac5b6fc54e8d10f2a7c64a Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 22 Aug 2013 12:40:12 -0300 Subject: [PATCH 143/206] 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 144/206] 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 145/206] 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 146/206] 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 147/206] 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 148/206] 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 149/206] 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 150/206] 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 151/206] 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 152/206] 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 153/206] 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 154/206] 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 155/206] 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 156/206] 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 157/206] 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 158/206] 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 159/206] 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 160/206] 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 161/206] 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 162/206] 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 163/206] 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 164/206] 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 165/206] 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 166/206] 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 167/206] 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 168/206] 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 169/206] 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 170/206] 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 171/206] 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 172/206] 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 173/206] 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 174/206] 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 175/206] 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 176/206] 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 177/206] 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 178/206] 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 179/206] 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 6f8acb5a768d5d79efd7b39c5229bc4262e467a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Aug 2013 09:31:12 +0100 Subject: [PATCH 180/206] 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 181/206] 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 182/206] 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 183/206] 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 184/206] 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 185/206] 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 186/206] 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 187/206] 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 188/206] 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 189/206] 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 190/206] 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 191/206] 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 192/206] 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 193/206] 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 194/206] 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 195/206] 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 196/206] 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 197/206] 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 198/206] 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 199/206] 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 200/206] 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 201/206] 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 202/206] 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 203/206] 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 204/206] 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 205/206] 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 206/206] 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) ---