Fixed #23941 -- Removed implicit decimal formatting from expressions.

This commit is contained in:
Josh Smeaton 2014-12-01 17:11:23 +11:00 committed by Tim Graham
parent e2868308bf
commit 267a1dcd9b
9 changed files with 57 additions and 20 deletions

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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
===================== =====================

View File

@ -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'):

View File

@ -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):