From 6de7f9ec60fbdc59797bc21803f16260bd203f04 Mon Sep 17 00:00:00 2001
From: Stefano Chiodino <StefanoChiodino@users.noreply.github.com>
Date: Wed, 3 Oct 2018 00:17:23 +0100
Subject: [PATCH] Fixed #29598 -- Deprecated FloatRangeField in favor of
 DecimalRangeField.

---
 django/contrib/postgres/apps.py               |  2 +-
 django/contrib/postgres/fields/ranges.py      | 19 ++++-
 django/contrib/postgres/forms/ranges.py       | 22 ++++--
 docs/internals/deprecation.txt                |  3 +
 docs/ref/checks.txt                           |  2 +
 docs/ref/contrib/postgres/fields.txt          | 16 +++++
 docs/ref/contrib/postgres/forms.txt           | 15 ++++
 docs/releases/2.2.txt                         |  4 ++
 tests/postgres_tests/fields.py                |  4 +-
 .../migrations/0002_create_test_models.py     |  4 +-
 tests/postgres_tests/models.py                |  4 +-
 tests/postgres_tests/test_introspection.py    |  2 +-
 tests/postgres_tests/test_ranges.py           | 71 +++++++++++--------
 13 files changed, 126 insertions(+), 42 deletions(-)

diff --git a/django/contrib/postgres/apps.py b/django/contrib/postgres/apps.py
index 1ab5074f19..f1ec09806a 100644
--- a/django/contrib/postgres/apps.py
+++ b/django/contrib/postgres/apps.py
@@ -19,7 +19,7 @@ class PostgresConfig(AppConfig):
                 conn.introspection.data_types_reverse.update({
                     3802: 'django.contrib.postgres.fields.JSONField',
                     3904: 'django.contrib.postgres.fields.IntegerRangeField',
-                    3906: 'django.contrib.postgres.fields.FloatRangeField',
+                    3906: 'django.contrib.postgres.fields.DecimalRangeField',
                     3910: 'django.contrib.postgres.fields.DateTimeRangeField',
                     3912: 'django.contrib.postgres.fields.DateRangeField',
                     3926: 'django.contrib.postgres.fields.BigIntegerRangeField',
diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py
index 0bb914d383..74ba4eb230 100644
--- a/django/contrib/postgres/fields/ranges.py
+++ b/django/contrib/postgres/fields/ranges.py
@@ -10,7 +10,8 @@ from .utils import AttributeSetter
 
 __all__ = [
     'RangeField', 'IntegerRangeField', 'BigIntegerRangeField',
-    'FloatRangeField', 'DateTimeRangeField', 'DateRangeField',
+    'DecimalRangeField', 'DateTimeRangeField', 'DateRangeField',
+    'FloatRangeField',
 ]
 
 
@@ -100,7 +101,23 @@ class BigIntegerRangeField(RangeField):
         return 'int8range'
 
 
+class DecimalRangeField(RangeField):
+    base_field = models.DecimalField
+    range_type = NumericRange
+    form_field = forms.DecimalRangeField
+
+    def db_type(self, connection):
+        return 'numrange'
+
+
 class FloatRangeField(RangeField):
+    system_check_deprecated_details = {
+        'msg': (
+            'FloatRangeField is deprecated and will be removed in Django 3.1.'
+        ),
+        'hint': 'Use DecimalRangeField instead.',
+        'id': 'fields.W902',
+    }
     base_field = models.FloatField
     range_type = NumericRange
     form_field = forms.FloatRangeField
diff --git a/django/contrib/postgres/forms/ranges.py b/django/contrib/postgres/forms/ranges.py
index 5f2b2434a3..c36bec8479 100644
--- a/django/contrib/postgres/forms/ranges.py
+++ b/django/contrib/postgres/forms/ranges.py
@@ -1,13 +1,16 @@
+import warnings
+
 from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange
 
 from django import forms
 from django.core import exceptions
 from django.forms.widgets import MultiWidget
+from django.utils.deprecation import RemovedInDjango31Warning
 from django.utils.translation import gettext_lazy as _
 
 __all__ = [
-    'BaseRangeField', 'IntegerRangeField', 'FloatRangeField',
-    'DateTimeRangeField', 'DateRangeField', 'RangeWidget',
+    'BaseRangeField', 'IntegerRangeField', 'DecimalRangeField',
+    'DateTimeRangeField', 'DateRangeField', 'FloatRangeField', 'RangeWidget',
 ]
 
 
@@ -66,12 +69,23 @@ class IntegerRangeField(BaseRangeField):
     range_type = NumericRange
 
 
-class FloatRangeField(BaseRangeField):
+class DecimalRangeField(BaseRangeField):
     default_error_messages = {'invalid': _('Enter two numbers.')}
-    base_field = forms.FloatField
+    base_field = forms.DecimalField
     range_type = NumericRange
 
 
+class FloatRangeField(DecimalRangeField):
+    base_field = forms.FloatField
+
+    def __init__(self, **kwargs):
+        warnings.warn(
+            'FloatRangeField is deprecated in favor of DecimalRangeField.',
+            RemovedInDjango31Warning, stacklevel=2,
+        )
+        super().__init__(**kwargs)
+
+
 class DateTimeRangeField(BaseRangeField):
     default_error_messages = {'invalid': _('Enter two valid date/times.')}
     base_field = forms.DateTimeField
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 7043391527..1d03648ab1 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -21,6 +21,9 @@ details on these changes.
 
 * A model's ``Meta.ordering`` will no longer affect ``GROUP BY`` queries.
 
+* ``django.contrib.postgres.fields.FloatRangeField`` and
+  ``django.contrib.postgres.forms.FloatRangeField`` will be removed.
+
 .. _deprecation-removed-in-3.0:
 
 3.0
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index c8b13aa100..dd74639d33 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -168,6 +168,8 @@ Model fields
   check appeared in Django 1.10 and 1.11*.
 * **fields.E901**: ``CommaSeparatedIntegerField`` is removed except for support
   in historical migrations.
+* **fields.W902**: ``FloatRangeField`` is deprecated and will be removed in
+  Django 3.1.
 
 File fields
 ~~~~~~~~~~~
diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt
index 0cac299f8d..bcab67d132 100644
--- a/docs/ref/contrib/postgres/fields.txt
+++ b/docs/ref/contrib/postgres/fields.txt
@@ -653,6 +653,18 @@ excluded; that is, ``[)``.
     returns a range in a canonical form that includes the lower bound and
     excludes the upper bound; that is ``[)``.
 
+``DecimalRangeField``
+---------------------
+
+.. class:: DecimalRangeField(**options)
+
+    .. versionadded:: 2.2
+
+    Stores a range of floating point values. Based on a
+    :class:`~django.db.models.DecimalField`. Represented by a ``numrange`` in
+    the database and a :class:`~psycopg2:psycopg2.extras.NumericRange` in
+    Python.
+
 ``FloatRangeField``
 -------------------
 
@@ -662,6 +674,10 @@ excluded; that is, ``[)``.
     :class:`~django.db.models.FloatField`. Represented by a ``numrange`` in the
     database and a :class:`~psycopg2:psycopg2.extras.NumericRange` in Python.
 
+    .. deprecated:: 2.2
+
+        Use :class:`DecimalRangeField` instead.
+
 ``DateTimeRangeField``
 ----------------------
 
diff --git a/docs/ref/contrib/postgres/forms.txt b/docs/ref/contrib/postgres/forms.txt
index 04adbc3a40..b5effb520c 100644
--- a/docs/ref/contrib/postgres/forms.txt
+++ b/docs/ref/contrib/postgres/forms.txt
@@ -193,6 +193,17 @@ not greater than the upper bound. All of these fields use
     :class:`~django.contrib.postgres.fields.IntegerRangeField` and
     :class:`~django.contrib.postgres.fields.BigIntegerRangeField`.
 
+``DecimalRangeField``
+~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: DecimalRangeField
+
+    .. versionadded:: 2.2
+
+    Based on :class:`~django.forms.DecimalField` and translates its input into
+    :class:`~psycopg2:psycopg2.extras.NumericRange`. Default for
+    :class:`~django.contrib.postgres.fields.DecimalRangeField`.
+
 ``FloatRangeField``
 ~~~~~~~~~~~~~~~~~~~
 
@@ -202,6 +213,10 @@ not greater than the upper bound. All of these fields use
     :class:`~psycopg2:psycopg2.extras.NumericRange`. Default for
     :class:`~django.contrib.postgres.fields.FloatRangeField`.
 
+    .. deprecated:: 2.2
+
+        Use :class:`DecimalRangeField` instead.
+
 ``DateTimeRangeField``
 ~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt
index 901868cebf..7d85d30c4a 100644
--- a/docs/releases/2.2.txt
+++ b/docs/releases/2.2.txt
@@ -320,3 +320,7 @@ Miscellaneous
 
 * The undocumented ``QuerySetPaginator`` alias of
   ``django.core.paginator.Paginator`` is deprecated.
+
+* The ``FloatRangeField`` model and form fields in ``django.contrib.postgres``
+  are deprecated in favor of a new name, ``DecimalRangeField``, to match the
+  underlying ``numrange`` data type used in the database.
diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py
index bcc6998a3c..2275eb2ab2 100644
--- a/tests/postgres_tests/fields.py
+++ b/tests/postgres_tests/fields.py
@@ -7,7 +7,7 @@ from django.db import models
 try:
     from django.contrib.postgres.fields import (
         ArrayField, BigIntegerRangeField, CICharField, CIEmailField,
-        CITextField, DateRangeField, DateTimeRangeField, FloatRangeField,
+        CITextField, DateRangeField, DateTimeRangeField, DecimalRangeField,
         HStoreField, IntegerRangeField, JSONField,
     )
     from django.contrib.postgres.search import SearchVectorField
@@ -35,7 +35,7 @@ except ImportError:
     CITextField = models.Field
     DateRangeField = models.Field
     DateTimeRangeField = models.Field
-    FloatRangeField = models.Field
+    DecimalRangeField = models.Field
     HStoreField = models.Field
     IntegerRangeField = models.Field
     JSONField = DummyJSONField
diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py
index 0e7ba938ca..5db8a71385 100644
--- a/tests/postgres_tests/migrations/0002_create_test_models.py
+++ b/tests/postgres_tests/migrations/0002_create_test_models.py
@@ -3,7 +3,7 @@ from django.db import migrations, models
 
 from ..fields import (
     ArrayField, BigIntegerRangeField, CICharField, CIEmailField, CITextField,
-    DateRangeField, DateTimeRangeField, FloatRangeField, HStoreField,
+    DateRangeField, DateTimeRangeField, DecimalRangeField, HStoreField,
     IntegerRangeField, JSONField, SearchVectorField,
 )
 from ..models import TagField
@@ -209,7 +209,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('ints', IntegerRangeField(null=True, blank=True)),
                 ('bigints', BigIntegerRangeField(null=True, blank=True)),
-                ('floats', FloatRangeField(null=True, blank=True)),
+                ('decimals', DecimalRangeField(null=True, blank=True)),
                 ('timestamps', DateTimeRangeField(null=True, blank=True)),
                 ('dates', DateRangeField(null=True, blank=True)),
             ],
diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py
index 841f246c6a..cbe477e402 100644
--- a/tests/postgres_tests/models.py
+++ b/tests/postgres_tests/models.py
@@ -3,7 +3,7 @@ from django.db import models
 
 from .fields import (
     ArrayField, BigIntegerRangeField, CICharField, CIEmailField, CITextField,
-    DateRangeField, DateTimeRangeField, FloatRangeField, HStoreField,
+    DateRangeField, DateTimeRangeField, DecimalRangeField, HStoreField,
     IntegerRangeField, JSONField, SearchVectorField,
 )
 
@@ -129,7 +129,7 @@ class Line(PostgreSQLModel):
 class RangesModel(PostgreSQLModel):
     ints = IntegerRangeField(blank=True, null=True)
     bigints = BigIntegerRangeField(blank=True, null=True)
-    floats = FloatRangeField(blank=True, null=True)
+    decimals = DecimalRangeField(blank=True, null=True)
     timestamps = DateTimeRangeField(blank=True, null=True)
     dates = DateRangeField(blank=True, null=True)
 
diff --git a/tests/postgres_tests/test_introspection.py b/tests/postgres_tests/test_introspection.py
index 2ab2c96cea..8ae5b80da1 100644
--- a/tests/postgres_tests/test_introspection.py
+++ b/tests/postgres_tests/test_introspection.py
@@ -31,7 +31,7 @@ class InspectDBTests(PostgreSQLTestCase):
             [
                 'ints = django.contrib.postgres.fields.IntegerRangeField(blank=True, null=True)',
                 'bigints = django.contrib.postgres.fields.BigIntegerRangeField(blank=True, null=True)',
-                'floats = django.contrib.postgres.fields.FloatRangeField(blank=True, null=True)',
+                'decimals = django.contrib.postgres.fields.DecimalRangeField(blank=True, null=True)',
                 'timestamps = django.contrib.postgres.fields.DateTimeRangeField(blank=True, null=True)',
                 'dates = django.contrib.postgres.fields.DateRangeField(blank=True, null=True)',
             ],
diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py
index 6aa6c889dd..c05ac19699 100644
--- a/tests/postgres_tests/test_ranges.py
+++ b/tests/postgres_tests/test_ranges.py
@@ -1,11 +1,13 @@
 import datetime
 import json
+from decimal import Decimal
 
 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.test import ignore_warnings, override_settings
 from django.utils import timezone
+from django.utils.deprecation import RemovedInDjango31Warning
 
 from . import PostgreSQLTestCase
 from .models import RangeLookupsModel, RangesModel
@@ -27,7 +29,7 @@ class TestSaveLoad(PostgreSQLTestCase):
         instance = RangesModel(
             ints=NumericRange(0, 10),
             bigints=NumericRange(10, 20),
-            floats=NumericRange(20, 30),
+            decimals=NumericRange(20, 30),
             timestamps=DateTimeTZRange(now - datetime.timedelta(hours=1), now),
             dates=DateRange(now.date() - datetime.timedelta(days=1), now.date()),
         )
@@ -35,7 +37,7 @@ class TestSaveLoad(PostgreSQLTestCase):
         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.decimals, loaded.decimals)
         self.assertEqual(instance.timestamps, loaded.timestamps)
         self.assertEqual(instance.dates, loaded.dates)
 
@@ -54,18 +56,18 @@ class TestSaveLoad(PostgreSQLTestCase):
 
     def test_range_object_boundaries(self):
         r = NumericRange(0, 10, '[]')
-        instance = RangesModel(floats=r)
+        instance = RangesModel(decimals=r)
         instance.save()
         loaded = RangesModel.objects.get()
-        self.assertEqual(r, loaded.floats)
-        self.assertIn(10, loaded.floats)
+        self.assertEqual(r, loaded.decimals)
+        self.assertIn(10, loaded.decimals)
 
     def test_unbounded(self):
         r = NumericRange(None, None, '()')
-        instance = RangesModel(floats=r)
+        instance = RangesModel(decimals=r)
         instance.save()
         loaded = RangesModel.objects.get()
-        self.assertEqual(r, loaded.floats)
+        self.assertEqual(r, loaded.decimals)
 
     def test_empty(self):
         r = NumericRange(empty=True)
@@ -331,13 +333,13 @@ class TestQueryingWithRanges(PostgreSQLTestCase):
         )
 
     def test_f_ranges(self):
-        parent = RangesModel.objects.create(floats=NumericRange(0, 10))
+        parent = RangesModel.objects.create(decimals=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')),
+            RangeLookupsModel.objects.filter(float__contained_by=F('parent__decimals')),
             [objs[0]]
         )
 
@@ -356,7 +358,7 @@ class TestQueryingWithRanges(PostgreSQLTestCase):
 class TestSerialization(PostgreSQLTestCase):
     test_data = (
         '[{"fields": {"ints": "{\\"upper\\": \\"10\\", \\"lower\\": \\"0\\", '
-        '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", '
+        '\\"bounds\\": \\"[)\\"}", "decimals": "{\\"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\\": \\"[)\\"}" }, '
@@ -370,7 +372,7 @@ class TestSerialization(PostgreSQLTestCase):
 
     def test_dumping(self):
         instance = RangesModel(
-            ints=NumericRange(0, 10), floats=NumericRange(empty=True),
+            ints=NumericRange(0, 10), decimals=NumericRange(empty=True),
             timestamps=DateTimeTZRange(self.lower_dt, self.upper_dt),
             dates=DateRange(self.lower_date, self.upper_date),
         )
@@ -386,7 +388,7 @@ class TestSerialization(PostgreSQLTestCase):
     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.decimals, 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))
@@ -435,11 +437,22 @@ class TestFormField(PostgreSQLTestCase):
         value = field.clean(['1', '2'])
         self.assertEqual(value, NumericRange(1, 2))
 
+    @ignore_warnings(category=RemovedInDjango31Warning)
     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_decimal(self):
+        field = pg_forms.DecimalRangeField()
+        value = field.clean(['1.12345', '2.001'])
+        self.assertEqual(value, NumericRange(Decimal('1.12345'), Decimal('2.001')))
+
+    def test_float_range_field_deprecation(self):
+        msg = 'FloatRangeField is deprecated in favor of DecimalRangeField.'
+        with self.assertRaisesMessage(RemovedInDjango31Warning, msg):
+            pg_forms.FloatRangeField()
+
     def test_valid_timestamps(self):
         field = pg_forms.DateTimeRangeField()
         value = field.clean(['01/01/2014 00:00:00', '02/02/2014 12:12:12'])
@@ -544,44 +557,44 @@ class TestFormField(PostgreSQLTestCase):
         value = field.clean([1, ''])
         self.assertEqual(value, NumericRange(1, None))
 
-    def test_float_lower_bound_higher(self):
-        field = pg_forms.FloatRangeField()
+    def test_decimal_lower_bound_higher(self):
+        field = pg_forms.DecimalRangeField()
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean(['1.8', '1.6'])
         self.assertEqual(cm.exception.messages[0], 'The start of the range must not exceed the end of the range.')
         self.assertEqual(cm.exception.code, 'bound_ordering')
 
-    def test_float_open(self):
-        field = pg_forms.FloatRangeField()
+    def test_decimal_open(self):
+        field = pg_forms.DecimalRangeField()
         value = field.clean(['', '3.1415926'])
-        self.assertEqual(value, NumericRange(None, 3.1415926))
+        self.assertEqual(value, NumericRange(None, Decimal('3.1415926')))
 
-    def test_float_incorrect_data_type(self):
-        field = pg_forms.FloatRangeField()
+    def test_decimal_incorrect_data_type(self):
+        field = pg_forms.DecimalRangeField()
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean('1.6')
         self.assertEqual(cm.exception.messages[0], 'Enter two numbers.')
         self.assertEqual(cm.exception.code, 'invalid')
 
-    def test_float_invalid_lower(self):
-        field = pg_forms.FloatRangeField()
+    def test_decimal_invalid_lower(self):
+        field = pg_forms.DecimalRangeField()
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean(['a', '3.1415926'])
         self.assertEqual(cm.exception.messages[0], 'Enter a number.')
 
-    def test_float_invalid_upper(self):
-        field = pg_forms.FloatRangeField()
+    def test_decimal_invalid_upper(self):
+        field = pg_forms.DecimalRangeField()
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean(['1.61803399', 'b'])
         self.assertEqual(cm.exception.messages[0], 'Enter a number.')
 
-    def test_float_required(self):
-        field = pg_forms.FloatRangeField(required=True)
+    def test_decimal_required(self):
+        field = pg_forms.DecimalRangeField(required=True)
         with self.assertRaises(exceptions.ValidationError) as cm:
             field.clean(['', ''])
         self.assertEqual(cm.exception.messages[0], 'This field is required.')
         value = field.clean(['1.61803399', ''])
-        self.assertEqual(value, NumericRange(1.61803399, None))
+        self.assertEqual(value, NumericRange(Decimal('1.61803399'), None))
 
     def test_date_lower_bound_higher(self):
         field = pg_forms.DateRangeField()
@@ -680,9 +693,9 @@ class TestFormField(PostgreSQLTestCase):
         self.assertIsInstance(form_field, pg_forms.IntegerRangeField)
 
     def test_model_field_formfield_float(self):
-        model_field = pg_fields.FloatRangeField()
+        model_field = pg_fields.DecimalRangeField()
         form_field = model_field.formfield()
-        self.assertIsInstance(form_field, pg_forms.FloatRangeField)
+        self.assertIsInstance(form_field, pg_forms.DecimalRangeField)
 
     def test_model_field_formfield_date(self):
         model_field = pg_fields.DateRangeField()