From 2926559cce34e48efb4b073721926d737e372dd3 Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Fri, 5 Jun 2015 21:56:00 +0100 Subject: [PATCH 1/3] Fixed #24937 -- fix serialization of Date(Time)RangeField. Use the DjangoJSONEncoder so that datetime and date are encoded appropriately. --- django/contrib/postgres/fields/ranges.py | 3 ++- docs/releases/1.9.txt | 4 ++++ tests/postgres_tests/test_ranges.py | 21 ++++++++++++++++----- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py index 6e0f8e2284..81c04eab92 100644 --- a/django/contrib/postgres/fields/ranges.py +++ b/django/contrib/postgres/fields/ranges.py @@ -3,6 +3,7 @@ import json from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange, Range from django.contrib.postgres import forms, lookups +from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.utils import six @@ -41,7 +42,7 @@ class RangeField(models.Field): "lower": value.lower, "upper": value.upper, "bounds": value._bounds, - }) + }, cls=DjangoJSONEncoder) def formfield(self, **kwargs): kwargs.setdefault('form_class', self.form_field) diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 793dcd6a82..c6467d9f65 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -102,6 +102,10 @@ Minor features * Added :class:`~django.contrib.postgres.fields.JSONField`. * Added :doc:`/ref/contrib/postgres/aggregates`. +* Fixed serialization of + :class:`~django.contrib.postgres.fields.DateRangeField` and + :class:`~django.contrib.postgres.fields.DateTimeRangeField`. + :mod:`django.contrib.redirects` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py index 2461130b35..eca22c17f5 100644 --- a/tests/postgres_tests/test_ranges.py +++ b/tests/postgres_tests/test_ranges.py @@ -293,24 +293,35 @@ class TestSerialization(TestCase): test_data = ( '[{"fields": {"ints": "{\\"upper\\": 10, \\"lower\\": 0, ' '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", ' - '"bigints": null, "timestamps": null, "dates": null}, ' + '"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12\\", ' + '\\"lower\\": \\"2014-01-01T00:00:00\\", \\"bounds\\": \\"[)\\"}", ' + '"dates": "{\\"upper\\": \\"2014-02-02\\", \\"lower\\": \\"2014-01-01\\", \\"bounds\\": \\"[)\\"}" }, ' '"model": "postgres_tests.rangesmodel", "pk": null}]' ) + lower_date = datetime.date(2014, 1, 1) + upper_date = datetime.date(2014, 2, 2) + lower_dt = datetime.datetime(2014, 1, 1, 0, 0, 0) + upper_dt = datetime.datetime(2014, 2, 2, 12, 12, 12) + def test_dumping(self): - instance = RangesModel(ints=NumericRange(0, 10), floats=NumericRange(empty=True)) + instance = RangesModel(ints=NumericRange(0, 10), floats=NumericRange(empty=True), + timestamps=DateTimeTZRange(self.lower_dt, self.upper_dt), + dates=DateRange(self.lower_date, self.upper_date)) data = serializers.serialize('json', [instance]) dumped = json.loads(data) - dumped[0]['fields']['ints'] = json.loads(dumped[0]['fields']['ints']) + for field in ('ints', 'dates', 'timestamps'): + dumped[0]['fields'][field] = json.loads(dumped[0]['fields'][field]) check = json.loads(self.test_data) - check[0]['fields']['ints'] = json.loads(check[0]['fields']['ints']) + for field in ('ints', 'dates', 'timestamps'): + check[0]['fields'][field] = json.loads(check[0]['fields'][field]) self.assertEqual(dumped, check) def test_loading(self): instance = list(serializers.deserialize('json', self.test_data))[0].object self.assertEqual(instance.ints, NumericRange(0, 10)) self.assertEqual(instance.floats, NumericRange(empty=True)) - self.assertEqual(instance.dates, None) + self.assertEqual(instance.bigints, None) class TestValidators(PostgreSQLTestCase): From 86d9b10dc33cc115fee2ecab40a569354ac55d15 Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Sat, 6 Jun 2015 12:55:04 +0100 Subject: [PATCH 2/3] Instead of using DjangoJSONEncoder, use base_field's value_to_string. Note this means the serialization of e.g. IntegerRangeField now has strings for lower and upper, so use to_python when they came back in (same behaviour as ArrayField, hopefully, from where I also got the set_attributes_from_name function). --- django/contrib/postgres/fields/array.py | 7 ++----- django/contrib/postgres/fields/ranges.py | 25 +++++++++++++++++------- django/contrib/postgres/fields/utils.py | 3 +++ tests/postgres_tests/test_ranges.py | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 django/contrib/postgres/fields/utils.py diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index 9da7ec4cb7..f9242f086b 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -8,14 +8,11 @@ from django.db.models import Field, IntegerField, Transform from django.utils import six from django.utils.translation import string_concat, ugettext_lazy as _ +from .utils import AttributeSetter + __all__ = ['ArrayField'] -class AttributeSetter(object): - def __init__(self, name, value): - setattr(self, name, value) - - class ArrayField(Field): empty_strings_allowed = False default_error_messages = { diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py index 81c04eab92..13ba606c79 100644 --- a/django/contrib/postgres/fields/ranges.py +++ b/django/contrib/postgres/fields/ranges.py @@ -3,10 +3,11 @@ import json from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange, Range from django.contrib.postgres import forms, lookups -from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.utils import six +from .utils import AttributeSetter + __all__ = [ 'RangeField', 'IntegerRangeField', 'BigIntegerRangeField', 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField', @@ -27,22 +28,32 @@ class RangeField(models.Field): def to_python(self, value): if isinstance(value, six.string_types): - value = self.range_type(**json.loads(value)) + # Assume we're deserializing + vals = json.loads(value) + for end in ('lower', 'upper'): + if end in vals: + vals[end] = self.base_field.to_python(vals[end]) + value = self.range_type(**vals) elif isinstance(value, (list, tuple)): value = self.range_type(value[0], value[1]) return value + def set_attributes_from_name(self, name): + super(RangeField, self).set_attributes_from_name(name) + self.base_field.set_attributes_from_name(name) + def value_to_string(self, obj): value = self._get_val_from_obj(obj) if value is None: return None if value.isempty: return json.dumps({"empty": True}) - return json.dumps({ - "lower": value.lower, - "upper": value.upper, - "bounds": value._bounds, - }, cls=DjangoJSONEncoder) + base_field = self.base_field + result = {"bounds": value._bounds} + for end in ('lower', 'upper'): + obj = AttributeSetter(base_field.attname, getattr(value, end)) + result[end] = base_field.value_to_string(obj) + return json.dumps(result) def formfield(self, **kwargs): kwargs.setdefault('form_class', self.form_field) diff --git a/django/contrib/postgres/fields/utils.py b/django/contrib/postgres/fields/utils.py new file mode 100644 index 0000000000..424a78f521 --- /dev/null +++ b/django/contrib/postgres/fields/utils.py @@ -0,0 +1,3 @@ +class AttributeSetter(object): + def __init__(self, name, value): + setattr(self, name, value) diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py index eca22c17f5..b3b591f4e4 100644 --- a/tests/postgres_tests/test_ranges.py +++ b/tests/postgres_tests/test_ranges.py @@ -291,7 +291,7 @@ class TestQueringWithRanges(TestCase): @skipUnlessPG92 class TestSerialization(TestCase): test_data = ( - '[{"fields": {"ints": "{\\"upper\\": 10, \\"lower\\": 0, ' + '[{"fields": {"ints": "{\\"upper\\": \\"10\\", \\"lower\\": \\"0\\", ' '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", ' '"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12\\", ' '\\"lower\\": \\"2014-01-01T00:00:00\\", \\"bounds\\": \\"[)\\"}", ' From 8a842148b6deaab021526e2689279cf5e232945f Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Sat, 6 Jun 2015 13:05:29 +0100 Subject: [PATCH 3/3] Switch to aware datetimes in test. --- tests/postgres_tests/test_ranges.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py index b3b591f4e4..7d7fb33c4d 100644 --- a/tests/postgres_tests/test_ranges.py +++ b/tests/postgres_tests/test_ranges.py @@ -293,16 +293,16 @@ class TestSerialization(TestCase): test_data = ( '[{"fields": {"ints": "{\\"upper\\": \\"10\\", \\"lower\\": \\"0\\", ' '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", ' - '"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12\\", ' - '\\"lower\\": \\"2014-01-01T00:00:00\\", \\"bounds\\": \\"[)\\"}", ' + '"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12+00:00\\", ' + '\\"lower\\": \\"2014-01-01T00:00:00+00:00\\", \\"bounds\\": \\"[)\\"}", ' '"dates": "{\\"upper\\": \\"2014-02-02\\", \\"lower\\": \\"2014-01-01\\", \\"bounds\\": \\"[)\\"}" }, ' '"model": "postgres_tests.rangesmodel", "pk": null}]' ) lower_date = datetime.date(2014, 1, 1) upper_date = datetime.date(2014, 2, 2) - lower_dt = datetime.datetime(2014, 1, 1, 0, 0, 0) - upper_dt = datetime.datetime(2014, 2, 2, 12, 12, 12) + lower_dt = datetime.datetime(2014, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + upper_dt = datetime.datetime(2014, 2, 2, 12, 12, 12, tzinfo=timezone.utc) def test_dumping(self): instance = RangesModel(ints=NumericRange(0, 10), floats=NumericRange(empty=True),