import datetime import json from django import forms from django.core import exceptions, serializers from django.db.models import DateField, DateTimeField, F, Func, Value from django.test import override_settings from django.utils import timezone from . import PostgreSQLTestCase from .models import RangeLookupsModel, RangesModel try: from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange from django.contrib.postgres import fields as pg_fields, forms as pg_forms from django.contrib.postgres.validators import ( RangeMaxValueValidator, RangeMinValueValidator, ) except ImportError: pass class TestSaveLoad(PostgreSQLTestCase): def test_all_fields(self): now = timezone.now() instance = RangesModel( ints=NumericRange(0, 10), bigints=NumericRange(10, 20), floats=NumericRange(20, 30), timestamps=DateTimeTZRange(now - datetime.timedelta(hours=1), now), dates=DateRange(now.date() - datetime.timedelta(days=1), now.date()), ) instance.save() loaded = RangesModel.objects.get() self.assertEqual(instance.ints, loaded.ints) self.assertEqual(instance.bigints, loaded.bigints) self.assertEqual(instance.floats, loaded.floats) self.assertEqual(instance.timestamps, loaded.timestamps) self.assertEqual(instance.dates, loaded.dates) def test_range_object(self): r = NumericRange(0, 10) instance = RangesModel(ints=r) instance.save() loaded = RangesModel.objects.get() self.assertEqual(r, loaded.ints) def test_tuple(self): instance = RangesModel(ints=(0, 10)) instance.save() loaded = RangesModel.objects.get() self.assertEqual(NumericRange(0, 10), loaded.ints) def test_range_object_boundaries(self): r = NumericRange(0, 10, '[]') instance = RangesModel(floats=r) instance.save() loaded = RangesModel.objects.get() self.assertEqual(r, loaded.floats) self.assertIn(10, loaded.floats) def test_unbounded(self): r = NumericRange(None, None, '()') instance = RangesModel(floats=r) instance.save() loaded = RangesModel.objects.get() self.assertEqual(r, loaded.floats) def test_empty(self): r = NumericRange(empty=True) instance = RangesModel(ints=r) instance.save() loaded = RangesModel.objects.get() self.assertEqual(r, loaded.ints) def test_null(self): instance = RangesModel(ints=None) instance.save() loaded = RangesModel.objects.get() self.assertIsNone(loaded.ints) def test_model_set_on_base_field(self): instance = RangesModel() field = instance._meta.get_field('ints') self.assertEqual(field.model, RangesModel) self.assertEqual(field.base_field.model, RangesModel) class TestRangeContainsLookup(PostgreSQLTestCase): @classmethod def setUpTestData(cls): cls.timestamps = [ datetime.datetime(year=2016, month=1, day=1), datetime.datetime(year=2016, month=1, day=2, hour=1), datetime.datetime(year=2016, month=1, day=2, hour=12), datetime.datetime(year=2016, month=1, day=3), datetime.datetime(year=2016, month=1, day=3, hour=1), datetime.datetime(year=2016, month=2, day=2), ] cls.aware_timestamps = [ timezone.make_aware(timestamp, timezone.get_current_timezone()) for timestamp in cls.timestamps ] cls.dates = [ datetime.date(year=2016, month=1, day=1), datetime.date(year=2016, month=1, day=2), datetime.date(year=2016, month=1, day=3), datetime.date(year=2016, month=1, day=4), datetime.date(year=2016, month=2, day=2), datetime.date(year=2016, month=2, day=3), ] cls.obj = RangesModel.objects.create( dates=(cls.dates[0], cls.dates[3]), timestamps=(cls.timestamps[0], cls.timestamps[3]), ) cls.aware_obj = RangesModel.objects.create( dates=(cls.dates[0], cls.dates[3]), timestamps=(cls.aware_timestamps[0], cls.aware_timestamps[3]), ) # Objects that don't match any queries. for i in range(3, 4): RangesModel.objects.create( dates=(cls.dates[i], cls.dates[i + 1]), timestamps=(cls.timestamps[i], cls.timestamps[i + 1]), ) RangesModel.objects.create( dates=(cls.dates[i], cls.dates[i + 1]), timestamps=(cls.aware_timestamps[i], cls.aware_timestamps[i + 1]), ) def test_datetime_range_contains(self): filter_args = ( self.timestamps[1], self.aware_timestamps[1], (self.timestamps[1], self.timestamps[2]), (self.aware_timestamps[1], self.aware_timestamps[2]), Value(self.dates[0], output_field=DateTimeField()), Func(F('dates'), function='lower', output_field=DateTimeField()), ) for filter_arg in filter_args: with self.subTest(filter_arg=filter_arg): self.assertCountEqual( RangesModel.objects.filter(**{'timestamps__contains': filter_arg}), [self.obj, self.aware_obj], ) def test_date_range_contains(self): filter_args = ( self.timestamps[1], (self.dates[1], self.dates[2]), Value(self.dates[0], output_field=DateField()), Func(F('timestamps'), function='lower', output_field=DateField()), ) for filter_arg in filter_args: with self.subTest(filter_arg=filter_arg): self.assertCountEqual( RangesModel.objects.filter(**{'dates__contains': filter_arg}), [self.obj, self.aware_obj], ) class TestQuerying(PostgreSQLTestCase): @classmethod def setUpTestData(cls): cls.objs = [ RangesModel.objects.create(ints=NumericRange(0, 10)), RangesModel.objects.create(ints=NumericRange(5, 15)), RangesModel.objects.create(ints=NumericRange(None, 0)), RangesModel.objects.create(ints=NumericRange(empty=True)), RangesModel.objects.create(ints=None), ] def test_exact(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__exact=NumericRange(0, 10)), [self.objs[0]], ) def test_isnull(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__isnull=True), [self.objs[4]], ) def test_isempty(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__isempty=True), [self.objs[3]], ) def test_contains(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__contains=8), [self.objs[0], self.objs[1]], ) def test_contains_range(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__contains=NumericRange(3, 8)), [self.objs[0]], ) def test_contained_by(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__contained_by=NumericRange(0, 20)), [self.objs[0], self.objs[1], self.objs[3]], ) def test_overlap(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__overlap=NumericRange(3, 8)), [self.objs[0], self.objs[1]], ) def test_fully_lt(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__fully_lt=NumericRange(5, 10)), [self.objs[2]], ) def test_fully_gt(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__fully_gt=NumericRange(5, 10)), [], ) def test_not_lt(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__not_lt=NumericRange(5, 10)), [self.objs[1]], ) def test_not_gt(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__not_gt=NumericRange(5, 10)), [self.objs[0], self.objs[2]], ) def test_adjacent_to(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__adjacent_to=NumericRange(0, 5)), [self.objs[1], self.objs[2]], ) def test_startswith(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__startswith=0), [self.objs[0]], ) def test_endswith(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__endswith=0), [self.objs[2]], ) def test_startswith_chaining(self): self.assertSequenceEqual( RangesModel.objects.filter(ints__startswith__gte=0), [self.objs[0], self.objs[1]], ) class TestQueryingWithRanges(PostgreSQLTestCase): def test_date_range(self): objs = [ RangeLookupsModel.objects.create(date='2015-01-01'), RangeLookupsModel.objects.create(date='2015-05-05'), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(date__contained_by=DateRange('2015-01-01', '2015-05-04')), [objs[0]], ) def test_date_range_datetime_field(self): objs = [ RangeLookupsModel.objects.create(timestamp='2015-01-01'), RangeLookupsModel.objects.create(timestamp='2015-05-05'), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(timestamp__date__contained_by=DateRange('2015-01-01', '2015-05-04')), [objs[0]], ) def test_datetime_range(self): objs = [ RangeLookupsModel.objects.create(timestamp='2015-01-01T09:00:00'), RangeLookupsModel.objects.create(timestamp='2015-05-05T17:00:00'), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter( timestamp__contained_by=DateTimeTZRange('2015-01-01T09:00', '2015-05-04T23:55') ), [objs[0]], ) def test_integer_range(self): objs = [ RangeLookupsModel.objects.create(integer=5), RangeLookupsModel.objects.create(integer=99), RangeLookupsModel.objects.create(integer=-1), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(integer__contained_by=NumericRange(1, 98)), [objs[0]] ) def test_biginteger_range(self): objs = [ RangeLookupsModel.objects.create(big_integer=5), RangeLookupsModel.objects.create(big_integer=99), RangeLookupsModel.objects.create(big_integer=-1), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(big_integer__contained_by=NumericRange(1, 98)), [objs[0]] ) def test_float_range(self): objs = [ RangeLookupsModel.objects.create(float=5), RangeLookupsModel.objects.create(float=99), RangeLookupsModel.objects.create(float=-1), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(float__contained_by=NumericRange(1, 98)), [objs[0]] ) def test_f_ranges(self): parent = RangesModel.objects.create(floats=NumericRange(0, 10)) objs = [ RangeLookupsModel.objects.create(float=5, parent=parent), RangeLookupsModel.objects.create(float=99, parent=parent), ] self.assertSequenceEqual( RangeLookupsModel.objects.filter(float__contained_by=F('parent__floats')), [objs[0]] ) def test_exclude(self): objs = [ RangeLookupsModel.objects.create(float=5), RangeLookupsModel.objects.create(float=99), RangeLookupsModel.objects.create(float=-1), ] self.assertSequenceEqual( RangeLookupsModel.objects.exclude(float__contained_by=NumericRange(0, 100)), [objs[2]] ) class TestSerialization(PostgreSQLTestCase): test_data = ( '[{"fields": {"ints": "{\\"upper\\": \\"10\\", \\"lower\\": \\"0\\", ' '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", ' '"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, 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), 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) for field in ('ints', 'dates', 'timestamps'): dumped[0]['fields'][field] = json.loads(dumped[0]['fields'][field]) check = json.loads(self.test_data) 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.assertIsNone(instance.bigints) self.assertEqual(instance.dates, DateRange(self.lower_date, self.upper_date)) self.assertEqual(instance.timestamps, DateTimeTZRange(self.lower_dt, self.upper_dt)) def test_serialize_range_with_null(self): instance = RangesModel(ints=NumericRange(None, 10)) data = serializers.serialize('json', [instance]) new_instance = list(serializers.deserialize('json', data))[0].object self.assertEqual(new_instance.ints, NumericRange(None, 10)) instance = RangesModel(ints=NumericRange(10, None)) data = serializers.serialize('json', [instance]) new_instance = list(serializers.deserialize('json', data))[0].object self.assertEqual(new_instance.ints, NumericRange(10, None)) class TestValidators(PostgreSQLTestCase): def test_max(self): validator = RangeMaxValueValidator(5) validator(NumericRange(0, 5)) msg = 'Ensure that this range is completely less than or equal to 5.' with self.assertRaises(exceptions.ValidationError) as cm: validator(NumericRange(0, 10)) self.assertEqual(cm.exception.messages[0], msg) self.assertEqual(cm.exception.code, 'max_value') with self.assertRaisesMessage(exceptions.ValidationError, msg): validator(NumericRange(0, None)) # an unbound range def test_min(self): validator = RangeMinValueValidator(5) validator(NumericRange(10, 15)) msg = 'Ensure that this range is completely greater than or equal to 5.' with self.assertRaises(exceptions.ValidationError) as cm: validator(NumericRange(0, 10)) self.assertEqual(cm.exception.messages[0], msg) self.assertEqual(cm.exception.code, 'min_value') with self.assertRaisesMessage(exceptions.ValidationError, msg): validator(NumericRange(None, 10)) # an unbound range class TestFormField(PostgreSQLTestCase): def test_valid_integer(self): field = pg_forms.IntegerRangeField() value = field.clean(['1', '2']) self.assertEqual(value, NumericRange(1, 2)) def test_valid_floats(self): field = pg_forms.FloatRangeField() value = field.clean(['1.12345', '2.001']) self.assertEqual(value, NumericRange(1.12345, 2.001)) def test_valid_timestamps(self): field = pg_forms.DateTimeRangeField() value = field.clean(['01/01/2014 00:00:00', '02/02/2014 12:12:12']) lower = datetime.datetime(2014, 1, 1, 0, 0, 0) upper = datetime.datetime(2014, 2, 2, 12, 12, 12) self.assertEqual(value, DateTimeTZRange(lower, upper)) def test_valid_dates(self): field = pg_forms.DateRangeField() value = field.clean(['01/01/2014', '02/02/2014']) lower = datetime.date(2014, 1, 1) upper = datetime.date(2014, 2, 2) self.assertEqual(value, DateRange(lower, upper)) def test_using_split_datetime_widget(self): class SplitDateTimeRangeField(pg_forms.DateTimeRangeField): base_field = forms.SplitDateTimeField class SplitForm(forms.Form): field = SplitDateTimeRangeField() form = SplitForm() self.assertHTMLEqual(str(form), '''