From 24b9f5082344a127147266dd52d5d2dcd1c9cb44 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Wed, 6 Nov 2019 18:22:15 +0800 Subject: [PATCH] Fixed #29916 -- Added lower_inc, lower_inf, upper_inc, and upper_inf lookups for RangeFields. Co-Authored-By: Mariusz Felisiak --- django/contrib/postgres/fields/ranges.py | 28 ++++++++++++ docs/ref/contrib/postgres/fields.txt | 56 ++++++++++++++++++++++++ docs/releases/3.1.txt | 4 ++ tests/postgres_tests/test_ranges.py | 24 ++++++++++ 4 files changed, 112 insertions(+) diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py index c0d985afa3..77a15632eb 100644 --- a/django/contrib/postgres/fields/ranges.py +++ b/django/contrib/postgres/fields/ranges.py @@ -284,3 +284,31 @@ class IsEmpty(models.Transform): lookup_name = 'isempty' function = 'isempty' output_field = models.BooleanField() + + +@RangeField.register_lookup +class LowerInclusive(models.Transform): + lookup_name = 'lower_inc' + function = 'LOWER_INC' + output_field = models.BooleanField() + + +@RangeField.register_lookup +class LowerInfinite(models.Transform): + lookup_name = 'lower_inf' + function = 'LOWER_INF' + output_field = models.BooleanField() + + +@RangeField.register_lookup +class UpperInclusive(models.Transform): + lookup_name = 'upper_inc' + function = 'UPPER_INC' + output_field = models.BooleanField() + + +@RangeField.register_lookup +class UpperInfinite(models.Transform): + lookup_name = 'upper_inf' + function = 'UPPER_INF' + output_field = models.BooleanField() diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 27cf6469f1..cd35a33ee6 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -864,6 +864,62 @@ Returned objects are empty ranges. Can be chained to valid lookups for a >>> Event.objects.filter(ages__isempty=True) +.. fieldlookup:: rangefield.lower_inc + +``lower_inc`` +^^^^^^^^^^^^^ + +.. versionadded:: 3.1 + +Returns objects that have inclusive or exclusive lower bounds, depending on the +boolean value passed. Can be chained to valid lookups for a +:class:`~django.db.models.BooleanField`. + + >>> Event.objects.filter(ages__lower_inc=True) + , ]> + +.. fieldlookup:: rangefield.lower_inf + +``lower_inf`` +^^^^^^^^^^^^^ + +.. versionadded:: 3.1 + +Returns objects that have unbounded (infinite) or bounded lower bound, +depending on the boolean value passed. Can be chained to valid lookups for a +:class:`~django.db.models.BooleanField`. + + >>> Event.objects.filter(ages__lower_inf=True) + + +.. fieldlookup:: rangefield.upper_inc + +``upper_inc`` +^^^^^^^^^^^^^ + +.. versionadded:: 3.1 + +Returns objects that have inclusive or exclusive upper bounds, depending on the +boolean value passed. Can be chained to valid lookups for a +:class:`~django.db.models.BooleanField`. + + >>> Event.objects.filter(ages__upper_inc=True) + + +.. fieldlookup:: rangefield.upper_inf + +``upper_inf`` +^^^^^^^^^^^^^ + +.. versionadded:: 3.1 + +Returns objects that have unbounded (infinite) or bounded upper bound, +depending on the boolean value passed. Can be chained to valid lookups for a +:class:`~django.db.models.BooleanField`. + + >>> Event.objects.filter(ages__upper_inf=True) + ]> + Defining your own range types ----------------------------- diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index f725e4e056..a7c9caa31e 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -80,6 +80,10 @@ Minor features :class:`~django.contrib.postgres.fields.ArrayField` and :class:`~django.contrib.postgres.fields.RangeField`. +* The new :lookup:`rangefield.lower_inc`, :lookup:`rangefield.lower_inf`, + :lookup:`rangefield.upper_inc`, and :lookup:`rangefield.upper_inf` allows + querying :class:`~django.contrib.postgres.fields.RangeField` by a bound type. + :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py index 326015e46d..789ff3d546 100644 --- a/tests/postgres_tests/test_ranges.py +++ b/tests/postgres_tests/test_ranges.py @@ -296,6 +296,30 @@ class TestQuerying(PostgreSQLTestCase): [self.objs[0], self.objs[1]], ) + def test_bound_type(self): + decimals = RangesModel.objects.bulk_create([ + RangesModel(decimals=NumericRange(None, 10)), + RangesModel(decimals=NumericRange(10, None)), + RangesModel(decimals=NumericRange(5, 15)), + RangesModel(decimals=NumericRange(5, 15, '(]')), + ]) + tests = [ + ('lower_inc', True, [decimals[1], decimals[2]]), + ('lower_inc', False, [decimals[0], decimals[3]]), + ('lower_inf', True, [decimals[0]]), + ('lower_inf', False, [decimals[1], decimals[2], decimals[3]]), + ('upper_inc', True, [decimals[3]]), + ('upper_inc', False, [decimals[0], decimals[1], decimals[2]]), + ('upper_inf', True, [decimals[1]]), + ('upper_inf', False, [decimals[0], decimals[2], decimals[3]]), + ] + for lookup, filter_arg, excepted_result in tests: + with self.subTest(lookup=lookup, filter_arg=filter_arg): + self.assertSequenceEqual( + RangesModel.objects.filter(**{'decimals__%s' % lookup: filter_arg}), + excepted_result, + ) + class TestQueryingWithRanges(PostgreSQLTestCase): def test_date_range(self):