﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
28758	Fix range min/max validators to prevent errors for infinite ranges	myii	Tim Graham <timograham@…>	"== References

1. [=#point01] https://www.postgresql.org/docs/current/static/rangetypes.html#rangetypes-infinite

> === 8.17.4. Infinite (Unbounded) Ranges
> 
> The lower bound of a range can be omitted, meaning that all points less than the upper bound are included in the range. Likewise, if the upper bound of the range is omitted, then all points greater than the lower bound are included in the range.

2. [=#point02] https://docs.djangoproject.com/en/dev/ref/contrib/postgres/fields/#range-fields

> All of the range fields translate to [http://initd.org/psycopg/docs/extras.html#adapt-range psycopg2 Range objects] in python, ...

3. [=#point03] http://initd.org/psycopg/docs/extras.html#adapt-range

> '''Parameters:'''
> * '''lower''' – lower bound for the range. `None` means unbound
> * '''upper''' – upper bound for the range. `None` means unbound

4. [=#point04] https://docs.djangoproject.com/en/dev/ref/contrib/postgres/validators/#range-validators

> === Range validators
> ==== RangeMaxValueValidator
> 
> ...
> 
> Validates that the upper bound of the range is not greater than `limit_value`.
> 
> ==== RangeMinValueValidator
> 
> ...
> 
> Validates that the lower bound of the range is not less than the `limit_value`.

5. [=#point05] https://github.com/django/django/blob/617686e226231fe8ad3f2e49d3efabf6f5f434d3/tests/postgres_tests/test_ranges.py#L406,L422

> {{{#!python
> class TestValidators(PostgreSQLTestCase):
> 
>     def test_max(self):
>         validator = RangeMaxValueValidator(5)
>         validator(NumericRange(0, 5))
>         with self.assertRaises(exceptions.ValidationError) as cm:
>             validator(NumericRange(0, 10))
>         self.assertEqual(cm.exception.messages[0], 'Ensure that this range is completely less than or equal to 5.')
>         self.assertEqual(cm.exception.code, 'max_value')
> 
>     def test_min(self):
>         validator = RangeMinValueValidator(5)
>         validator(NumericRange(10, 15))
>         with self.assertRaises(exceptions.ValidationError) as cm:
>             validator(NumericRange(0, 10))
>         self.assertEqual(cm.exception.messages[0], 'Ensure that this range is completely greater than or equal to 5.')
>         self.assertEqual(cm.exception.code, 'min_value')
> }}}

6. [=#point06] https://github.com/django/django/blob/617686e226231fe8ad3f2e49d3efabf6f5f434d3/django/contrib/postgres/validators.py#L70,L79

> {{{#!python
> class RangeMaxValueValidator(MaxValueValidator):
>     def compare(self, a, b):
>         return a.upper > b
>     message = _('Ensure that this range is completely less than or equal to %(limit_value)s.')
> 
> 
> class RangeMinValueValidator(MinValueValidator):
>     def compare(self, a, b):
>         return a.lower < b
>     message = _('Ensure that this range is completely greater than or equal to %(limit_value)s.')
> }}}


== Encountered `Server Error (500)` using Django 1.10 (Python 3.5)

Based on:

{{{#!python
    pages = IntegerRangeField(
        validators=[
            RangeMaxValueValidator(750),
            RangeMinValueValidator(0),
        ],
    )
}}}

Input (via. `django-admin`):

* '''lower''': `0`
* '''upper''': (blank)

Resulted in `Server Error (500)`, with the corresponding traceback tail:

{{{#!python
  File "".../django/contrib/postgres/validators.py"", line 75, in compare
    return a.upper > b
TypeError: unorderable types: NoneType() > int()
}}}

Subsequently tested `RangeMinValueValidator` with the following input:

* '''lower''': (blank)
* '''upper''': `750`

Resulted in `Server Error (500)`, with the corresponding traceback tail:

{{{#!python
  File "".../django/contrib/postgres/validators.py"", line 82, in compare
    return a.lower < b
TypeError: unorderable types: NoneType() < int()
}}}


== Devised test cases based on `master`

Neither of the two existing test cases^([#point05 5])^ test for infinite (unbounded) ranges^([#point01 1])^, i.e. using `None`^([#point03 3])^.  Furthermore, only `NumericRange` was being tested.

1. Using the two existing test cases as a basis, produced another two test cases using `None`
1. Then used these four test cases to devise similar test cases for `DateRange` as well

`$ ./runtests.py --settings=test_postgres postgres_tests.test_ranges.TestValidators`:

{{{#!python
.E.E.E.E
======================================================================
ERROR: test_daterange_max_with_infinite_bound (postgres_tests.test_ranges.TestValidators)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ""/home/scratch/GitHub/django-repo/tests/postgres_tests/test_ranges.py"", line 455, in test_daterange_max_with_infinite_bound
    validator(DateRange('2017-01-01', None))
  File ""/home/scratch/GitHub/django-repo/django/core/validators.py"", line 321, in __call__
    if self.compare(cleaned, self.limit_value):
  File ""/home/scratch/GitHub/django-repo/django/contrib/postgres/validators.py"", line 72, in compare
    return a.upper > b
TypeError: '>' not supported between instances of 'NoneType' and 'str'

======================================================================
ERROR: test_daterange_min_with_infinite_bound (postgres_tests.test_ranges.TestValidators)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ""/home/scratch/GitHub/django-repo/tests/postgres_tests/test_ranges.py"", line 477, in test_daterange_min_with_infinite_bound
    validator(DateRange(None, '2019-01-01'))
  File ""/home/scratch/GitHub/django-repo/django/core/validators.py"", line 321, in __call__
    if self.compare(cleaned, self.limit_value):
  File ""/home/scratch/GitHub/django-repo/django/contrib/postgres/validators.py"", line 78, in compare
    return a.lower < b
TypeError: '<' not supported between instances of 'NoneType' and 'str'

======================================================================
ERROR: test_numericrange_max_with_infinite_bound (postgres_tests.test_ranges.TestValidators)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ""/home/scratch/GitHub/django-repo/tests/postgres_tests/test_ranges.py"", line 420, in test_numericrange_max_with_infinite_bound
    validator(NumericRange(0, None))
  File ""/home/scratch/GitHub/django-repo/django/core/validators.py"", line 321, in __call__
    if self.compare(cleaned, self.limit_value):
  File ""/home/scratch/GitHub/django-repo/django/contrib/postgres/validators.py"", line 72, in compare
    return a.upper > b
TypeError: '>' not supported between instances of 'NoneType' and 'int'

======================================================================
ERROR: test_numericrange_min_with_infinite_bound (postgres_tests.test_ranges.TestValidators)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ""/home/scratch/GitHub/django-repo/tests/postgres_tests/test_ranges.py"", line 436, in test_numericrange_min_with_infinite_bound
    validator(NumericRange(None, 10))
  File ""/home/scratch/GitHub/django-repo/django/core/validators.py"", line 321, in __call__
    if self.compare(cleaned, self.limit_value):
  File ""/home/scratch/GitHub/django-repo/django/contrib/postgres/validators.py"", line 78, in compare
    return a.lower < b
TypeError: '<' not supported between instances of 'NoneType' and 'int'

----------------------------------------------------------------------
Ran 8 tests in 0.038s

FAILED (errors=4)
}}}


== Prepared patch

Proposed patch is to modify both of the range validators^([#point06 6])^:

{{{#!diff
-        return a.upper > b
+        return True if a.upper is None else a.upper > b
}}}

{{{#!diff
-        return a.lower < b
+        return True if a.lower is None else a.lower < b
}}}

`$ ./runtests.py --settings=test_postgres postgres_tests.test_ranges.TestValidators`:

{{{#!python
........
----------------------------------------------------------------------
Ran 8 tests in 0.006s

OK
}}}"	Bug	closed	contrib.postgres	dev	Normal	fixed		Shai Berger myii	Accepted	1	0	0	0	0	0
