Fixed #23941 -- Removed implicit decimal formatting from expressions.
This commit is contained in:
parent
e2868308bf
commit
267a1dcd9b
|
@ -1195,8 +1195,6 @@ class BaseDatabaseOperations(object):
|
||||||
Transform a decimal.Decimal value to an object compatible with what is
|
Transform a decimal.Decimal value to an object compatible with what is
|
||||||
expected by the backend driver for decimal (numeric) columns.
|
expected by the backend driver for decimal (numeric) columns.
|
||||||
"""
|
"""
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return utils.format_number(value, max_digits, decimal_places)
|
return utils.format_number(value, max_digits, decimal_places)
|
||||||
|
|
||||||
def year_lookup_bounds_for_date_field(self, value):
|
def year_lookup_bounds_for_date_field(self, value):
|
||||||
|
|
|
@ -312,9 +312,7 @@ WHEN (new.%(col_name)s IS NULL)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def convert_decimalfield_value(self, value, field):
|
def convert_decimalfield_value(self, value, field):
|
||||||
if value is not None:
|
return backend_utils.typecast_decimal(field.format_number(value))
|
||||||
value = backend_utils.typecast_decimal(field.format_number(value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
# cx_Oracle always returns datetime.datetime objects for
|
# cx_Oracle always returns datetime.datetime objects for
|
||||||
# DATE and TIMESTAMP columns, but Django wants to see a
|
# DATE and TIMESTAMP columns, but Django wants to see a
|
||||||
|
|
|
@ -277,9 +277,7 @@ class DatabaseOperations(BaseDatabaseOperations):
|
||||||
return converters
|
return converters
|
||||||
|
|
||||||
def convert_decimalfield_value(self, value, field):
|
def convert_decimalfield_value(self, value, field):
|
||||||
if value is not None:
|
return backend_utils.typecast_decimal(field.format_number(value))
|
||||||
value = backend_utils.typecast_decimal(field.format_number(value))
|
|
||||||
return value
|
|
||||||
|
|
||||||
def convert_datefield_value(self, value, field):
|
def convert_datefield_value(self, value, field):
|
||||||
if value is not None and not isinstance(value, datetime.date):
|
if value is not None and not isinstance(value, datetime.date):
|
||||||
|
|
|
@ -191,9 +191,18 @@ def format_number(value, max_digits, decimal_places):
|
||||||
Formats a number into a string with the requisite number of digits and
|
Formats a number into a string with the requisite number of digits and
|
||||||
decimal places.
|
decimal places.
|
||||||
"""
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
if isinstance(value, decimal.Decimal):
|
if isinstance(value, decimal.Decimal):
|
||||||
context = decimal.getcontext().copy()
|
context = decimal.getcontext().copy()
|
||||||
|
if max_digits is not None:
|
||||||
context.prec = max_digits
|
context.prec = max_digits
|
||||||
return "{0:f}".format(value.quantize(decimal.Decimal(".1") ** decimal_places, context=context))
|
if decimal_places is not None:
|
||||||
|
value = value.quantize(decimal.Decimal(".1") ** decimal_places, context=context)
|
||||||
else:
|
else:
|
||||||
|
context.traps[decimal.Rounded] = 1
|
||||||
|
value = context.create_decimal(value)
|
||||||
|
return "{:f}".format(value)
|
||||||
|
if decimal_places is not None:
|
||||||
return "%.*f" % (decimal_places, value)
|
return "%.*f" % (decimal_places, value)
|
||||||
|
return "{:f}".format(value)
|
||||||
|
|
|
@ -255,7 +255,7 @@ class ExpressionNode(CombinableMixin):
|
||||||
elif internal_type.endswith('IntegerField'):
|
elif internal_type.endswith('IntegerField'):
|
||||||
return int(value)
|
return int(value)
|
||||||
elif internal_type == 'DecimalField':
|
elif internal_type == 'DecimalField':
|
||||||
return backend_utils.typecast_decimal(field.format_number(value))
|
return backend_utils.typecast_decimal(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get_lookup(self, lookup):
|
def get_lookup(self, lookup):
|
||||||
|
|
|
@ -1536,7 +1536,7 @@ class DecimalField(Field):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _format(self, value):
|
def _format(self, value):
|
||||||
if isinstance(value, six.string_types) or value is None:
|
if isinstance(value, six.string_types):
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
return self.format_number(value)
|
return self.format_number(value)
|
||||||
|
|
|
@ -257,7 +257,10 @@ placeholder within the ``template``.
|
||||||
|
|
||||||
The ``output_field`` argument requires a model field instance, like
|
The ``output_field`` argument requires a model field instance, like
|
||||||
``IntegerField()`` or ``BooleanField()``, into which Django will load the value
|
``IntegerField()`` or ``BooleanField()``, into which Django will load the value
|
||||||
after it's retrieved from the database.
|
after it's retrieved from the database. Usually no arguments are needed when
|
||||||
|
instantiating the model field as any arguments relating to data validation
|
||||||
|
(``max_length``, ``max_digits``, etc.) will not be enforced on the expression's
|
||||||
|
output value.
|
||||||
|
|
||||||
Note that ``output_field`` is only required when Django is unable to determine
|
Note that ``output_field`` is only required when Django is unable to determine
|
||||||
what field type the result should be. Complex expressions that mix field types
|
what field type the result should be. Complex expressions that mix field types
|
||||||
|
@ -318,8 +321,10 @@ values into their corresponding database type.
|
||||||
|
|
||||||
The ``output_field`` argument should be a model field instance, like
|
The ``output_field`` argument should be a model field instance, like
|
||||||
``IntegerField()`` or ``BooleanField()``, into which Django will load the value
|
``IntegerField()`` or ``BooleanField()``, into which Django will load the value
|
||||||
after it's retrieved from the database.
|
after it's retrieved from the database. Usually no arguments are needed when
|
||||||
|
instantiating the model field as any arguments relating to data validation
|
||||||
|
(``max_length``, ``max_digits``, etc.) will not be enforced on the expression's
|
||||||
|
output value.
|
||||||
|
|
||||||
Technical Information
|
Technical Information
|
||||||
=====================
|
=====================
|
||||||
|
|
|
@ -17,7 +17,7 @@ with warnings.catch_warnings(record=True) as w:
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import Approximate
|
from django.test.utils import Approximate
|
||||||
from django.test.utils import CaptureQueriesContext
|
from django.test.utils import CaptureQueriesContext
|
||||||
from django.utils import six
|
from django.utils import six, timezone
|
||||||
from django.utils.deprecation import RemovedInDjango20Warning
|
from django.utils.deprecation import RemovedInDjango20Warning
|
||||||
|
|
||||||
from .models import Author, Publisher, Book, Store
|
from .models import Author, Publisher, Book, Store
|
||||||
|
@ -689,6 +689,19 @@ class BaseAggregateTestCase(TestCase):
|
||||||
self.assertNotIn('order by', qstr)
|
self.assertNotIn('order by', qstr)
|
||||||
self.assertEqual(qstr.count(' join '), 0)
|
self.assertEqual(qstr.count(' join '), 0)
|
||||||
|
|
||||||
|
def test_decimal_max_digits_has_no_effect(self):
|
||||||
|
Book.objects.all().delete()
|
||||||
|
a1 = Author.objects.first()
|
||||||
|
p1 = Publisher.objects.first()
|
||||||
|
thedate = timezone.now()
|
||||||
|
for i in range(10):
|
||||||
|
Book.objects.create(
|
||||||
|
isbn="abcde{}".format(i), name="none", pages=10, rating=4.0,
|
||||||
|
price=9999.98, contact=a1, publisher=p1, pubdate=thedate)
|
||||||
|
|
||||||
|
book = Book.objects.aggregate(price_sum=Sum('price'))
|
||||||
|
self.assertEqual(book['price_sum'], Decimal("99999.80"))
|
||||||
|
|
||||||
|
|
||||||
class ComplexAggregateTestCase(TestCase):
|
class ComplexAggregateTestCase(TestCase):
|
||||||
fixtures = ["aggregation.json"]
|
fixtures = ["aggregation.json"]
|
||||||
|
@ -755,8 +768,8 @@ class ComplexAggregateTestCase(TestCase):
|
||||||
self.assertEqual(b2.sums, 383.69)
|
self.assertEqual(b2.sums, 383.69)
|
||||||
|
|
||||||
b3 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
|
b3 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
|
||||||
output_field=DecimalField(max_digits=6, decimal_places=2))).get(pk=4)
|
output_field=DecimalField())).get(pk=4)
|
||||||
self.assertEqual(b3.sums, Decimal("383.69"))
|
self.assertEqual(b3.sums, Approximate(Decimal("383.69"), places=2))
|
||||||
|
|
||||||
def test_complex_aggregations_require_kwarg(self):
|
def test_complex_aggregations_require_kwarg(self):
|
||||||
with six.assertRaisesRegex(self, TypeError, 'Complex expressions require an alias'):
|
with six.assertRaisesRegex(self, TypeError, 'Complex expressions require an alias'):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal, Rounded
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -1059,6 +1059,22 @@ class BackendUtilTests(TestCase):
|
||||||
'0.1')
|
'0.1')
|
||||||
equal('0.1234567890', 12, 0,
|
equal('0.1234567890', 12, 0,
|
||||||
'0')
|
'0')
|
||||||
|
equal('0.1234567890', None, 0,
|
||||||
|
'0')
|
||||||
|
equal('1234567890.1234567890', None, 0,
|
||||||
|
'1234567890')
|
||||||
|
equal('1234567890.1234567890', None, 2,
|
||||||
|
'1234567890.12')
|
||||||
|
equal('0.1234', 5, None,
|
||||||
|
'0.1234')
|
||||||
|
equal('123.12', 5, None,
|
||||||
|
'123.12')
|
||||||
|
with self.assertRaises(Rounded):
|
||||||
|
equal('0.1234567890', 5, None,
|
||||||
|
'0.12346')
|
||||||
|
with self.assertRaises(Rounded):
|
||||||
|
equal('1234567890.1234', 5, None,
|
||||||
|
'1234600000')
|
||||||
|
|
||||||
|
|
||||||
class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase):
|
class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase):
|
||||||
|
|
Loading…
Reference in New Issue