From 83f0cb574db5fc706b24549ab90c6f548b1defab Mon Sep 17 00:00:00 2001 From: Darian Moody Date: Sun, 16 Feb 2014 22:15:31 +0000 Subject: [PATCH] Created a new serializer field `NullBooleanField` to fix #1422. This field is used as the default for Django's `NullBooleanField` in `ModelSerializer`s; it differs from `BooleanField` in that it is nullable and therefore allows `None` values. --- rest_framework/fields.py | 20 +++++ rest_framework/serializers.py | 2 +- rest_framework/tests/test_serializer.py | 99 +++++++++++++++---------- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 05daaab76..dd1186aad 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -453,6 +453,26 @@ class BooleanField(WritableField): return bool(value) +class NullBooleanField(WritableField): + type_name = 'NullBooleanField' + type_label = 'null boolean' + form_field_class = forms.NullBooleanField + widget = widgets.NullBooleanSelect + default_error_messages = { + 'invalid': _("'%s' value must be either None, True or False."), + } + empty = None + + def from_native(self, value): + if value is None: + return None + if value in ('true', 't', 'True', '1'): + return True + if value in ('false', 'f', 'False', '0'): + return False + return bool(value) + + class CharField(WritableField): type_name = 'CharField' type_label = 'string' diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 10256d479..2cea628d2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -648,7 +648,7 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, - models.NullBooleanField: BooleanField, + models.NullBooleanField: NullBooleanField, models.FileField: FileField, models.ImageField: ImageField, } diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c818200a4..4f9a5b9ca 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django import forms from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH from django.test import TestCase @@ -1847,19 +1848,12 @@ class BooleanFieldTypeTest(TestCase): bfield = self.serializer.get_fields()['done'] self.assertEqual(type(bfield), fields.BooleanField) - def test_nullbooleanfield_type(self): - ''' - Test that BooleanField is infered from models.NullBooleanField - - https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 - ''' - bfield = self.serializer.get_fields()['started'] - self.assertEqual(type(bfield), fields.BooleanField) - class NullBooleanFieldModel(models.Model): """ - A model for use in the NullBooleanFieldSerializer test. + A model for use in the NullBooleanFieldSerializer test. Showcases + all variations of the 'default' parameter (which is allowed to be + unset unlike models.BooleanField. """ cat = models.NullBooleanField(default=True) dog = models.NullBooleanField(default=False) @@ -1881,43 +1875,51 @@ class NullBooleanFieldSerializerTest(TestCase): fields = ('cat', 'dog', 'rat') self.serializer = NullBooleanFieldSerializer + # For ease we save a reference to the fields we need. + self.fields = self.serializer.Meta.fields def test_false(self): - data = { - 'cat': False, - 'dog': False, - 'rat': False - } - serializer = self.serializer(data=data) - self.assertEqual(serializer.is_valid(), True) - self.assertEqual(serializer.data['cat'], False) - self.assertEqual(serializer.data['dog'], False) - self.assertEqual(serializer.data['rat'], False) + """ + Check that all 'falsy' inputs result in the correct output + to the data dictionary across all our variations of NullBooleanField. + """ + falsy_values = ['false', 'f', 'False', '0', False, 0, ''] + for val in falsy_values: + # For each falsy value, we create a data dictionary where all + # fields are set to that falsy value and then check for the + # desired serializer output: False. + data = dict([(field, val) for field in self.fields]) + serializer = self.serializer(data=data) + self.assertEqual(serializer.is_valid(), True) + for field in self.fields: + self.assertEqual(serializer.data[field], False) def test_true(self): - data = { - 'cat': True, - 'dog': True, - 'rat': True - } - - serializer = self.serializer(data=data) - self.assertEqual(serializer.is_valid(), True) - self.assertEqual(serializer.data['cat'], True) - self.assertEqual(serializer.data['dog'], True) - self.assertEqual(serializer.data['rat'], True) + """ + Check that all 'truthy' inputs result in the correct output + to the data dictionary across all our variations of NullBooleanField. + """ + truthy_values = ['true', 't', 'True', '1', True, 1, 'truthy-string'] + for val in truthy_values: + # For each truthy value, we create a data dictionary where all + # fields are set to that falsy value and then check for the + # desired serializer output: True. + data = dict([(field, val) for field in self.fields]) + serializer = self.serializer(data=data) + self.assertEqual(serializer.is_valid(), True) + for field in self.fields: + self.assertEqual(serializer.data[field], True) def test_none(self): - data = { - 'cat': None, - 'dog': None, - 'rat': None - } + """ + Check that all 'None' input result in the correct output + to the data dictionary across all our variations of NullBooleanField. + """ + data = dict([(field, None) for field in self.fields]) serializer = self.serializer(data=data) self.assertEqual(serializer.is_valid(), True) - self.assertEqual(serializer.data['cat'], None) - self.assertEqual(serializer.data['dog'], None) - self.assertEqual(serializer.data['rat'], None) + for field in self.fields: + self.assertEqual(serializer.data[field], None) def test_partial(self): serializer = self.serializer(data={'rat': None}, partial=True) @@ -1925,3 +1927,22 @@ class NullBooleanFieldSerializerTest(TestCase): self.assertEqual(serializer.data['cat'], True) self.assertEqual(serializer.data['dog'], False) self.assertEqual(serializer.data['rat'], None) + + def test_nullbooleanfield_type(self): + """ + Test that DRF's NullBooleanField is selected by default for + Django's NullBooleanFields. + """ + serializer = self.serializer() + for field in self.fields: + bfield = serializer.get_fields()[field] + self.assertEqual(type(bfield), fields.NullBooleanField) + + def test_nullbooleanfield_widget_type(self): + """ + Test that Django's NullBooleanSelect widget is used as the default + widget for DRF's NullBooleanField. + """ + serializer = self.serializer() + for field_name, field in serializer.fields.iteritems(): + self.assertEqual(type(field.widget), forms.NullBooleanSelect)