From 643d3491a65237fef6932ef8833472c243ad7ee8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 23:48:52 +0100 Subject: [PATCH 1/6] First pass at pastebin tutorial --- rest_framework/fields.py | 80 ++++++++++++++++++++++++++++++++----- rest_framework/renderers.py | 13 +++++- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bb9a523d6..14422b279 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -7,6 +7,7 @@ from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve from django.conf import settings +from django.forms import widgets from django.utils.encoding import is_protected_type, smart_unicode from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse @@ -105,10 +106,14 @@ class WritableField(Field): 'required': _('This field is required.'), 'invalid': _('Invalid value.'), } + widget = widgets.TextInput def __init__(self, source=None, readonly=False, required=None, - validators=[], error_messages=None): + validators=[], error_messages=None, widget=None, + help_text=None, initial=None): + super(WritableField, self).__init__(source=source) + self.readonly = readonly if required is None: self.required = not(readonly) @@ -124,6 +129,15 @@ class WritableField(Field): self.validators = self.default_validators + validators + # These attributes are ony used for HTML forms. + self.initial = initial + self.help_text = help_text and smart_unicode(help_text) or '' + + widget = widget or self.widget + if isinstance(widget, type): + widget = widget() + self.widget = widget + def validate(self, value): if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) @@ -157,9 +171,12 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if self.required: - raise ValidationError(self.error_messages['required']) - return + if getattr(self, 'missing_value', None) is not None: + native = self.missing_value + else: + if self.required: + raise ValidationError(self.error_messages['required']) + return value = self.from_native(native) if self.source == '*': @@ -394,20 +411,19 @@ class HyperlinkedIdentityField(Field): class BooleanField(WritableField): type_name = 'BooleanField' + widget = widgets.CheckboxInput default_error_messages = { 'invalid': _(u"'%s' value must be either True or False."), } + empty = False + missing_value = False # Fill in missing value not supplied by html form def from_native(self, value): - if value in (True, False): - # if value is 1 or 0 than it's equal to True or False, but we want - # to return a true bool for semantic reasons. - return bool(value) if value in ('t', 'True', '1'): return True if value in ('f', 'False', '0'): return False - raise ValidationError(self.error_messages['invalid'] % value) + return bool(value) class CharField(WritableField): @@ -427,6 +443,52 @@ class CharField(WritableField): return smart_unicode(value) +class ChoiceField(WritableField): + type_name = 'ChoiceField' + widget = widgets.Select + default_error_messages = { + 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), + } + + def __init__(self, choices=(), *args, **kwargs): + super(ChoiceField, self).__init__(*args, **kwargs) + self.choices = choices + + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def validate(self, value): + """ + Validates that the input is in self.choices. + """ + super(ChoiceField, self).validate(value) + if value and not self.valid_value(value): + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + + def valid_value(self, value): + """ + Check to see if the provided value is a valid choice. + """ + for k, v in self.choices: + if isinstance(v, (list, tuple)): + # This is an optgroup, so look inside the group for options + for k2, v2 in v: + if value == smart_unicode(k2): + return True + else: + if value == smart_unicode(k): + return True + return False + + class EmailField(CharField): type_name = 'EmailField' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b7..936bec367 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -279,13 +279,22 @@ class BrowsableAPIRenderer(BaseRenderer): continue kwargs = {} + kwargs['required'] = v.required if getattr(v, 'queryset', None): - kwargs['queryset'] = getattr(v, 'queryset', None) + kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): + kwargs['widget'] = v.widget + if getattr(v, 'initial', None): + kwargs['initial'] = v.initial + if getattr(v, 'help_text', None): + kwargs['help_text'] = v.help_text + kwargs['label'] = k + print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: - fields[k] = forms.CharField() + fields[k] = forms.CharField(**kwargs) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted From dab177e29e45b657bb43705979c8e601d5a1b31b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:20:54 +0100 Subject: [PATCH 2/6] Drop help_text --- rest_framework/fields.py | 3 +-- rest_framework/renderers.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 14422b279..0990eadc9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -110,7 +110,7 @@ class WritableField(Field): def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - help_text=None, initial=None): + initial=None): super(WritableField, self).__init__(source=source) @@ -131,7 +131,6 @@ class WritableField(Field): # These attributes are ony used for HTML forms. self.initial = initial - self.help_text = help_text and smart_unicode(help_text) or '' widget = widget or self.widget if isinstance(widget, type): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 936bec367..de2276ad7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -286,8 +286,6 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs['widget'] = v.widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial - if getattr(v, 'help_text', None): - kwargs['help_text'] = v.help_text kwargs['label'] = k print kwargs From a7390fe7044c4f10d15fdbe9de9e594d0ffa2e05 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:47:01 +0100 Subject: [PATCH 3/6] Fix up widget choices --- rest_framework/renderers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index de2276ad7..ba5489bcb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,6 +6,7 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer the renders the browseable API. """ +import copy import string from django import forms from django.http.multipartparser import parse_header @@ -283,11 +284,19 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset if getattr(v, 'widget', None): - kwargs['widget'] = v.widget + widget = copy.deepcopy(v.widget) + # If choices have friendly readable names, + # then add in the identities too + if getattr(widget, 'choices', None): + choices = widget.choices + if any([ident != desc for (ident, desc) in choices]): + choices = [(ident, "%s (%s)" % (desc, ident)) + for (ident, desc) in choices] + widget.choices = choices + kwargs['widget'] = widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial kwargs['label'] = k - print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) From 13d0a829390105aa53602be7dc713092ead5a66c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:40:49 +0100 Subject: [PATCH 4/6] Minor docs tweaks --- docs/api-guide/fields.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index dc9ab0452..234c65ad0 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -73,34 +73,52 @@ These fields represent basic datatypes, and support both reading and writing val ## BooleanField -A Boolean representation, corresponds to `django.db.models.fields.BooleanField`. +A Boolean representation. + +Corresponds to `django.db.models.fields.BooleanField`. ## CharField -A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`, corresponds to `django.db.models.fields.CharField` +A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`. + +Corresponds to `django.db.models.fields.CharField` or `django.db.models.fields.TextField`. -**Signature:** `CharField([max_length=[, min_length=]])` +**Signature:** `CharField(max_length=None, min_length=None)` + +## ChoiceField + +A field that can accept on of a limited set of choices. ## EmailField -A text representation, validates the text to be a valid e-mail adress. Corresponds to `django.db.models.fields.EmailField` +A text representation, validates the text to be a valid e-mail adress. + +Corresponds to `django.db.models.fields.EmailField` ## DateField -A date representation. Corresponds to `django.db.models.fields.DateField` +A date representation. + +Corresponds to `django.db.models.fields.DateField` ## DateTimeField -A date and time representation. Corresponds to `django.db.models.fields.DateTimeField` +A date and time representation. + +Corresponds to `django.db.models.fields.DateTimeField` ## IntegerField -An integer representation. Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField` +An integer representation. + +Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField` ## FloatField -A floating point representation. Corresponds to `django.db.models.fields.FloatField`. +A floating point representation. + +Corresponds to `django.db.models.fields.FloatField`. --- From 93f1aa4f69df85add114c9730a01b50d013a844a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:41:05 +0100 Subject: [PATCH 5/6] Remove `initial` kwarg, add `default`. --- rest_framework/fields.py | 18 +++++++++++------- rest_framework/renderers.py | 8 ++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 0990eadc9..299409468 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -107,10 +107,11 @@ class WritableField(Field): 'invalid': _('Invalid value.'), } widget = widgets.TextInput + default = None def __init__(self, source=None, readonly=False, required=None, validators=[], error_messages=None, widget=None, - initial=None): + default=None): super(WritableField, self).__init__(source=source) @@ -128,10 +129,9 @@ class WritableField(Field): self.error_messages = messages self.validators = self.default_validators + validators + self.default = default or self.default - # These attributes are ony used for HTML forms. - self.initial = initial - + # Widgets are ony used for HTML forms. widget = widget or self.widget if isinstance(widget, type): widget = widget() @@ -170,8 +170,8 @@ class WritableField(Field): try: native = data[field_name] except KeyError: - if getattr(self, 'missing_value', None) is not None: - native = self.missing_value + if self.default is not None: + native = self.default else: if self.required: raise ValidationError(self.error_messages['required']) @@ -415,7 +415,11 @@ class BooleanField(WritableField): 'invalid': _(u"'%s' value must be either True or False."), } empty = False - missing_value = False # Fill in missing value not supplied by html form + + # Note: we set default to `False` in order to fill in missing value not + # supplied by html form. TODO: Fix so that only html form input gets + # this behavior. + default = False def from_native(self, value): if value in ('t', 'True', '1'): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba5489bcb..b2dbffd27 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -281,8 +281,10 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs = {} kwargs['required'] = v.required + if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): widget = copy.deepcopy(v.widget) # If choices have friendly readable names, @@ -294,8 +296,10 @@ class BrowsableAPIRenderer(BaseRenderer): for (ident, desc) in choices] widget.choices = choices kwargs['widget'] = widget - if getattr(v, 'initial', None): - kwargs['initial'] = v.initial + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + kwargs['label'] = k try: From 45d4622f090f8d81a04b4d3e888017419676bbc0 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 22 Oct 2012 15:12:25 +0100 Subject: [PATCH 6/6] Fix serialization of reverse relationships --- rest_framework/serializers.py | 23 +++++++++++++---------- rest_framework/tests/models.py | 11 +++++++++++ rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6724bbdf7..221cbf2fc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -247,6 +247,19 @@ class BaseSerializer(Field): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def field_to_native(self, obj, field_name): + """ + Override default so that we can apply ModelSerializer as a nested + field to relationships. + """ + obj = getattr(obj, self.source or field_name) + + # If the object has an "all" method, assume it's a relationship + if is_simple_callable(getattr(obj, 'all', None)): + return [self.to_native(item) for item in obj.all()] + + return self.to_native(obj) + @property def errors(self): """ @@ -295,16 +308,6 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - def field_to_native(self, obj, field_name): - """ - Override default so that we can apply ModelSerializer as a nested - field to relationships. - """ - obj = getattr(obj, self.source or field_name) - if obj.__class__.__name__ in ('RelatedManager', 'ManyRelatedManager'): - return [self.to_native(item) for item in obj.all()] - return self.to_native(obj) - def default_fields(self, serialize, obj=None, data=None, nested=False): """ Return all the fields that should be serialized for the model. diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 75dab2f7d..8e7217373 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -92,6 +92,17 @@ class Comment(RESTFrameworkModel): content = models.CharField(max_length=200) created = models.DateTimeField(auto_now_add=True) + class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) done = models.BooleanField(default=False) + + +# Models for reverse relations +class BlogPost(RESTFrameworkModel): + title = models.CharField(max_length=100) + + +class BlogPostComment(RESTFrameworkModel): + text = models.TextField() + blog_post = models.ForeignKey(BlogPost) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index bd1f07da8..2dfc04e15 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -302,3 +302,32 @@ class CallableDefaultValueTests(TestCase): self.assertEquals(len(self.objects.all()), 1) self.assertEquals(instance.pk, 1) self.assertEquals(instance.text, 'overridden') + + +class ManyRelatedTests(TestCase): + def setUp(self): + + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + comments = BlogPostCommentSerializer(source='blogpostcomment_set') + + self.serializer_class = BlogPostSerializer + + def test_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + serializer = self.serializer_class(instance=post) + expected = { + 'title': 'Test blog post', + 'comments': [ + {'text': 'I hate this blog post'}, + {'text': 'I love this blog post'} + ] + } + + self.assertEqual(serializer.data, expected)