Browse Source

Fixed #27582 -- Allowed HStoreField to store null values.

David Hoffman 8 years ago
parent
commit
bf84d042e0

+ 3 - 3
django/contrib/postgres/fields/hstore.py

@@ -13,9 +13,9 @@ __all__ = ['HStoreField']
 
 class HStoreField(Field):
     empty_strings_allowed = False
-    description = _('Map of strings to strings')
+    description = _('Map of strings to strings/nulls')
     default_error_messages = {
-        'not_a_string': _('The value of "%(key)s" is not a string.'),
+        'not_a_string': _('The value of "%(key)s" is not a string or null.'),
     }
 
     def db_type(self, connection):
@@ -30,7 +30,7 @@ class HStoreField(Field):
     def validate(self, value, model_instance):
         super(HStoreField, self).validate(value, model_instance)
         for key, val in value.items():
-            if not isinstance(val, six.string_types):
+            if not isinstance(val, six.string_types) and val is not None:
                 raise exceptions.ValidationError(
                     self.error_messages['not_a_string'],
                     code='not_a_string',

+ 3 - 1
django/contrib/postgres/forms/hstore.py

@@ -43,7 +43,9 @@ class HStoreField(forms.CharField):
 
         # Cast everything to strings for ease.
         for key, val in value.items():
-            value[key] = six.text_type(val)
+            if val is not None:
+                val = six.text_type(val)
+            value[key] = val
         return value
 
     def has_changed(self, initial, data):

+ 7 - 2
docs/ref/contrib/postgres/fields.txt

@@ -275,8 +275,9 @@ A more useful index is a ``GIN`` index, which you should create using a
 
 .. class:: HStoreField(**options)
 
-    A field for storing mappings of strings to strings. The Python data type
-    used is a ``dict``.
+    A field for storing key-value pairs. The Python data type used is a
+    ``dict``. Keys must be strings, and values may be either strings or nulls
+    (``None`` in Python).
 
     To use this field, you'll need to:
 
@@ -287,6 +288,10 @@ A more useful index is a ``GIN`` index, which you should create using a
     You'll see an error like ``can't adapt type 'dict'`` if you skip the first
     step, or ``type "hstore" does not exist`` if you skip the second.
 
+    .. versionchanged:: 1.11
+
+        Added the ability to store nulls. Previously, they were cast to strings.
+
 .. note::
 
     On occasions it may be useful to require or restrict the keys which are

+ 6 - 2
docs/ref/contrib/postgres/forms.txt

@@ -144,8 +144,8 @@ Fields
 .. class:: HStoreField
 
     A field which accepts JSON encoded data for an
-    :class:`~django.contrib.postgres.fields.HStoreField`. It will cast all the
-    values to strings. It is represented by an HTML ``<textarea>``.
+    :class:`~django.contrib.postgres.fields.HStoreField`. It casts all values
+    (except nulls) to strings. It is represented by an HTML ``<textarea>``.
 
     .. admonition:: User friendly forms
 
@@ -159,6 +159,10 @@ Fields
         valid for a given field. This can be done using the
         :class:`~django.contrib.postgres.validators.KeysValidator`.
 
+    .. versionchanged:: 1.11
+
+        Added the ability to store nulls.
+
 ``JSONField``
 -------------
 

+ 4 - 0
docs/releases/1.11.txt

@@ -194,6 +194,10 @@ Minor features
 * The new :class:`~django.contrib.postgres.aggregates.JSONBAgg` allows
   aggregating values as a JSON array.
 
+* :class:`~django.contrib.postgres.fields.HStoreField` and
+  :class:`~django.contrib.postgres.forms.HStoreField` allow storing null
+  values.
+
 :mod:`django.contrib.redirects`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 10 - 1
tests/postgres_tests/test_hstore.py

@@ -195,7 +195,11 @@ class TestValidation(HStoreTestCase):
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean({'a': 1}, None)
         self.assertEqual(cm.exception.code, 'not_a_string')
-        self.assertEqual(cm.exception.message % cm.exception.params, 'The value of "a" is not a string.')
+        self.assertEqual(cm.exception.message % cm.exception.params, 'The value of "a" is not a string or null.')
+
+    def test_none_allowed_as_value(self):
+        field = HStoreField()
+        self.assertEqual(field.clean({'a': None}, None), {'a': None})
 
 
 class TestFormField(HStoreTestCase):
@@ -224,6 +228,11 @@ class TestFormField(HStoreTestCase):
         value = field.clean('{"a": 1}')
         self.assertEqual(value, {'a': '1'})
 
+    def test_none_value(self):
+        field = forms.HStoreField()
+        value = field.clean('{"a": null}')
+        self.assertEqual(value, {'a': None})
+
     def test_empty(self):
         field = forms.HStoreField(required=False)
         value = field.clean('')