Browse Source

Fixed #22936 -- Obsoleted Field.get_prep_lookup()/get_db_prep_lookup()

Thanks Tim Graham for completing the initial patch.
Claude Paroz 9 years ago
parent
commit
388bb5bd9a

+ 3 - 0
django/contrib/admin/filters.py

@@ -232,6 +232,9 @@ class BooleanFieldListFilter(FieldListFilter):
         self.lookup_val = request.GET.get(self.lookup_kwarg)
         self.lookup_val2 = request.GET.get(self.lookup_kwarg2)
         super(BooleanFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
+        if (self.used_parameters and self.lookup_kwarg in self.used_parameters and
+                self.used_parameters[self.lookup_kwarg] in ('1', '0')):
+            self.used_parameters[self.lookup_kwarg] = bool(int(self.used_parameters[self.lookup_kwarg]))
 
     def expected_parameters(self):
         return [self.lookup_kwarg, self.lookup_kwarg2]

+ 0 - 1
django/contrib/admin/widgets.py

@@ -128,7 +128,6 @@ def url_params_from_lookup_dict(lookups):
             if isinstance(v, (tuple, list)):
                 v = ','.join(str(x) for x in v)
             elif isinstance(v, bool):
-                # See django.db.fields.BooleanField.get_prep_lookup
                 v = ('0', '1')[v]
             else:
                 v = six.text_type(v)

+ 0 - 7
django/contrib/gis/db/models/fields.py

@@ -316,13 +316,6 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
             params = [connection.ops.Adapter(value)]
         return params
 
-    def get_prep_lookup(self, lookup_type, value):
-        if lookup_type == 'contains':
-            # 'contains' name might conflict with the "normal" contains lookup,
-            # for which the value is not prepared, but left as-is.
-            return self.get_prep_value(value)
-        return super(GeometryField, self).get_prep_lookup(lookup_type, value)
-
     def get_db_prep_save(self, value, connection):
         "Prepares the value for saving in the database."
         if not value:

+ 0 - 7
django/contrib/postgres/fields/jsonb.py

@@ -31,13 +31,6 @@ class JSONField(Field):
             return Json(value)
         return value
 
-    def get_prep_lookup(self, lookup_type, value):
-        if lookup_type in ('has_key', 'has_keys', 'has_any_keys'):
-            return value
-        if isinstance(value, (dict, list)):
-            return Json(value)
-        return super(JSONField, self).get_prep_lookup(lookup_type, value)
-
     def validate(self, value, model_instance):
         super(JSONField, self).validate(value, model_instance)
         try:

+ 1 - 1
django/contrib/postgres/fields/ranges.py

@@ -154,7 +154,7 @@ class RangeContainedBy(models.Lookup):
         return sql % (lhs, rhs), params
 
     def get_prep_lookup(self):
-        return RangeField().get_prep_lookup(self.lookup_name, self.rhs)
+        return RangeField().get_prep_value(self.rhs)
 
 
 models.DateField.register_lookup(RangeContainedBy)

+ 6 - 1
django/contrib/postgres/lookups.py

@@ -1,4 +1,5 @@
 from django.db.models import Lookup, Transform
+from django.utils.encoding import force_text
 
 from .search import SearchVector, SearchVectorExact, SearchVectorField
 
@@ -29,14 +30,18 @@ class Overlap(PostgresSimpleLookup):
 class HasKey(PostgresSimpleLookup):
     lookup_name = 'has_key'
     operator = '?'
+    prepare_rhs = False
 
 
 class HasKeys(PostgresSimpleLookup):
     lookup_name = 'has_keys'
     operator = '?&'
 
+    def get_prep_lookup(self):
+        return [force_text(item) for item in self.rhs]
 
-class HasAnyKeys(PostgresSimpleLookup):
+
+class HasAnyKeys(HasKeys):
     lookup_name = 'has_any_keys'
     operator = '?|'
 

+ 1 - 1
django/db/models/expressions.py

@@ -213,7 +213,7 @@ class BaseExpression(object):
 
     def _prepare(self, field):
         """
-        Hook used by Field.get_prep_lookup() to do custom preparation.
+        Hook used by Lookup.get_prep_lookup() to do custom preparation.
         """
         return self
 

+ 3 - 55
django/db/models/fields/__init__.py

@@ -741,8 +741,7 @@ class Field(RegisterLookupMixin):
         """Returns field's value prepared for interacting with the database
         backend.
 
-        Used by the default implementations of ``get_db_prep_save``and
-        `get_db_prep_lookup```
+        Used by the default implementations of get_db_prep_save().
         """
         if not prepared:
             value = self.get_prep_value(value)
@@ -755,36 +754,6 @@ class Field(RegisterLookupMixin):
         return self.get_db_prep_value(value, connection=connection,
                                       prepared=False)
 
-    def get_prep_lookup(self, lookup_type, value):
-        """
-        Perform preliminary non-db specific lookup checks and conversions
-        """
-        if hasattr(value, '_prepare'):
-            return value._prepare(self)
-
-        if lookup_type in {
-            'iexact', 'contains', 'icontains',
-            'startswith', 'istartswith', 'endswith', 'iendswith',
-            'isnull', 'search', 'regex', 'iregex',
-        }:
-            return value
-        elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
-            return self.get_prep_value(value)
-        elif lookup_type in ('range', 'in'):
-            return [self.get_prep_value(v) for v in value]
-        return self.get_prep_value(value)
-
-    def get_db_prep_lookup(self, lookup_type, value, connection,
-                           prepared=False):
-        """
-        Returns field's value prepared for database lookup.
-        """
-        if not prepared:
-            value = self.get_prep_lookup(lookup_type, value)
-            prepared = True
-
-        return [value]
-
     def has_default(self):
         """
         Returns a boolean of whether this field has a default value.
@@ -1049,20 +1018,11 @@ class BooleanField(Field):
             params={'value': value},
         )
 
-    def get_prep_lookup(self, lookup_type, value):
-        # Special-case handling for filters coming from a Web request (e.g. the
-        # admin interface). Only works for scalar values (not lists). If you're
-        # passing in a list, you might as well make things the right type when
-        # constructing the list.
-        if value in ('1', '0'):
-            value = bool(int(value))
-        return super(BooleanField, self).get_prep_lookup(lookup_type, value)
-
     def get_prep_value(self, value):
         value = super(BooleanField, self).get_prep_value(value)
         if value is None:
             return None
-        return bool(value)
+        return self.to_python(value)
 
     def formfield(self, **kwargs):
         # Unlike most fields, BooleanField figures out include_blank from
@@ -1453,8 +1413,6 @@ class DateTimeField(DateField):
     # contribute_to_class is inherited from DateField, it registers
     # get_next_by_FOO and get_prev_by_FOO
 
-    # get_prep_lookup is inherited from DateField
-
     def get_prep_value(self, value):
         value = super(DateTimeField, self).get_prep_value(value)
         value = self.to_python(value)
@@ -2051,21 +2009,11 @@ class NullBooleanField(Field):
             params={'value': value},
         )
 
-    def get_prep_lookup(self, lookup_type, value):
-        # Special-case handling for filters coming from a Web request (e.g. the
-        # admin interface). Only works for scalar values (not lists). If you're
-        # passing in a list, you might as well make things the right type when
-        # constructing the list.
-        if value in ('1', '0'):
-            value = bool(int(value))
-        return super(NullBooleanField, self).get_prep_lookup(lookup_type,
-                                                             value)
-
     def get_prep_value(self, value):
         value = super(NullBooleanField, self).get_prep_value(value)
         if value is None:
             return None
-        return bool(value)
+        return self.to_python(value)
 
     def formfield(self, **kwargs):
         defaults = {

+ 0 - 5
django/db/models/fields/files.py

@@ -271,11 +271,6 @@ class FileField(Field):
     def get_internal_type(self):
         return "FileField"
 
-    def get_prep_lookup(self, lookup_type, value):
-        if hasattr(value, 'name'):
-            value = value.name
-        return super(FileField, self).get_prep_lookup(lookup_type, value)
-
     def get_prep_value(self, value):
         "Returns field's value prepared for saving into a database."
         value = super(FileField, self).get_prep_value(value)

+ 7 - 7
django/db/models/fields/related_lookups.py

@@ -44,15 +44,15 @@ class RelatedIn(In):
         if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value():
             # If we get here, we are dealing with single-column relations.
             self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs]
-            # We need to run the related field's get_prep_lookup(). Consider case
+            # We need to run the related field's get_prep_value(). Consider case
             # ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
             # doesn't have validation for non-integers, so we must run validation
             # using the target field.
             if hasattr(self.lhs.output_field, 'get_path_info'):
-                # Run the target field's get_prep_lookup. We can safely assume there is
+                # Run the target field's get_prep_value. We can safely assume there is
                 # only one as we don't get to the direct value branch otherwise.
-                self.rhs = self.lhs.output_field.get_path_info()[-1].target_fields[-1].get_prep_lookup(
-                    self.lookup_name, self.rhs)
+                target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
+                self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
         return super(RelatedIn, self).get_prep_lookup()
 
     def as_sql(self, compiler, connection):
@@ -88,15 +88,15 @@ class RelatedLookupMixin(object):
         if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value():
             # If we get here, we are dealing with single-column relations.
             self.rhs = get_normalized_value(self.rhs, self.lhs)[0]
-            # We need to run the related field's get_prep_lookup(). Consider case
+            # We need to run the related field's get_prep_value(). Consider case
             # ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
             # doesn't have validation for non-integers, so we must run validation
             # using the target field.
             if hasattr(self.lhs.output_field, 'get_path_info'):
                 # Get the target field. We can safely assume there is only one
                 # as we don't get to the direct value branch otherwise.
-                self.rhs = self.lhs.output_field.get_path_info()[-1].target_fields[-1].get_prep_lookup(
-                    self.lookup_name, self.rhs)
+                target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
+                self.rhs = target_field.get_prep_value(self.rhs)
 
         return super(RelatedLookupMixin, self).get_prep_lookup()
 

+ 0 - 7
django/db/models/fields/reverse_related.py

@@ -110,9 +110,6 @@ class ForeignObjectRel(object):
     def one_to_one(self):
         return self.field.one_to_one
 
-    def get_prep_lookup(self, lookup_name, value):
-        return self.field.get_prep_lookup(lookup_name, value)
-
     def get_lookup(self, lookup_name):
         return self.field.get_lookup(lookup_name)
 
@@ -142,10 +139,6 @@ class ForeignObjectRel(object):
             (x._get_pk_val(), smart_text(x)) for x in self.related_model._default_manager.all()
         ]
 
-    def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
-        # Defer to the actual field definition for db prep
-        return self.field.get_db_prep_lookup(lookup_type, value, connection=connection, prepared=prepared)
-
     def is_hidden(self):
         "Should the related object be hidden?"
         return bool(self.related_name) and self.related_name[-1] == '+'

+ 29 - 4
django/db/models/lookups.py

@@ -16,6 +16,7 @@ from django.utils.six.moves import range
 
 class Lookup(object):
     lookup_name = None
+    prepare_rhs = True
 
     def __init__(self, lhs, rhs):
         self.lhs, self.rhs = lhs, rhs
@@ -56,12 +57,14 @@ class Lookup(object):
         return sqls, sqls_params
 
     def get_prep_lookup(self):
-        return self.lhs.output_field.get_prep_lookup(self.lookup_name, self.rhs)
+        if hasattr(self.rhs, '_prepare'):
+            return self.rhs._prepare(self.lhs.output_field)
+        if self.prepare_rhs and hasattr(self.lhs.output_field, 'get_prep_value'):
+            return self.lhs.output_field.get_prep_value(self.rhs)
+        return self.rhs
 
     def get_db_prep_lookup(self, value, connection):
-        return (
-            '%s', self.lhs.output_field.get_db_prep_lookup(
-                self.lookup_name, value, connection, prepared=True))
+        return ('%s', [value])
 
     def process_lhs(self, compiler, connection, lhs=None):
         lhs = lhs or self.lhs
@@ -199,6 +202,7 @@ Field.register_lookup(Exact)
 
 class IExact(BuiltinLookup):
     lookup_name = 'iexact'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(IExact, self).process_rhs(qn, connection)
@@ -254,6 +258,13 @@ IntegerField.register_lookup(IntegerLessThan)
 class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
     lookup_name = 'in'
 
+    def get_prep_lookup(self):
+        if hasattr(self.rhs, '_prepare'):
+            return self.rhs._prepare(self.lhs.output_field)
+        if hasattr(self.lhs.output_field, 'get_prep_value'):
+            return [self.lhs.output_field.get_prep_value(v) for v in self.rhs]
+        return self.rhs
+
     def process_rhs(self, compiler, connection):
         db_rhs = getattr(self.rhs, '_db', None)
         if db_rhs is not None and db_rhs != connection.alias:
@@ -335,6 +346,7 @@ class PatternLookup(BuiltinLookup):
 
 class Contains(PatternLookup):
     lookup_name = 'contains'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(Contains, self).process_rhs(qn, connection)
@@ -346,11 +358,13 @@ Field.register_lookup(Contains)
 
 class IContains(Contains):
     lookup_name = 'icontains'
+    prepare_rhs = False
 Field.register_lookup(IContains)
 
 
 class StartsWith(PatternLookup):
     lookup_name = 'startswith'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(StartsWith, self).process_rhs(qn, connection)
@@ -362,6 +376,7 @@ Field.register_lookup(StartsWith)
 
 class IStartsWith(PatternLookup):
     lookup_name = 'istartswith'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(IStartsWith, self).process_rhs(qn, connection)
@@ -373,6 +388,7 @@ Field.register_lookup(IStartsWith)
 
 class EndsWith(PatternLookup):
     lookup_name = 'endswith'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(EndsWith, self).process_rhs(qn, connection)
@@ -384,6 +400,7 @@ Field.register_lookup(EndsWith)
 
 class IEndsWith(PatternLookup):
     lookup_name = 'iendswith'
+    prepare_rhs = False
 
     def process_rhs(self, qn, connection):
         rhs, params = super(IEndsWith, self).process_rhs(qn, connection)
@@ -396,6 +413,11 @@ Field.register_lookup(IEndsWith)
 class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
     lookup_name = 'range'
 
+    def get_prep_lookup(self):
+        if hasattr(self.rhs, '_prepare'):
+            return self.rhs._prepare(self.lhs.output_field)
+        return [self.lhs.output_field.get_prep_value(v) for v in self.rhs]
+
     def get_rhs_op(self, connection, rhs):
         return "BETWEEN %s AND %s" % (rhs[0], rhs[1])
 
@@ -411,6 +433,7 @@ Field.register_lookup(Range)
 
 class IsNull(BuiltinLookup):
     lookup_name = 'isnull'
+    prepare_rhs = False
 
     def as_sql(self, compiler, connection):
         sql, params = compiler.compile(self.lhs)
@@ -423,6 +446,7 @@ Field.register_lookup(IsNull)
 
 class Search(BuiltinLookup):
     lookup_name = 'search'
+    prepare_rhs = False
 
     def as_sql(self, compiler, connection):
         warnings.warn(
@@ -438,6 +462,7 @@ Field.register_lookup(Search)
 
 class Regex(BuiltinLookup):
     lookup_name = 'regex'
+    prepare_rhs = False
 
     def as_sql(self, compiler, connection):
         if self.lookup_name in connection.operators:

+ 0 - 61
docs/howto/custom-model-fields.txt

@@ -577,67 +577,6 @@ the end. You should also update the model's attribute if you make any changes
 to the value so that code holding references to the model will always see the
 correct value.
 
-.. _preparing-values-for-use-in-database-lookups:
-
-Preparing values for use in database lookups
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-As with value conversions, preparing a value for database lookups is a
-two phase process.
-
-:meth:`.get_prep_lookup` performs the first phase of lookup preparation:
-type conversion and data validation.
-
-Prepares the ``value`` for passing to the database when used in a lookup (a
-``WHERE`` constraint in SQL). The ``lookup_type`` parameter will be one of the
-valid Django filter lookups: ``exact``, ``iexact``, ``contains``, ``icontains``,
-``gt``, ``gte``, ``lt``, ``lte``, ``in``, ``startswith``, ``istartswith``,
-``endswith``, ``iendswith``, ``range``, ``year``, ``month``, ``day``,
-``isnull``, ``search``, ``regex``, and ``iregex``.
-
-If you are using :doc:`custom lookups </howto/custom-lookups>`, the
-``lookup_type`` can be any ``lookup_name`` used by the project's custom lookups.
-
-Your method must be prepared to handle all of these ``lookup_type`` values and
-should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a
-list when you were expecting an object, for example) or a ``TypeError`` if
-your field does not support that type of lookup. For many fields, you can get
-by with handling the lookup types that need special handling for your field
-and pass the rest to the :meth:`~Field.get_db_prep_lookup` method of the parent
-class.
-
-If you needed to implement :meth:`.get_db_prep_save`, you will usually need to
-implement :meth:`.get_prep_lookup`. If you don't, :meth:`.get_prep_value` will
-be called by the default implementation, to manage ``exact``, ``gt``, ``gte``,
-``lt``, ``lte``, ``in`` and ``range`` lookups.
-
-You may also want to implement this method to limit the lookup types that could
-be used with your custom field type.
-
-Note that, for ``"range"`` and ``"in"`` lookups, ``get_prep_lookup`` will receive
-a list of objects (presumably of the right type) and will need to convert them
-to a list of things of the right type for passing to the database. Most of the
-time, you can reuse ``get_prep_value()``, or at least factor out some common
-pieces.
-
-For example, the following code implements ``get_prep_lookup`` to limit the
-accepted lookup types to ``exact`` and ``in``::
-
-    class HandField(models.Field):
-        # ...
-
-        def get_prep_lookup(self, lookup_type, value):
-            # We only handle 'exact' and 'in'. All others are errors.
-            if lookup_type == 'exact':
-                return self.get_prep_value(value)
-            elif lookup_type == 'in':
-                return [self.get_prep_value(v) for v in value]
-            else:
-                raise TypeError('Lookup type %r not supported.' % lookup_type)
-
-For performing database-specific data conversions required by a lookup,
-you can override :meth:`~Field.get_db_prep_lookup`.
-
 .. _specifying-form-field-for-model-field:
 
 Specifying the form field for a model field

+ 1 - 22
docs/ref/models/fields.txt

@@ -1717,8 +1717,7 @@ Field API reference
     ``Field`` is an abstract class that represents a database table column.
     Django uses fields to create the database table (:meth:`db_type`), to map
     Python types to database (:meth:`get_prep_value`) and vice-versa
-    (:meth:`from_db_value`), and to apply :doc:`/ref/models/lookups`
-    (:meth:`get_prep_lookup`).
+    (:meth:`from_db_value`).
 
     A field is thus a fundamental piece in different Django APIs, notably,
     :class:`models <django.db.models.Model>` and :class:`querysets
@@ -1847,26 +1846,6 @@ Field API reference
 
         See :ref:`preprocessing-values-before-saving` for usage.
 
-    When a lookup is used on a field, the value may need to be "prepared".
-    Django exposes two methods for this:
-
-    .. method:: get_prep_lookup(lookup_type, value)
-
-        Prepares ``value`` to the database prior to be used in a lookup.
-        The ``lookup_type`` will be the registered name of the lookup. For
-        example: ``"exact"``, ``"iexact"``, or ``"contains"``.
-
-        See :ref:`preparing-values-for-use-in-database-lookups` for usage.
-
-    .. method:: get_db_prep_lookup(lookup_type, value, connection, prepared=False)
-
-        Similar to :meth:`get_db_prep_value`, but for performing a lookup.
-
-        As with :meth:`get_db_prep_value`, the specific connection that will
-        be used for the query is passed as ``connection``. In addition,
-        ``prepared`` describes whether the value has already been prepared with
-        :meth:`get_prep_lookup`.
-
     Fields often receive their values as a different type, either from
     serialization or from forms.
 

+ 19 - 0
docs/releases/1.10.txt

@@ -677,6 +677,25 @@ You can check if your database has any of the removed hashers like this::
     # Unsalted MD5 passwords might not have an 'md5$$' prefix:
     User.objects.filter(password__length=32)
 
+``Field.get_prep_lookup()`` and ``Field.get_db_prep_lookup()`` methods are removed
+----------------------------------------------------------------------------------
+
+If you have a custom field that implements either of these methods, register a
+custom lookup for it. For example::
+
+    from django.db.models import Field
+    from django.db.models.lookups import Exact
+
+    class MyField(Field):
+        ...
+
+    class MyFieldExact(Exact):
+        def get_prep_lookup(self):
+            # do_custom_stuff_for_myfield
+            ....
+
+    MyField.register_lookup(MyFieldExact)
+
 :mod:`django.contrib.gis`
 -------------------------
 

+ 13 - 13
tests/model_fields/test_booleanfield.py

@@ -1,29 +1,29 @@
 from django.core.exceptions import ValidationError
-from django.db import IntegrityError, connection, models, transaction
+from django.db import IntegrityError, models, transaction
 from django.test import SimpleTestCase, TestCase
 
 from .models import BooleanModel, FksToBooleans, NullBooleanModel
 
 
 class BooleanFieldTests(TestCase):
-    def _test_get_db_prep_lookup(self, f):
-        self.assertEqual(f.get_db_prep_lookup('exact', True, connection=connection), [True])
-        self.assertEqual(f.get_db_prep_lookup('exact', '1', connection=connection), [True])
-        self.assertEqual(f.get_db_prep_lookup('exact', 1, connection=connection), [True])
-        self.assertEqual(f.get_db_prep_lookup('exact', False, connection=connection), [False])
-        self.assertEqual(f.get_db_prep_lookup('exact', '0', connection=connection), [False])
-        self.assertEqual(f.get_db_prep_lookup('exact', 0, connection=connection), [False])
-        self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
+    def _test_get_prep_value(self, f):
+        self.assertEqual(f.get_prep_value(True), True)
+        self.assertEqual(f.get_prep_value('1'), True)
+        self.assertEqual(f.get_prep_value(1), True)
+        self.assertEqual(f.get_prep_value(False), False)
+        self.assertEqual(f.get_prep_value('0'), False)
+        self.assertEqual(f.get_prep_value(0), False)
+        self.assertEqual(f.get_prep_value(None), None)
 
     def _test_to_python(self, f):
         self.assertIs(f.to_python(1), True)
         self.assertIs(f.to_python(0), False)
 
-    def test_booleanfield_get_db_prep_lookup(self):
-        self._test_get_db_prep_lookup(models.BooleanField())
+    def test_booleanfield_get_prep_value(self):
+        self._test_get_prep_value(models.BooleanField())
 
-    def test_nullbooleanfield_get_db_prep_lookup(self):
-        self._test_get_db_prep_lookup(models.NullBooleanField())
+    def test_nullbooleanfield_get_prep_value(self):
+        self._test_get_prep_value(models.NullBooleanField())
 
     def test_booleanfield_to_python(self):
         self._test_to_python(models.BooleanField())

+ 0 - 22
tests/model_fields/test_custom_fields.py

@@ -1,22 +0,0 @@
-from django.db import connection, models
-from django.test import SimpleTestCase
-
-
-class CustomFieldTests(SimpleTestCase):
-
-    def test_get_prep_value_count(self):
-        """
-        Field values are not prepared twice in get_db_prep_lookup() (#14786).
-        """
-        class NoopField(models.TextField):
-            def __init__(self, *args, **kwargs):
-                self.prep_value_count = 0
-                super(NoopField, self).__init__(*args, **kwargs)
-
-            def get_prep_value(self, value):
-                self.prep_value_count += 1
-                return super(NoopField, self).get_prep_value(value)
-
-        field = NoopField()
-        field.get_db_prep_lookup('exact', 'TEST', connection=connection, prepared=False)
-        self.assertEqual(field.prep_value_count, 1)

+ 4 - 3
tests/model_fields/test_decimalfield.py

@@ -2,7 +2,7 @@ from decimal import Decimal
 
 from django.core import validators
 from django.core.exceptions import ValidationError
-from django.db import connection, models
+from django.db import models
 from django.test import TestCase
 
 from .models import BigD, Foo
@@ -27,9 +27,10 @@ class DecimalFieldTests(TestCase):
         self.assertEqual(f._format(f.to_python('2.6')), '2.6')
         self.assertEqual(f._format(None), None)
 
-    def test_get_db_prep_lookup(self):
+    def test_get_prep_value(self):
         f = models.DecimalField(max_digits=5, decimal_places=1)
-        self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
+        self.assertEqual(f.get_prep_value(None), None)
+        self.assertEqual(f.get_prep_value('2.4'), Decimal('2.4'))
 
     def test_filter_with_strings(self):
         """

+ 2 - 3
tests/queries/tests.py

@@ -1229,9 +1229,8 @@ class Queries2Tests(TestCase):
         )
 
     def test_ticket12239(self):
-        # Float was being rounded to integer on gte queries on integer field.  Tests
-        # show that gt, lt, gte, and lte work as desired.  Note that the fix changes
-        # get_prep_lookup for gte and lt queries only.
+        # Custom lookups are registered to round float values correctly on gte
+        # and lt IntegerField queries.
         self.assertQuerysetEqual(
             Number.objects.filter(num__gt=11.9),
             ['<Number: 12>']