+
{{ request.method }} {{ request.get_full_path }}
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index a3e73b6b..be9a0072 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -4,8 +4,10 @@
{% block style %}
- {% block bootstrap_theme %}{% endblock %}
-
+ {% block bootstrap_theme %}
+
+
+ {% endblock %}
{% endblock %}
--
cgit v1.2.3
From 770ed3de2ef8339ec0b74f7eb522283718e01a3b Mon Sep 17 00:00:00 2001
From: Ryan Kaskel
Date: Sat, 18 May 2013 13:11:40 +0100
Subject: ToMany fields default to read-only if targeting ManyToManyField.
---
docs/api-guide/relations.md | 9 ++++++
rest_framework/serializers.py | 17 ++++++++++
rest_framework/tests/relations_pk.py | 63 ++++++++++++++++++++++++++++++++++++
3 files changed, 89 insertions(+)
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 155c89de..99fe1083 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -381,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
For more information see [the Django documentation on generic relations][generic-relations].
+## ManyToManyFields with a Through Model
+
+By default, relational fields that target a ``ManyToManyField`` with a
+``through`` model specified are set to read-only.
+
+If you exlicitly specify a relational field pointing to a
+``ManyToManyField`` with a through model, be sure to set ``read_only``
+to ``True``.
+
## Advanced Hyperlinked fields
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 7707de7a..75f1e1d0 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -587,11 +587,16 @@ class ModelSerializer(Serializer):
forward_rels += [field for field in opts.many_to_many if field.serialize]
for model_field in forward_rels:
+ has_user_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_user_through_model = True
+
if model_field.rel and nested:
if len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn(
@@ -620,6 +625,9 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
+ if has_user_through_model:
+ field.read_only = True
+
ret[model_field.name] = field
# Deal with reverse relationships
@@ -637,6 +645,12 @@ class ModelSerializer(Serializer):
continue
related_model = relation.model
to_many = relation.field.rel.multiple
+ has_user_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_user_through_model = True
if nested:
field = self.get_nested_field(None, related_model, to_many)
@@ -644,6 +658,9 @@ class ModelSerializer(Serializer):
field = self.get_related_field(None, related_model, to_many)
if field:
+ if has_user_through_model:
+ field.read_only = True
+
ret[accessor_name] = field
# Add the `read_only` flag to any fields that have bee specified
diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py
index 5ce8b567..02ffd264 100644
--- a/rest_framework/tests/relations_pk.py
+++ b/rest_framework/tests/relations_pk.py
@@ -1,4 +1,5 @@
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 ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
@@ -124,6 +125,7 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
+ self.assertFalse(serializer.fields['targets'].read_only)
expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
@@ -135,6 +137,7 @@ class PKManyToManyTests(TestCase):
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
+ self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(serializer.data, data)
@@ -421,3 +424,63 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
self.assertEqual(serializer.data, expected)
+
+
+# The below models and tests ensure that serializer fields corresponding
+# to a ManyToManyField field with a user-specified ``through`` model are
+# set to read only
+
+
+class ManyToManyThroughTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManyThrough(models.Model):
+ source = models.ForeignKey('ManyToManyThroughSource')
+ target = models.ForeignKey(ManyToManyThroughTarget)
+
+
+class ManyToManyThroughSource(models.Model):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyThroughTarget,
+ related_name='sources',
+ through='ManyToManyThrough')
+
+
+class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyThroughTarget
+ fields = ('id', 'name', 'sources')
+
+
+class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyThroughSource
+ fields = ('id', 'name', 'targets')
+
+
+class PKManyToManyThroughTests(TestCase):
+ def setUp(self):
+ self.source = ManyToManyThroughSource.objects.create(
+ name='through-source-1')
+ self.target = ManyToManyThroughTarget.objects.create(
+ name='through-target-1')
+
+ def test_many_to_many_create(self):
+ data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
+ serializer = ManyToManyThroughSourceSerializer(data=data)
+ self.assertTrue(serializer.fields['targets'].read_only)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEqual(obj.name, 'source-2')
+ self.assertEqual(obj.targets.count(), 0)
+
+ def test_many_to_many_reverse_create(self):
+ data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
+ serializer = ManyToManyThroughTargetSerializer(data=data)
+ self.assertTrue(serializer.fields['sources'].read_only)
+ self.assertTrue(serializer.is_valid())
+ serializer.save()
+ obj = serializer.save()
+ self.assertEqual(obj.name, 'target-2')
+ self.assertEqual(obj.sources.count(), 0)
--
cgit v1.2.3
From 72bc5a35569e75468442287e74656e4b18adc163 Mon Sep 17 00:00:00 2001
From: Mark McArdle
Date: Sat, 18 May 2013 13:19:13 +0100
Subject: documentation for
https://github.com/tomchristie/django-rest-framework/issues/775
---
docs/topics/browsable-api.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md
index 8ee01824..65f76abc 100644
--- a/docs/topics/browsable-api.md
+++ b/docs/topics/browsable-api.md
@@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
+Full Example
+
+ {% extends "rest_framework/base.html" %}
+
+ {% block bootstrap_theme %}
+
+ {% endblock %}
+
+ {% block bootstrap_navbar_variant %}{% endblock %}
+
+
For more specific CSS tweaks, use the `style` block instead.
--
cgit v1.2.3
From 392c8556aed41df2120623fe1c891610bb111100 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 15:01:58 +0200
Subject: Display an empty label into related fields if it isn't required
---
rest_framework/relations.py | 8 +++++-
rest_framework/tests/serializer.py | 57 ++++++++++++++++++++++++++++++++++++++
2 files changed, 64 insertions(+), 1 deletion(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 884b954c..3aaf6648 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -8,6 +8,7 @@ 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 _
@@ -47,7 +48,7 @@ class RelatedField(WritableField):
DeprecationWarning, stacklevel=2)
kwargs['required'] = not kwargs.pop('null')
- self.queryset = kwargs.pop('queryset', None)
+ queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
@@ -56,6 +57,11 @@ class RelatedField(WritableField):
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:
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index d0a8570c..69b33209 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1117,6 +1117,63 @@ class SerializerChoiceFields(TestCase):
)
+# Regression tests for #675
+class Ticket(models.Model):
+ assigned = models.ForeignKey(
+ Person, related_name='assigned_tickets')
+ reviewer = models.ForeignKey(
+ Person, blank=True, null=True, related_name='reviewed_tickets')
+
+
+class SerializerRelatedChoicesTest(TestCase):
+
+ def setUp(self):
+ super(SerializerRelatedChoicesTest, self).setUp()
+
+ class RelatedChoicesSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Ticket
+ fields = ('assigned', 'reviewer')
+
+ self.related_fields_serializer = RelatedChoicesSerializer
+
+ def test_empty_queryset_required(self):
+ serializer = self.related_fields_serializer()
+ self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
+ self.assertEqual(
+ [x for x in serializer.fields['assigned'].widget.choices],
+ []
+ )
+
+ def test_empty_queryset_not_required(self):
+ serializer = self.related_fields_serializer()
+ self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
+ self.assertEqual(
+ [x for x in serializer.fields['reviewer'].widget.choices],
+ [(u'', u'---------')]
+ )
+
+ def test_with_some_persons_required(self):
+ Person.objects.create(name="Lionel Messi")
+ Person.objects.create(name="Xavi Hernandez")
+ serializer = self.related_fields_serializer()
+ self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
+ self.assertEqual(
+ [x for x in serializer.fields['assigned'].widget.choices],
+ [(1, u'Person object - 1'), (2, u'Person object - 2')]
+ )
+
+ def test_with_some_persons_not_required(self):
+ Person.objects.create(name="Lionel Messi")
+ Person.objects.create(name="Xavi Hernandez")
+ serializer = self.related_fields_serializer()
+ self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
+ self.assertEqual(
+ [x for x in serializer.fields['reviewer'].widget.choices],
+ [(u'', u'---------'), (1, u'Person object - 1'), (2, u'Person object - 2')]
+ )
+
+
class DepthTest(TestCase):
def test_implicit_nesting(self):
--
cgit v1.2.3
From 275bfa2320eeffcfd307daaf7c85faaa0f55a8ee Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 14:03:08 +0100
Subject: Added @grimborg and @ryankask. Go Team. :)
---
docs/topics/credits.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 8151b4d3..62f113c2 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -124,6 +124,8 @@ The following people have helped make REST framework great.
* Marlon Bailey - [avinash240]
* James Summerfield - [jsummerfield]
* Andy Freeland - [rouge8]
+* Òscar Vilaplana - [grimborg]
+* Ryan Kaskel - [ryankask]
Many thanks to everyone who's contributed to the project.
@@ -284,3 +286,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[avinash240]: https://github.com/avinash240
[jsummerfield]: https://github.com/jsummerfield
[rouge8]: https://github.com/rouge8
+[grimborg]: https://github.com/grimborg
+[ryankask]: https://github.com/ryankask
--
cgit v1.2.3
From a71acc76d8ede28ad9c5f9fd0fc129f6321c9231 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 15:12:54 +0200
Subject: Fix for #710
---
rest_framework/serializers.py | 16 ++++++++
rest_framework/tests/serializer.py | 84 +++++++++++++++++++++++++++++++++++++-
2 files changed, 99 insertions(+), 1 deletion(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 500bb306..1b451f2d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -723,6 +723,22 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
+ 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: model_field.__getattribute__(attribute)})
+
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index d0a8570c..73d6a15b 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -6,7 +6,7 @@ from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
+ ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
import datetime
import pickle
@@ -1242,3 +1242,85 @@ class DeserializeListTestCase(TestCase):
self.assertFalse(serializer.is_valid())
expected = [{}, {'email': ['This field is required.']}, {}]
self.assertEqual(serializer.errors, expected)
+
+
+class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
+
+ def setUp(self):
+ class AMOAFModel(RESTFrameworkModel):
+ char_field = models.CharField(max_length=1024, blank=True)
+ comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
+ decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
+ email_field = models.EmailField(max_length=1024, blank=True)
+ file_field = models.FileField(max_length=1024, blank=True)
+ image_field = models.ImageField(max_length=1024, blank=True)
+ slug_field = models.SlugField(max_length=1024, blank=True)
+ url_field = models.URLField(max_length=1024, blank=True)
+
+ class AMOAFSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AMOAFModel
+
+ self.serializer_class = AMOAFSerializer
+ self.objects = AMOAFModel.objects
+ self.fields_attributes = {
+ 'char_field': [
+ ('max_length', 1024),
+ ],
+ 'comma_separated_integer_field': [
+ ('max_length', 1024),
+ ],
+ 'decimal_field': [
+ ('max_digits', 64),
+ ('decimal_places', 32),
+ ],
+ 'email_field': [
+ ('max_length', 1024),
+ ],
+ 'file_field': [
+ ('max_length', 1024),
+ ],
+ 'image_field': [
+ ('max_length', 1024),
+ ],
+ 'slug_field': [
+ ('max_length', 1024),
+ ],
+ 'url_field': [
+ ('max_length', 1024),
+ ],
+ }
+
+ def field_test(self, field):
+ serializer = self.serializer_class(data={})
+ self.assertEqual(serializer.is_valid(), True)
+
+ for attribute in self.fields_attributes[field]:
+ self.assertEqual(
+ getattr(serializer.fields[field], attribute[0]),
+ attribute[1]
+ )
+
+ def test_char_field(self):
+ self.field_test('char_field')
+
+ def test_comma_separated_integer_field(self):
+ self.field_test('comma_separated_integer_field')
+
+ def test_decimal_field(self):
+ self.field_test('decimal_field')
+
+ def test_email_field(self):
+ self.field_test('email_field')
+
+ def test_file_field(self):
+ self.field_test('file_field')
+
+ def test_image_field(self):
+ self.field_test('image_field')
+
+ def test_slug_field(self):
+ self.field_test('slug_field')
+
+ def test_url_field(self):
+ self.field_test('url_field')
--
cgit v1.2.3
From 208bd991dacb6c2edc9fc820717354c579c2e6d6 Mon Sep 17 00:00:00 2001
From: Craig de Stigter
Date: Sat, 18 May 2013 15:23:43 +0200
Subject: when source='*' on a nested serializer, expand fields into outer
serializer when writing. fixes #765
---
rest_framework/serializers.py | 34 +++++++++++++++++++---------------
rest_framework/tests/serializer.py | 22 ++++++++++++++++++++++
2 files changed, 41 insertions(+), 15 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 7707de7a..ef099e57 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -378,23 +378,27 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists
obj = getattr(self.parent.object, field_name) if self.parent.object else None
- if value in (None, ''):
- into[(self.source or field_name)] = None
+ if self.source == '*':
+ if value:
+ into.update(value)
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
+ if value in (None, ''):
+ into[(self.source or field_name)] = None
else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
+ 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):
"""
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 34acbaab..116459e0 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -78,6 +78,17 @@ class PersonSerializer(serializers.ModelSerializer):
read_only_fields = ('age',)
+class NestedSerializer(serializers.Serializer):
+ info = serializers.Field()
+
+
+class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
+ nested = NestedSerializer(source='*')
+
+ class Meta:
+ model = Person
+
+
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
"""
Testing for #652.
@@ -369,6 +380,17 @@ class ValidationTests(TestCase):
except:
self.fail('Wrong exception type thrown.')
+ def test_writable_star_source_on_nested_serializer(self):
+ """
+ Assert that a nested serializer instantiated with source='*' correctly
+ expands the data into the outer serializer.
+ """
+ serializer = ModelSerializerWithNestedSerializer(data={
+ 'name': 'marko',
+ 'nested': {'info': 'hi'}},
+ )
+ self.assertEqual(serializer.is_valid(), True)
+
class CustomValidationTests(TestCase):
class CommentSerializerWithFieldValidator(CommentSerializer):
--
cgit v1.2.3
From ef383d969c440a20fdf25748de590e07cb1abfda Mon Sep 17 00:00:00 2001
From: Ryan Kaskel
Date: Sat, 18 May 2013 14:31:29 +0100
Subject: Clean up test case.
---
rest_framework/tests/serializer.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index d978dc87..e999b624 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH
-from django.utils.datastructures import MultiValueDict
from django.test import TestCase
+from django.utils.datastructures import MultiValueDict
+from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
@@ -1246,6 +1247,7 @@ class DeserializeListTestCase(TestCase):
# test for issue 747
+
class LazyStringModel(object):
def __init__(self, lazystring):
self.lazystring = lazystring
@@ -1258,16 +1260,14 @@ class LazyStringSerializer(serializers.Serializer):
if instance is not None:
instance.lazystring = attrs.get('lazystring', instance.lazystring)
return instance
- return Comment(**attrs)
+ return LazyStringModel(**attrs)
class LazyStringsTestCase(TestCase):
-
def setUp(self):
- from django.utils.translation import ugettext_lazy as _
-
- self.model = LazyStringModel(lazystring=_("lazystring"))
+ self.model = LazyStringModel(lazystring=_('lazystring'))
def test_lazy_strings_are_translated(self):
serializer = LazyStringSerializer(self.model)
- self.assertEqual(type(serializer.data['lazystring']), type("lazystring"))
+ self.assertEqual(type(serializer.data['lazystring']),
+ type('lazystring'))
--
cgit v1.2.3
From 2e8ff4473bbfe4c8d7ead59e299a663d1a493478 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 14:47:39 +0100
Subject: Tweak docs
---
docs/api-guide/renderers.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 2a1a6979..b9a9fd7a 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -272,10 +272,10 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
* Load and render a template named `api_exception.html`.
* Render the HTTP status code and text, for example "404 Not Found".
-**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
-
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
+**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
+
---
# Third party packages
--
cgit v1.2.3
From e6409bbc836cbe61be7f525900f5bc0616f5c225 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 15:48:36 +0200
Subject: Temp commit for branch change ;-)
---
rest_framework/tests/fields.py | 61 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index 6b1cdfc7..cb5b05dc 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -10,6 +10,7 @@ from django.test import TestCase
from django.core import validators
from rest_framework import serializers
from rest_framework.serializers import Serializer
+from rest_framework.tests.models import RESTFrameworkModel
class TimestampedModel(models.Model):
@@ -685,3 +686,63 @@ class ChoiceFieldTests(TestCase):
"""
f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
+
+
+class SlugFieldTests(TestCase):
+ """
+ Tests for SlugField attribute values
+ """
+
+ def test_default_value(self):
+ class SlugFieldModel(RESTFrameworkModel):
+ slug_field = models.SlugField(blank=True)
+
+ class SlugFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SlugFieldModel
+
+ serializer = SlugFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
+
+ def test_given_value(self):
+ class SlugFieldModel(RESTFrameworkModel):
+ slug_field = models.SlugField(max_length=84, blank=True)
+
+ class SlugFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SlugFieldModel
+
+ serializer = SlugFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
+
+
+class URLFieldTests(TestCase):
+ """
+ Tests for URLField attribute values
+ """
+
+ def test_default_value(self):
+ class URLFieldModel(RESTFrameworkModel):
+ url_field = models.URLField(blank=True)
+
+ class URLFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = URLFieldModel
+
+ serializer = URLFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200)
+
+ def test_given_value(self):
+ class URLFieldModel(RESTFrameworkModel):
+ url_field = models.URLField(max_length=128, blank=True)
+
+ class URLFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = URLFieldModel
+
+ serializer = URLFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128)
\ No newline at end of file
--
cgit v1.2.3
From eb9fdd04fceac322af92bd196293390270653ccd Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 15:50:12 +0200
Subject: Remove unused var
---
rest_framework/tests/serializer.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 73d6a15b..8b487b54 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1262,7 +1262,6 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
model = AMOAFModel
self.serializer_class = AMOAFSerializer
- self.objects = AMOAFModel.objects
self.fields_attributes = {
'char_field': [
('max_length', 1024),
--
cgit v1.2.3
From 80a2600891aab2211c8c4bf3ce78e3c1d5c6de72 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 14:55:48 +0100
Subject: Styling tweaks
---
rest_framework/static/rest_framework/css/bootstrap-tweaks.css | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 69e1e70d..9b520156 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -37,7 +37,7 @@ a single block in the template.
border-radius: 0px;
}
-.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
+.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
color: white;
}
@@ -178,4 +178,4 @@ body a:hover{
.request-info {
clear:both;
-}
\ No newline at end of file
+}
--
cgit v1.2.3
From b7176065a9aafb3d560f9e8c2e420bd4cc5841dc Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 16:06:30 +0200
Subject: Update getattr
---
rest_framework/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 1b451f2d..338803fe 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -737,7 +737,7 @@ class ModelSerializer(Serializer):
if model_field.__class__ in attribute_dict:
attributes = attribute_dict[model_field.__class__]
for attribute in attributes:
- kwargs.update({attribute: model_field.__getattribute__(attribute)})
+ kwargs.update({attribute: getattr(model_field, attribute)})
try:
return self.field_mapping[model_field.__class__](**kwargs)
--
cgit v1.2.3
From aeba5bed05e37344caf15adff180fc48404d992d Mon Sep 17 00:00:00 2001
From: Andy McKay
Date: Sat, 18 May 2013 07:09:24 -0700
Subject: startswith can take a tuple
---
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 884b954c..c7256587 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -442,7 +442,7 @@ class HyperlinkedRelatedField(RelatedField):
raise Exception('Writable related fields must include a `queryset` argument')
try:
- http_prefix = value.startswith('http:') or value.startswith('https:')
+ http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
--
cgit v1.2.3
From 7dca0e68591569c2838e383f15f690f5891509b1 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 16:12:48 +0200
Subject: Removed unicode literals
---
rest_framework/tests/serializer.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 50b06582..96f18451 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1172,7 +1172,7 @@ class SerializerRelatedChoicesTest(TestCase):
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
self.assertEqual(
[x for x in serializer.fields['reviewer'].widget.choices],
- [(u'', u'---------')]
+ [('', '---------')]
)
def test_with_some_persons_required(self):
@@ -1182,7 +1182,7 @@ class SerializerRelatedChoicesTest(TestCase):
self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
self.assertEqual(
[x for x in serializer.fields['assigned'].widget.choices],
- [(1, u'Person object - 1'), (2, u'Person object - 2')]
+ [(1, 'Person object - 1'), (2, 'Person object - 2')]
)
def test_with_some_persons_not_required(self):
@@ -1192,7 +1192,7 @@ class SerializerRelatedChoicesTest(TestCase):
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
self.assertEqual(
[x for x in serializer.fields['reviewer'].widget.choices],
- [(u'', u'---------'), (1, u'Person object - 1'), (2, u'Person object - 2')]
+ [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
)
--
cgit v1.2.3
From 3ba43b960ecacda1121670440a38c2883815bad2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 16:19:05 +0200
Subject: Added @andymckay for refactoring #860
---
docs/topics/credits.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index bba78ba8..d805c0c1 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -129,6 +129,7 @@ The following people have helped make REST framework great.
* Brian Zambrano - [brianz]
* Òscar Vilaplana - [grimborg]
* Ryan Kaskel - [ryankask]
+* Andy McKay - [andymckay]
Many thanks to everyone who's contributed to the project.
@@ -294,3 +295,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[brianz]: https://github.com/brianz
[grimborg]: https://github.com/grimborg
[ryankask]: https://github.com/ryankask
+[andymckay]: https://github.com/andymckay
--
cgit v1.2.3
From 9f9cb97d6538e320a0740749b200d418d9d52040 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 16:24:54 +0200
Subject: Add TestCases for default field values
---
rest_framework/fields.py | 2 -
rest_framework/tests/fields.py | 99 ++++++++++++++++++++++++++++++++++--------
2 files changed, 81 insertions(+), 20 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 491aa7ed..fc14184c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -383,7 +383,6 @@ class URLField(CharField):
type_name = 'URLField'
def __init__(self, **kwargs):
- kwargs['max_length'] = kwargs.get('max_length', 200)
kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs)
@@ -392,7 +391,6 @@ class SlugField(CharField):
type_name = 'SlugField'
def __init__(self, *args, **kwargs):
- kwargs['max_length'] = kwargs.get('max_length', 50)
super(SlugField, self).__init__(*args, **kwargs)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index cb5b05dc..d82f6b23 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -688,61 +688,124 @@ class ChoiceFieldTests(TestCase):
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
+class EmailFieldTests(TestCase):
+ """
+ Tests for EmailField attribute values
+ """
+
+ class EmailFieldModel(RESTFrameworkModel):
+ email_field = models.EmailField(blank=True)
+
+ class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+ email_field = models.EmailField(max_length=150, blank=True)
+
+ def test_default_model_value(self):
+ class EmailFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = self.EmailFieldModel
+
+ serializer = EmailFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
+
+ def test_given_model_value(self):
+ class EmailFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = self.EmailFieldWithGivenMaxLengthModel
+
+ serializer = EmailFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
+
+ def test_given_serializer_value(self):
+ class EmailFieldSerializer(serializers.ModelSerializer):
+ email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
+
+ class Meta:
+ model = self.EmailFieldModel
+
+ serializer = EmailFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
+
+
class SlugFieldTests(TestCase):
"""
Tests for SlugField attribute values
"""
- def test_default_value(self):
- class SlugFieldModel(RESTFrameworkModel):
- slug_field = models.SlugField(blank=True)
+ class SlugFieldModel(RESTFrameworkModel):
+ slug_field = models.SlugField(blank=True)
+
+ class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+ slug_field = models.SlugField(max_length=84, blank=True)
+ def test_default_model_value(self):
class SlugFieldSerializer(serializers.ModelSerializer):
class Meta:
- model = SlugFieldModel
+ model = self.SlugFieldModel
serializer = SlugFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
- def test_given_value(self):
- class SlugFieldModel(RESTFrameworkModel):
- slug_field = models.SlugField(max_length=84, blank=True)
-
+ def test_given_model_value(self):
class SlugFieldSerializer(serializers.ModelSerializer):
class Meta:
- model = SlugFieldModel
+ model = self.SlugFieldWithGivenMaxLengthModel
serializer = SlugFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
+ def test_given_serializer_value(self):
+ class SlugFieldSerializer(serializers.ModelSerializer):
+ slug_field = serializers.SlugField(source='slug_field', max_length=20, required=False)
+
+ class Meta:
+ model = self.SlugFieldModel
+
+ serializer = SlugFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20)
+
class URLFieldTests(TestCase):
"""
Tests for URLField attribute values
"""
- def test_default_value(self):
- class URLFieldModel(RESTFrameworkModel):
- url_field = models.URLField(blank=True)
+ class URLFieldModel(RESTFrameworkModel):
+ url_field = models.URLField(blank=True)
+
+ class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+ url_field = models.URLField(max_length=128, blank=True)
+ def test_default_model_value(self):
class URLFieldSerializer(serializers.ModelSerializer):
class Meta:
- model = URLFieldModel
+ model = self.URLFieldModel
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200)
- def test_given_value(self):
- class URLFieldModel(RESTFrameworkModel):
- url_field = models.URLField(max_length=128, blank=True)
+ def test_given_model_value(self):
+ class URLFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = self.URLFieldWithGivenMaxLengthModel
+
+ serializer = URLFieldSerializer(data={})
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128)
+ def test_given_serializer_value(self):
class URLFieldSerializer(serializers.ModelSerializer):
+ url_field = serializers.URLField(source='url_field', max_length=20, required=False)
+
class Meta:
- model = URLFieldModel
+ model = self.URLFieldWithGivenMaxLengthModel
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128)
\ No newline at end of file
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
\ No newline at end of file
--
cgit v1.2.3
From 5f8d353f0b4e0938866a1e954bbd762f383d893b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 15:25:49 +0100
Subject: Rename has_user_through_model -> has_through_model
---
rest_framework/serializers.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 3087d4e6..ff5eb873 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -591,7 +591,7 @@ class ModelSerializer(Serializer):
forward_rels += [field for field in opts.many_to_many if field.serialize]
for model_field in forward_rels:
- has_user_through_model = False
+ has_through_model = False
if model_field.rel:
to_many = isinstance(model_field,
@@ -599,7 +599,7 @@ class ModelSerializer(Serializer):
related_model = model_field.rel.to
if to_many and not model_field.rel.through._meta.auto_created:
- has_user_through_model = True
+ has_through_model = True
if model_field.rel and nested:
if len(inspect.getargspec(self.get_nested_field).args) == 2:
@@ -629,7 +629,7 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
- if has_user_through_model:
+ if has_through_model:
field.read_only = True
ret[model_field.name] = field
@@ -649,12 +649,12 @@ class ModelSerializer(Serializer):
continue
related_model = relation.model
to_many = relation.field.rel.multiple
- has_user_through_model = False
+ 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_user_through_model = True
+ has_through_model = True
if nested:
field = self.get_nested_field(None, related_model, to_many)
@@ -662,7 +662,7 @@ class ModelSerializer(Serializer):
field = self.get_related_field(None, related_model, to_many)
if field:
- if has_user_through_model:
+ if has_through_model:
field.read_only = True
ret[accessor_name] = field
--
cgit v1.2.3
From aeea32030b53eb73547696783cc6eb0d3e14f156 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 16:27:11 +0200
Subject: Add newline
---
rest_framework/tests/fields.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index d82f6b23..109fd01f 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -808,4 +808,5 @@ class URLFieldTests(TestCase):
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
\ No newline at end of file
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20
+
\ No newline at end of file
--
cgit v1.2.3
From 6714b755f4938e998907e3d949c832f77a76fadc Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 16:32:31 +0200
Subject: Fix missing para..
---
rest_framework/tests/fields.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index 109fd01f..dad69975 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -808,5 +808,4 @@ class URLFieldTests(TestCase):
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20
-
\ No newline at end of file
+ self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
--
cgit v1.2.3
From ebe959b52a10a88975b15c69275b0ef5c50cb9fa Mon Sep 17 00:00:00 2001
From: Karol Majta
Date: Sat, 18 May 2013 16:45:05 +0200
Subject: charset param gets now appended to response's Content-Type. Closes
#807
---
rest_framework/negotiation.py | 10 +++++--
rest_framework/renderers.py | 2 +-
rest_framework/response.py | 10 +++++--
rest_framework/settings.py | 2 ++
rest_framework/tests/negotiation.py | 35 +++++++++++++++++++----
rest_framework/tests/response.py | 57 +++++++++++++++++++++++++++++++++----
rest_framework/views.py | 16 +++++++++--
7 files changed, 111 insertions(+), 21 deletions(-)
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 4d205c0e..668c4e5c 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -58,11 +58,17 @@ class DefaultContentNegotiation(BaseContentNegotiation):
_MediaType(media_type).precedence):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
- return renderer, renderer.media_type
+ renderer_and_media_type = renderer, renderer.media_type
else:
# Eg client requests 'application/json; indent=8'
# Accepted media type is 'application/json; indent=8'
- return renderer, media_type
+ renderer_and_media_type = renderer, media_type
+ if renderer.charset:
+ charset = renderer.charset
+ else:
+ charset = self.__class__.settings.DEFAULT_CHARSET
+ retval = renderer_and_media_type + (charset,)
+ return retval
raise exceptions.NotAcceptable(available_renderers=renderers)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 8361cd40..65d8b686 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -36,11 +36,11 @@ class BaseRenderer(object):
media_type = None
format = None
+ charset = None
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.
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 26e4ab37..40372f22 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -39,14 +39,18 @@ class Response(SimpleTemplateResponse):
def rendered_content(self):
renderer = getattr(self, 'accepted_renderer', None)
media_type = getattr(self, 'accepted_media_type', None)
+ charset = getattr(self, 'charset', 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
-
- self['Content-Type'] = media_type
+ if charset is not None:
+ ct = "{0}; charset={1}".format(media_type, charset)
+ else:
+ ct = media_type
+ self['Content-Type'] = ct
return renderer.render(self.data, media_type, context)
@property
@@ -67,4 +71,4 @@ class Response(SimpleTemplateResponse):
for key in ('accepted_renderer', 'renderer_context', 'data'):
if key in state:
del state[key]
- return state
+ return state
\ No newline at end of file
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index beb511ac..255a95e2 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -83,6 +83,8 @@ DEFAULTS = {
'FORMAT_SUFFIX_KWARG': 'format',
# Input and output formats
+ 'DEFAULT_CHARSET': None,
+
'DATE_INPUT_FORMATS': (
ISO_8601,
),
diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py
index 43721b84..d7ef6470 100644
--- a/rest_framework/tests/negotiation.py
+++ b/rest_framework/tests/negotiation.py
@@ -3,18 +3,24 @@ 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
factory = RequestFactory()
-class MockJSONRenderer(object):
+class MockJSONRenderer(BaseRenderer):
media_type = 'application/json'
-
-class MockHTMLRenderer(object):
+class MockHTMLRenderer(BaseRenderer):
media_type = 'text/html'
+class NoCharsetSpecifiedRenderer(BaseRenderer):
+ media_type = 'my/media'
+
+class CharsetSpecifiedRenderer(BaseRenderer):
+ media_type = 'my/media'
+ charset = 'mycharset'
class TestAcceptedMediaType(TestCase):
def setUp(self):
@@ -26,15 +32,32 @@ class TestAcceptedMediaType(TestCase):
def test_client_without_accept_use_renderer(self):
request = Request(factory.get('/'))
- accepted_renderer, accepted_media_type = self.select_renderer(request)
+ accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json')
def test_client_underspecifies_accept_use_renderer(self):
request = Request(factory.get('/', HTTP_ACCEPT='*/*'))
- accepted_renderer, accepted_media_type = self.select_renderer(request)
+ accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json')
def test_client_overspecifies_accept_use_client(self):
request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8'))
- accepted_renderer, accepted_media_type = self.select_renderer(request)
+ accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json; indent=8')
+
+class TestCharset(TestCase):
+ def setUp(self):
+ self.renderers = [NoCharsetSpecifiedRenderer()]
+ self.negotiator = DefaultContentNegotiation()
+
+ def test_returns_none_if_no_charset_set(self):
+ request = Request(factory.get('/'))
+ renderers = [NoCharsetSpecifiedRenderer()]
+ _, _, charset = self.negotiator.select_renderer(request, renderers)
+ self.assertIsNone(charset)
+
+ def test_returns_attribute_from_renderer_if_charset_is_set(self):
+ request = Request(factory.get('/'))
+ renderers = [CharsetSpecifiedRenderer()]
+ _, _, charset = self.negotiator.select_renderer(request, renderers)
+ self.assertEquals(CharsetSpecifiedRenderer.charset, charset)
\ No newline at end of file
diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py
index aecf83f4..f2a1c635 100644
--- a/rest_framework/tests/response.py
+++ b/rest_framework/tests/response.py
@@ -12,7 +12,6 @@ from rest_framework.renderers import (
from rest_framework.settings import api_settings
from rest_framework.compat import six
-
class MockPickleRenderer(BaseRenderer):
media_type = 'application/pickle'
@@ -20,6 +19,8 @@ class MockPickleRenderer(BaseRenderer):
class MockJsonRenderer(BaseRenderer):
media_type = 'application/json'
+class MockTextMediaRenderer(BaseRenderer):
+ media_type = 'text/html'
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
@@ -43,14 +44,18 @@ class RendererB(BaseRenderer):
def render(self, data, media_type=None, renderer_context=None):
return RENDERER_B_SERIALIZER(data)
+class RendererC(RendererB):
+ media_type = 'mock/rendererc'
+ format = 'formatc'
+ charset = "rendererc"
+
class MockView(APIView):
- renderer_classes = (RendererA, RendererB)
+ renderer_classes = (RendererA, RendererB, RendererC)
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
-
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -64,10 +69,9 @@ class HTMLView1(APIView):
def get(self, request, **kwargs):
return Response('text')
-
urlpatterns = patterns('',
- url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
- url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
+ url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
@@ -173,3 +177,44 @@ class Issue122Tests(TestCase):
Test if no infinite recursion occurs.
"""
self.client.get('/html1')
+
+class Issue807Testts(TestCase):
+ """
+ Covers #807
+ """
+
+ urls = 'rest_framework.tests.response'
+
+ def test_does_not_append_charset_by_default(self):
+ """
+ For backwards compatibility `REST_FRAMEWORK['DEFAULT_CHARSET']` defaults
+ to None, so that all legacy code works as expected.
+ """
+ headers = {"HTTP_ACCEPT": RendererA.media_type}
+ resp = self.client.get('/', **headers)
+ self.assertEquals(RendererA.media_type, resp['Content-Type'])
+
+ def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self):
+ """
+ If renderer class has charset attribute declared, it gets appended
+ to Response's Content-Type
+ """
+ resp = self.client.get('/?format=%s' % RendererC.format)
+ expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
+ self.assertEquals(expected, resp['Content-Type'])
+
+ def test_if_there_is_default_charset_specified_it_gets_appended(self):
+ """
+ If user defines `REST_FRAMEWORK['DEFAULT_CHARSET']` it will get appended
+ to Content-Type of all responses.
+ """
+ original_default_charset = api_settings.DEFAULT_CHARSET
+ api_settings.DEFAULT_CHARSET = "utf-8"
+ headers = {'HTTP_ACCEPT': RendererA.media_type}
+ resp = self.client.get('/', **headers)
+ expected = "{0}; charset={1}".format(
+ RendererA.media_type,
+ api_settings.DEFAULT_CHARSET
+ )
+ self.assertEquals(expected, resp['Content-Type'])
+ api_settings.DEFAULT_CHARSET = original_default_charset
\ No newline at end of file
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 555fa2f4..035aa646 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -183,7 +183,9 @@ class APIView(View):
return conneg.select_renderer(request, renderers, self.format_kwarg)
except Exception:
if force:
- return (renderers[0], renderers[0].media_type)
+ charset = renderers[0].charset
+ charset = charset if charset is not None else api_settings.DEFAULT_CHARSET
+ return (renderers[0], renderers[0].media_type, renderers[0].charset)
raise
def perform_authentication(self, request):
@@ -250,7 +252,10 @@ class APIView(View):
# 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
+ renderer, media_type, charset = neg
+ request.accepted_renderer = renderer
+ request.accepted_media_type = media_type
+ request.accepted_charset = charset
def finalize_response(self, request, response, *args, **kwargs):
"""
@@ -265,11 +270,16 @@ class APIView(View):
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
+ renderer, media_type, charset = neg
+ request.accepted_renderer = renderer
+ request.accepted_media_type = media_type
response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type
response.renderer_context = self.get_renderer_context()
+ charset = request.accepted_renderer.charset
+ charset = charset if charset else api_settings.DEFAULT_CHARSET
+ response.charset = charset
for key, value in self.headers.items():
response[key] = value
--
cgit v1.2.3
From 0c81d04170da19e5bfb332641d92d9c3346598f8 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 17:04:20 +0200
Subject: Add min_value=0 to autogenerated Pos..IntFields
---
rest_framework/serializers.py | 4 +++
rest_framework/tests/serializer.py | 73 ++++++++++++++++++++++++++++++++++++++
2 files changed, 77 insertions(+)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ff5eb873..d0313513 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -739,6 +739,10 @@ class ModelSerializer(Serializer):
if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
+ if issubclass(model_field.__class__, models.PositiveIntegerField) or\
+ issubclass(model_field.__class__, models.PositiveSmallIntegerField):
+ kwargs['min_value'] = 0
+
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
kwargs['choices'] = model_field.flatchoices
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 4f188c3e..220a581a 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1402,3 +1402,76 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def test_url_field(self):
self.field_test('url_field')
+
+
+class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
+
+ def setUp(self):
+ class DVOAFModel(RESTFrameworkModel):
+ positive_integer_field = models.PositiveIntegerField(blank=True)
+ positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
+ email_field = models.EmailField(blank=True)
+ file_field = models.FileField(blank=True)
+ image_field = models.ImageField(blank=True)
+ slug_field = models.SlugField(blank=True)
+ url_field = models.URLField(blank=True)
+
+ class DVOAFSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DVOAFModel
+
+ self.serializer_class = DVOAFSerializer
+ self.fields_attributes = {
+ 'positive_integer_field': [
+ ('min_value', 0),
+ ],
+ 'positive_small_integer_field': [
+ ('min_value', 0),
+ ],
+ 'email_field': [
+ ('max_length', 75),
+ ],
+ 'file_field': [
+ ('max_length', 100),
+ ],
+ 'image_field': [
+ ('max_length', 100),
+ ],
+ 'slug_field': [
+ ('max_length', 50),
+ ],
+ 'url_field': [
+ ('max_length', 200),
+ ],
+ }
+
+ def field_test(self, field):
+ serializer = self.serializer_class(data={})
+ self.assertEqual(serializer.is_valid(), True)
+
+ for attribute in self.fields_attributes[field]:
+ self.assertEqual(
+ getattr(serializer.fields[field], attribute[0]),
+ attribute[1]
+ )
+
+ def test_positive_integer_field(self):
+ self.field_test('positive_integer_field')
+
+ def test_positive_small_integer_field(self):
+ self.field_test('positive_small_integer_field')
+
+ def test_email_field(self):
+ self.field_test('email_field')
+
+ def test_file_field(self):
+ self.field_test('file_field')
+
+ def test_image_field(self):
+ self.field_test('image_field')
+
+ def test_slug_field(self):
+ self.field_test('slug_field')
+
+ def test_url_field(self):
+ self.field_test('url_field')
--
cgit v1.2.3
From c69f960066dfdef2163a53205f27c2b9e457068f Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 16:30:40 +0200
Subject: Adding a class attribute into JSONRenderer for ensuring ascii, and
using it consistently
---
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 8361cd40..d55a3e0e 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -49,6 +49,7 @@ class JSONRenderer(BaseRenderer):
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
+ ensure_ascii = True
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -72,7 +73,7 @@ class JSONRenderer(BaseRenderer):
except (ValueError, TypeError):
indent = None
- return json.dumps(data, cls=self.encoder_class, indent=indent)
+ return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii)
class JSONPRenderer(JSONRenderer):
--
cgit v1.2.3
From 97f034e3d65068f7bd1e982e6fd251222a4feea1 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 16:31:12 +0200
Subject: Adds UnicodeJSONRenderer which doesn't ensure ascii
---
rest_framework/renderers.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index d55a3e0e..12e1107f 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -76,6 +76,10 @@ class JSONRenderer(BaseRenderer):
return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii)
+class UnicodeJSONRenderer(JSONRenderer):
+ ensure_ascii = False
+
+
class JSONPRenderer(JSONRenderer):
"""
Renderer which serializes to json,
--
cgit v1.2.3
From 6dbbbc16da740fb27f9a5a390fb61400e9f6ff64 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 16:32:53 +0200
Subject: Better checking if the content can be printable in the BrowsableAPI
---
rest_framework/renderers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 12e1107f..4345a313 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -325,7 +325,7 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
- if not all(char in string.printable for char in content):
+ if not isinstance(content, six.text_type):
return '[%d bytes of binary content]'
return content
--
cgit v1.2.3
From 71e29644a2950d8a82cb26f6a3e39fb76faf9707 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 16:50:09 +0200
Subject: Adds new renderer into the documentation
---
docs/api-guide/renderers.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index b9a9fd7a..1661ceec 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -67,7 +67,7 @@ If your API includes views that can serve both regular webpages and API response
## JSONRenderer
-Renders the request data into `JSON`.
+Renders the request data into `JSON` enforcing ASCII encoding
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
@@ -75,6 +75,10 @@ The client may additionally include an `'indent'` media type parameter, in which
**.format**: `'.json'`
+## UnicodeJSONRenderer
+
+Same as `JSONRenderer` but doesn't enforce ASCII encoding
+
## JSONPRenderer
Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
--
cgit v1.2.3
From b9b22976125fffa4552da695183bf75fbaf0b927 Mon Sep 17 00:00:00 2001
From: Pablo Recio
Date: Sat, 18 May 2013 17:06:25 +0200
Subject: Tests proper encoding in JSONRenderer and UnicodeJSONRenderer
---
rest_framework/tests/renderers.py | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 40bac9cb..739f9184 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
from decimal import Decimal
from django.core.cache import cache
from django.test import TestCase
@@ -8,7 +9,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
+ XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.compat import StringIO
@@ -254,6 +255,23 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content), _indented_repr)
+ def test_check_ascii(self):
+ obj = {'countries': ['United Kingdom', 'France', 'España']}
+ renderer = JSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}')
+
+
+class UnicodeJSONRendererTests(TestCase):
+ """
+ Tests specific for the Unicode JSON Renderer
+ """
+ def test_proper_encoding(self):
+ obj = {'countries': ['United Kingdom', 'France', 'España']}
+ renderer = UnicodeJSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}')
+
class JSONPRendererTests(TestCase):
"""
--
cgit v1.2.3
From b4dbfa9832e2a29a5908ddf27f8746971a8e3c56 Mon Sep 17 00:00:00 2001
From: Nikolaus Schlemm
Date: Sat, 18 May 2013 17:10:13 +0200
Subject: only catch relevant exceptions ;)
---
rest_framework/views.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 2dd2c59e..719df428 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -83,7 +83,10 @@ class APIView(View):
field_name_types[name] = field.__class__.__name__
actions[method] = field_name_types
- except:
+ except exceptions.PermissionDenied:
+ # don't add this method
+ pass
+ except exceptions.NotAuthenticated:
# don't add this method
pass
--
cgit v1.2.3
From a42afa04c38afe25c9032b8ce37b572678b02cf1 Mon Sep 17 00:00:00 2001
From: Nikolaus Schlemm
Date: Sat, 18 May 2013 17:13:23 +0200
Subject: draft for fields' metadata via OPTIONS - needs review and decision
---
rest_framework/tests/generics.py | 42 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 40 insertions(+), 2 deletions(-)
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index 15d87e86..014195ae 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -121,8 +121,27 @@ class TestRootView(TestCase):
'text/html'
],
'name': 'Root',
- 'description': 'Example description for OPTIONS.'
+ 'description': 'Example description for OPTIONS.',
+ 'actions': {}
}
+ # TODO: this is just a draft for fields' metadata - needs review and decision
+ for method in ('HEAD', 'GET', 'POST', 'OPTIONS'):
+ expected['actions'][method] = {
+ 'text': {
+ 'description': '',
+ 'label': '',
+ 'readonly': False,
+ 'required': True,
+ 'type': 'CharField',
+ },
+ 'id': {
+ 'description': '',
+ 'label': '',
+ 'readonly': True,
+ 'required': True,
+ 'type': 'IntegerField',
+ },
+ }
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected)
@@ -238,8 +257,27 @@ class TestInstanceView(TestCase):
'text/html'
],
'name': 'Instance',
- 'description': 'Example description for OPTIONS.'
+ 'description': 'Example description for OPTIONS.',
+ 'actions': {}
}
+ # TODO: this is just a draft idea for fields' metadata - needs review and decision
+ for method in ('HEAD', 'GET', 'PATCH', 'PUT', 'OPTIONS', 'DELETE'):
+ expected['actions'][method] = {
+ 'text': {
+ 'description': '',
+ 'label': '',
+ 'readonly': False,
+ 'required': True,
+ 'type': 'CharField',
+ },
+ 'id': {
+ 'description': '',
+ 'label': '',
+ 'readonly': True,
+ 'required': True,
+ 'type': 'IntegerField',
+ },
+ }
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected)
--
cgit v1.2.3
From 025c40e7d5efbba0c3e65ab3737adef988995a24 Mon Sep 17 00:00:00 2001
From: Stephan Groß
Date: Sat, 18 May 2013 17:16:29 +0200
Subject: Fix order for applying min_value to fields
---
rest_framework/serializers.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d0313513..943fba6b 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -739,15 +739,16 @@ class ModelSerializer(Serializer):
if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
- if issubclass(model_field.__class__, models.PositiveIntegerField) or\
- issubclass(model_field.__class__, models.PositiveSmallIntegerField):
- kwargs['min_value'] = 0
-
# 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'],
--
cgit v1.2.3
From 10e451a85a034d1158148f54e98147e81454c2ed Mon Sep 17 00:00:00 2001
From: Ryan Kaskel
Date: Sat, 18 May 2013 16:21:18 +0100
Subject: Handle Python 3 strings and lazy strings.
---
rest_framework/fields.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 2ab603cf..c3ac6281 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -19,6 +19,7 @@ from django.db.models.fields import BLANK_CHOICE_DASH
from django import forms
from django.forms import widgets
from django.utils.encoding import is_protected_type
+from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
@@ -45,6 +46,15 @@ def is_simple_callable(obj):
len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults
+if six.PY3:
+ def is_non_str_iterable(obj):
+ if (isinstance(obj, str) or
+ (isinstance(obj, Promise) and obj._delegate_text)):
+ return False
+ return hasattr(obj, '__iter__')
+else:
+ def is_non_str_iterable(obj):
+ return hasattr(obj, '__iter__')
def get_component(obj, attr_name):
"""
@@ -169,7 +179,8 @@ class Field(object):
if is_protected_type(value):
return value
- elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
+ 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
--
cgit v1.2.3
From 5ab7cc6e6be5445bc0d4ccc26f1ec84239af74d5 Mon Sep 17 00:00:00 2001
From: Nikolaus Schlemm
Date: Sat, 18 May 2013 17:38:47 +0200
Subject: HEAD and OPTIONS should not be exposed as actions as discussed in
https://github.com/nschlemm/django-rest-framework/commit/a42afa04c38afe25c9032b8ce37b572678b02cf1#commitcomment-3241476
---
rest_framework/tests/generics.py | 4 ++--
rest_framework/views.py | 4 ++++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index 014195ae..d8556638 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -125,7 +125,7 @@ class TestRootView(TestCase):
'actions': {}
}
# TODO: this is just a draft for fields' metadata - needs review and decision
- for method in ('HEAD', 'GET', 'POST', 'OPTIONS'):
+ for method in ('GET', 'POST',):
expected['actions'][method] = {
'text': {
'description': '',
@@ -261,7 +261,7 @@ class TestInstanceView(TestCase):
'actions': {}
}
# TODO: this is just a draft idea for fields' metadata - needs review and decision
- for method in ('HEAD', 'GET', 'PATCH', 'PUT', 'OPTIONS', 'DELETE'):
+ for method in ('GET', 'PATCH', 'PUT', 'DELETE'):
expected['actions'][method] = {
'text': {
'description': '',
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 719df428..c5b89a02 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -71,6 +71,10 @@ class APIView(View):
actions = {}
for method in self.allowed_methods:
+ # skip HEAD and OPTIONS
+ if method in ('HEAD', 'OPTIONS'):
+ continue
+
cloned_request = clone_request(request, method)
try:
self.check_permissions(cloned_request)
--
cgit v1.2.3
From 579f77ceaa03a216a7a635c3d3a4d83b0e5868f8 Mon Sep 17 00:00:00 2001
From: Ryan Kaskel
Date: Sat, 18 May 2013 17:04:17 +0100
Subject: Move function to compat.
---
rest_framework/compat.py | 13 +++++++++++++
rest_framework/fields.py | 13 +------------
2 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index cd39f544..76dc0052 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -495,3 +495,16 @@ except ImportError:
oauth2_provider_forms = None
oauth2_provider_scope = None
oauth2_constants = None
+
+# Handle lazy strings
+from django.utils.functional import Promise
+
+if six.PY3:
+ def is_non_str_iterable(obj):
+ if (isinstance(obj, str) or
+ (isinstance(obj, Promise) and obj._delegate_text)):
+ return False
+ return hasattr(obj, '__iter__')
+else:
+ def is_non_str_iterable(obj):
+ return hasattr(obj, '__iter__')
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index d5cf30e4..b5f99823 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -19,7 +19,6 @@ from django.db.models.fields import BLANK_CHOICE_DASH
from django import forms
from django.forms import widgets
from django.utils.encoding import is_protected_type
-from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
@@ -27,7 +26,7 @@ from rest_framework import ISO_8601
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
from rest_framework.compat import BytesIO
from rest_framework.compat import six
-from rest_framework.compat import smart_text, force_text
+from rest_framework.compat import smart_text, force_text, is_non_str_iterable
from rest_framework.settings import api_settings
@@ -46,16 +45,6 @@ def is_simple_callable(obj):
len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults
-if six.PY3:
- def is_non_str_iterable(obj):
- if (isinstance(obj, str) or
- (isinstance(obj, Promise) and obj._delegate_text)):
- return False
- return hasattr(obj, '__iter__')
-else:
- def is_non_str_iterable(obj):
- return hasattr(obj, '__iter__')
-
def get_component(obj, attr_name):
"""
Given an object, and an attribute name,
--
cgit v1.2.3
From 6af61a19b78b0ebb9d8403e96c6e4a77c675b141 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 18:07:43 +0200
Subject: Added @matteosuppo for work on #865. Thanks!
---
docs/topics/credits.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index d805c0c1..0e206de8 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -130,6 +130,7 @@ The following people have helped make REST framework great.
* Òscar Vilaplana - [grimborg]
* Ryan Kaskel - [ryankask]
* Andy McKay - [andymckay]
+* Matteo Suppo - [matteosuppo]
Many thanks to everyone who's contributed to the project.
@@ -296,3 +297,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[grimborg]: https://github.com/grimborg
[ryankask]: https://github.com/ryankask
[andymckay]: https://github.com/andymckay
+[matteosuppo]: https://github.com/matteosuppo
--
cgit v1.2.3
From 0d3d66cb0232e1067600ef22fcf88937ac6bee9d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 17:21:43 +0100
Subject: Added proper charset support
---
rest_framework/negotiation.py | 10 ++------
rest_framework/renderers.py | 6 +++++
rest_framework/response.py | 18 ++++++++-----
rest_framework/settings.py | 2 --
rest_framework/tests/htmlrenderer.py | 10 ++++----
rest_framework/tests/negotiation.py | 28 ++++----------------
rest_framework/tests/response.py | 50 ++++++++++++++++++++----------------
rest_framework/views.py | 16 +++---------
8 files changed, 60 insertions(+), 80 deletions(-)
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 668c4e5c..4d205c0e 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -58,17 +58,11 @@ class DefaultContentNegotiation(BaseContentNegotiation):
_MediaType(media_type).precedence):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
- renderer_and_media_type = renderer, renderer.media_type
+ return renderer, renderer.media_type
else:
# Eg client requests 'application/json; indent=8'
# Accepted media type is 'application/json; indent=8'
- renderer_and_media_type = renderer, media_type
- if renderer.charset:
- charset = renderer.charset
- else:
- charset = self.__class__.settings.DEFAULT_CHARSET
- retval = renderer_and_media_type + (charset,)
- return retval
+ return renderer, media_type
raise exceptions.NotAcceptable(available_renderers=renderers)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 65d8b686..b91e3861 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -41,6 +41,7 @@ class BaseRenderer(object):
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.
@@ -115,6 +116,7 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
+ charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -164,6 +166,7 @@ class YAMLRenderer(BaseRenderer):
media_type = 'application/yaml'
format = 'yaml'
encoder = encoders.SafeDumper
+ charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -204,6 +207,7 @@ class TemplateHTMLRenderer(BaseRenderer):
'%(status_code)s.html',
'api_exception.html'
]
+ charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -275,6 +279,7 @@ class StaticHTMLRenderer(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 {}
@@ -296,6 +301,7 @@ class BrowsableAPIRenderer(BaseRenderer):
media_type = 'text/html'
format = 'api'
template = 'rest_framework/api.html'
+ charset = 'utf-8'
def get_default_renderer(self, view):
"""
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 40372f22..32e74a45 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse):
def __init__(self, data=None, status=200,
template_name=None, headers=None,
- exception=False):
+ exception=False, charset=None):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
@@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse):
self.data = data
self.template_name = template_name
self.exception = exception
+ self.charset = charset
if headers:
for name, value in six.iteritems(headers):
@@ -39,18 +40,21 @@ class Response(SimpleTemplateResponse):
def rendered_content(self):
renderer = getattr(self, 'accepted_renderer', None)
media_type = getattr(self, 'accepted_media_type', None)
- charset = getattr(self, 'charset', 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
- if charset is not None:
- ct = "{0}; charset={1}".format(media_type, charset)
+
+ if self.charset is None:
+ self.charset = renderer.charset
+
+ if self.charset is not None:
+ content_type = "{0}; charset={1}".format(media_type, self.charset)
else:
- ct = media_type
- self['Content-Type'] = ct
+ content_type = media_type
+ self['Content-Type'] = content_type
return renderer.render(self.data, media_type, context)
@property
@@ -71,4 +75,4 @@ class Response(SimpleTemplateResponse):
for key in ('accepted_renderer', 'renderer_context', 'data'):
if key in state:
del state[key]
- return state
\ No newline at end of file
+ return state
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 255a95e2..beb511ac 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -83,8 +83,6 @@ DEFAULTS = {
'FORMAT_SUFFIX_KWARG': 'format',
# Input and output formats
- 'DEFAULT_CHARSET': None,
-
'DATE_INPUT_FORMATS': (
ISO_8601,
),
diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py
index 8f2e2b5a..5d18a6e8 100644
--- a/rest_framework/tests/htmlrenderer.py
+++ b/rest_framework/tests/htmlrenderer.py
@@ -66,19 +66,19 @@ class TemplateHTMLRendererTests(TestCase):
def test_simple_html_view(self):
response = self.client.get('/')
self.assertContains(response, "example: foobar")
- self.assertEqual(response['Content-Type'], 'text/html')
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_not_found_html_view(self):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.content, six.b("404 Not Found"))
- self.assertEqual(response['Content-Type'], 'text/html')
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("403 Forbidden"))
- self.assertEqual(response['Content-Type'], 'text/html')
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
class TemplateHTMLRendererExceptionTests(TestCase):
@@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.content, six.b("404: Not found"))
- self.assertEqual(response['Content-Type'], 'text/html')
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("403: Permission denied"))
- self.assertEqual(response['Content-Type'], 'text/html')
+ self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py
index d7ef6470..7f84827f 100644
--- a/rest_framework/tests/negotiation.py
+++ b/rest_framework/tests/negotiation.py
@@ -12,15 +12,14 @@ factory = RequestFactory()
class MockJSONRenderer(BaseRenderer):
media_type = 'application/json'
+
class MockHTMLRenderer(BaseRenderer):
media_type = 'text/html'
+
class NoCharsetSpecifiedRenderer(BaseRenderer):
media_type = 'my/media'
-class CharsetSpecifiedRenderer(BaseRenderer):
- media_type = 'my/media'
- charset = 'mycharset'
class TestAcceptedMediaType(TestCase):
def setUp(self):
@@ -32,32 +31,15 @@ class TestAcceptedMediaType(TestCase):
def test_client_without_accept_use_renderer(self):
request = Request(factory.get('/'))
- accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
+ accepted_renderer, accepted_media_type = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json')
def test_client_underspecifies_accept_use_renderer(self):
request = Request(factory.get('/', HTTP_ACCEPT='*/*'))
- accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
+ accepted_renderer, accepted_media_type = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json')
def test_client_overspecifies_accept_use_client(self):
request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8'))
- accepted_renderer, accepted_media_type, charset = self.select_renderer(request)
+ accepted_renderer, accepted_media_type = self.select_renderer(request)
self.assertEqual(accepted_media_type, 'application/json; indent=8')
-
-class TestCharset(TestCase):
- def setUp(self):
- self.renderers = [NoCharsetSpecifiedRenderer()]
- self.negotiator = DefaultContentNegotiation()
-
- def test_returns_none_if_no_charset_set(self):
- request = Request(factory.get('/'))
- renderers = [NoCharsetSpecifiedRenderer()]
- _, _, charset = self.negotiator.select_renderer(request, renderers)
- self.assertIsNone(charset)
-
- def test_returns_attribute_from_renderer_if_charset_is_set(self):
- request = Request(factory.get('/'))
- renderers = [CharsetSpecifiedRenderer()]
- _, _, charset = self.negotiator.select_renderer(request, renderers)
- self.assertEquals(CharsetSpecifiedRenderer.charset, charset)
\ No newline at end of file
diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py
index f2a1c635..8f1163e8 100644
--- a/rest_framework/tests/response.py
+++ b/rest_framework/tests/response.py
@@ -12,6 +12,7 @@ from rest_framework.renderers import (
from rest_framework.settings import api_settings
from rest_framework.compat import six
+
class MockPickleRenderer(BaseRenderer):
media_type = 'application/pickle'
@@ -19,6 +20,7 @@ class MockPickleRenderer(BaseRenderer):
class MockJsonRenderer(BaseRenderer):
media_type = 'application/json'
+
class MockTextMediaRenderer(BaseRenderer):
media_type = 'text/html'
@@ -44,6 +46,7 @@ class RendererB(BaseRenderer):
def render(self, data, media_type=None, renderer_context=None):
return RENDERER_B_SERIALIZER(data)
+
class RendererC(RendererB):
media_type = 'mock/rendererc'
format = 'formatc'
@@ -56,6 +59,14 @@ class MockView(APIView):
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
+
+class MockViewSettingCharset(APIView):
+ renderer_classes = (RendererA, RendererB, RendererC)
+
+ def get(self, request, **kwargs):
+ return Response(DUMMYCONTENT, status=DUMMYSTATUS, charset='setbyview')
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -70,6 +81,7 @@ class HTMLView1(APIView):
return Response('text')
urlpatterns = patterns('',
+ url(r'^setbyview$', MockViewSettingCharset.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()),
@@ -178,43 +190,37 @@ class Issue122Tests(TestCase):
"""
self.client.get('/html1')
+
class Issue807Testts(TestCase):
"""
Covers #807
"""
-
+
urls = 'rest_framework.tests.response'
-
+
def test_does_not_append_charset_by_default(self):
"""
- For backwards compatibility `REST_FRAMEWORK['DEFAULT_CHARSET']` defaults
- to None, so that all legacy code works as expected.
+ Renderers don't include a charset unless set explicitly.
"""
headers = {"HTTP_ACCEPT": RendererA.media_type}
resp = self.client.get('/', **headers)
- self.assertEquals(RendererA.media_type, resp['Content-Type'])
-
+ self.assertEqual(RendererA.media_type, resp['Content-Type'])
+
def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self):
"""
If renderer class has charset attribute declared, it gets appended
to Response's Content-Type
"""
- resp = self.client.get('/?format=%s' % RendererC.format)
+ headers = {"HTTP_ACCEPT": RendererC.media_type}
+ resp = self.client.get('/', **headers)
expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
- self.assertEquals(expected, resp['Content-Type'])
-
- def test_if_there_is_default_charset_specified_it_gets_appended(self):
+ self.assertEqual(expected, resp['Content-Type'])
+
+ def test_charset_set_explictly_on_response(self):
"""
- If user defines `REST_FRAMEWORK['DEFAULT_CHARSET']` it will get appended
- to Content-Type of all responses.
+ The charset may be set explictly on the response.
"""
- original_default_charset = api_settings.DEFAULT_CHARSET
- api_settings.DEFAULT_CHARSET = "utf-8"
- headers = {'HTTP_ACCEPT': RendererA.media_type}
- resp = self.client.get('/', **headers)
- expected = "{0}; charset={1}".format(
- RendererA.media_type,
- api_settings.DEFAULT_CHARSET
- )
- self.assertEquals(expected, resp['Content-Type'])
- api_settings.DEFAULT_CHARSET = original_default_charset
\ No newline at end of file
+ headers = {"HTTP_ACCEPT": RendererC.media_type}
+ resp = self.client.get('/setbyview', **headers)
+ expected = "{0}; charset={1}".format(RendererC.media_type, 'setbyview')
+ self.assertEqual(expected, resp['Content-Type'])
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 035aa646..555fa2f4 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -183,9 +183,7 @@ class APIView(View):
return conneg.select_renderer(request, renderers, self.format_kwarg)
except Exception:
if force:
- charset = renderers[0].charset
- charset = charset if charset is not None else api_settings.DEFAULT_CHARSET
- return (renderers[0], renderers[0].media_type, renderers[0].charset)
+ return (renderers[0], renderers[0].media_type)
raise
def perform_authentication(self, request):
@@ -252,10 +250,7 @@ class APIView(View):
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
- renderer, media_type, charset = neg
- request.accepted_renderer = renderer
- request.accepted_media_type = media_type
- request.accepted_charset = charset
+ request.accepted_renderer, request.accepted_media_type = neg
def finalize_response(self, request, response, *args, **kwargs):
"""
@@ -270,16 +265,11 @@ class APIView(View):
if isinstance(response, Response):
if not getattr(request, 'accepted_renderer', None):
neg = self.perform_content_negotiation(request, force=True)
- renderer, media_type, charset = neg
- request.accepted_renderer = renderer
- request.accepted_media_type = media_type
+ 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()
- charset = request.accepted_renderer.charset
- charset = charset if charset else api_settings.DEFAULT_CHARSET
- response.charset = charset
for key, value in self.headers.items():
response[key] = value
--
cgit v1.2.3
From d4721fc1f70050fc275ad16958e4795e0f3aab1c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 17:25:38 +0100
Subject: Correct charset on UnicodeJSONRenderer
---
rest_framework/renderers.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index a67c5eea..9d08c5d4 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -79,6 +79,7 @@ class JSONRenderer(BaseRenderer):
class UnicodeJSONRenderer(JSONRenderer):
ensure_ascii = False
+ charset = 'utf-8'
class JSONPRenderer(JSONRenderer):
--
cgit v1.2.3
From 7f1cc82f96c2ba4064b28957a8b2d5b313be3c40 Mon Sep 17 00:00:00 2001
From: Nikolaus Schlemm
Date: Sat, 18 May 2013 18:29:51 +0200
Subject: added unittests for permission check of exposing actions via OPTIONS
---
rest_framework/tests/permissions.py | 45 +++++++++++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py
index b3993be5..5a18182b 100644
--- a/rest_framework/tests/permissions.py
+++ b/rest_framework/tests/permissions.py
@@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase):
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',
+ 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.assertEquals(response.data['actions'].keys(), ['POST', 'GET',])
+
+ request = factory.options('/1', content_type='application/json',
+ HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'DELETE', 'GET',])
+
+ def test_options_disallowed(self):
+ request = factory.options('/', content_type='application/json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEquals(response.data['actions'].keys(), ['GET',])
+
+ request = factory.options('/1', content_type='application/json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEquals(response.data['actions'].keys(), ['GET',])
+
+ def test_options_updateonly(self):
+ request = factory.options('/', content_type='application/json',
+ HTTP_AUTHORIZATION=self.updateonly_credentials)
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEquals(response.data['actions'].keys(), ['GET',])
+
+ request = factory.options('/1', content_type='application/json',
+ HTTP_AUTHORIZATION=self.updateonly_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'GET',])
+
class OwnerModel(models.Model):
text = models.CharField(max_length=100)
--
cgit v1.2.3
From 351814e110d642bf1351b95141924190e56d9694 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 18:32:02 +0200
Subject: Added @lolek09 for work on #862. Kick ass!
---
docs/topics/credits.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index 0e206de8..acd6576e 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -131,6 +131,7 @@ The following people have helped make REST framework great.
* Ryan Kaskel - [ryankask]
* Andy McKay - [andymckay]
* Matteo Suppo - [matteosuppo]
+* Karol Majta - [lolek09]
Many thanks to everyone who's contributed to the project.
@@ -298,3 +299,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[ryankask]: https://github.com/ryankask
[andymckay]: https://github.com/andymckay
[matteosuppo]: https://github.com/matteosuppo
+[lolek09]: https://github.com/lolek09
--
cgit v1.2.3
From 4631b91e144ae758b4417f20b0379ce5b9179ee6 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 21:12:44 +0100
Subject: Fix broken 'binary content' in browseable API
---
rest_framework/renderers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 9d08c5d4..6b508e6d 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -332,8 +332,8 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
- if not isinstance(content, six.text_type):
- return '[%d bytes of binary content]'
+ if not all(char in string.printable for char in content):
+ return '[%d bytes of binary content]' % len(content)
return content
--
cgit v1.2.3
From 4802177766b92c644d7c6f446d0bdf2dbe1917e6 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 21:19:53 +0100
Subject: Enforce object permissions check when rendering forms in browseable
API
---
rest_framework/renderers.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 6b508e6d..08df7e91 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -349,6 +349,7 @@ class BrowsableAPIRenderer(BaseRenderer):
try:
view.check_permissions(request)
+ view.check_object_permissions(request, obj)
except exceptions.APIException:
return False # Doesn't have permissions
return True
--
cgit v1.2.3
From 7c945b43f05f1b340f78c23f80c8043937c7fd2a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 18 May 2013 23:52:02 +0100
Subject: Only use object permissions in browsable api form generation if an
object exists
---
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 08df7e91..c67c8ed6 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -349,7 +349,8 @@ class BrowsableAPIRenderer(BaseRenderer):
try:
view.check_permissions(request)
- view.check_object_permissions(request, obj)
+ if obj is not None:
+ view.check_object_permissions(request, obj)
except exceptions.APIException:
return False # Doesn't have permissions
return True
--
cgit v1.2.3
From c0f3a1c397a564ee78b3a656f14f7ff46b0d2b31 Mon Sep 17 00:00:00 2001
From: Nikolaus Schlemm
Date: Sun, 19 May 2013 09:25:02 +0200
Subject: Integrated status quo of grimborg's awesome humanize_field() for
exposing field metadata via OPTIONS :)
---
rest_framework/fields.py | 2 +-
rest_framework/tests/generics.py | 36 ++++++++++++++++++------------------
rest_framework/views.py | 3 +--
3 files changed, 20 insertions(+), 21 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index d6db3ebe..a215e02b 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -136,7 +136,7 @@ def humanize_field(field):
humanized = {
'type': humanize_field_type(field.__class__),
'required': getattr(field, 'required', False),
- 'label': field.label,
+ 'label': getattr(field, 'label', None),
}
optional_attrs = ['read_only', 'help_text']
for attr in optional_attrs:
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index d8556638..a1edd28d 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -128,18 +128,18 @@ class TestRootView(TestCase):
for method in ('GET', 'POST',):
expected['actions'][method] = {
'text': {
- 'description': '',
- 'label': '',
- 'readonly': False,
+ #'description': '',
+ 'label': None,
+ 'read_only': False,
'required': True,
- 'type': 'CharField',
+ 'type': 'Single Character',
},
'id': {
- 'description': '',
- 'label': '',
- 'readonly': True,
- 'required': True,
- 'type': 'IntegerField',
+ #'description': '',
+ 'label': None,
+ 'read_only': True,
+ 'required': False,
+ 'type': 'Integer',
},
}
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -264,18 +264,18 @@ class TestInstanceView(TestCase):
for method in ('GET', 'PATCH', 'PUT', 'DELETE'):
expected['actions'][method] = {
'text': {
- 'description': '',
- 'label': '',
- 'readonly': False,
+ #'description': '',
+ 'label': None,
+ 'read_only': False,
'required': True,
- 'type': 'CharField',
+ 'type': 'Single Character',
},
'id': {
- 'description': '',
- 'label': '',
- 'readonly': True,
- 'required': True,
- 'type': 'IntegerField',
+ #'description': '',
+ 'label': None,
+ 'read_only': True,
+ 'required': False,
+ 'type': 'Integer',
},
}
self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 11d50e5d..e8bd9f50 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -85,8 +85,7 @@ class APIView(View):
field_name_types = {}
for name, field in serializer.fields.iteritems():
from rest_framework.fields import humanize_field
- humanize_field(field)
- field_name_types[name] = field.__class__.__name__
+ field_name_types[name] = humanize_field(field)
actions[method] = field_name_types
except exceptions.PermissionDenied:
--
cgit v1.2.3