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
expected by the backend driver for decimal (numeric) columns.
"""
if value is None:
return None
return utils.format_number(value, max_digits, decimal_places)
def year_lookup_bounds_for_date_field(self, value):

View File

@ -312,9 +312,7 @@ WHEN (new.%(col_name)s IS NULL)
return value
def convert_decimalfield_value(self, value, field):
if value is not None:
value = backend_utils.typecast_decimal(field.format_number(value))
return value
return backend_utils.typecast_decimal(field.format_number(value))
# cx_Oracle always returns datetime.datetime objects for
# DATE and TIMESTAMP columns, but Django wants to see a

View File

@ -277,9 +277,7 @@ class DatabaseOperations(BaseDatabaseOperations):
return converters
def convert_decimalfield_value(self, value, field):
if value is not None:
value = backend_utils.typecast_decimal(field.format_number(value))
return value
return backend_utils.typecast_decimal(field.format_number(value))
def convert_datefield_value(self, value, field):
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
decimal places.
"""
if value is None:
return None
if isinstance(value, decimal.Decimal):
context = decimal.getcontext().copy()
if max_digits is not None:
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:
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}".format(value)

View File

@ -255,7 +255,7 @@ class ExpressionNode(CombinableMixin):
elif internal_type.endswith('IntegerField'):
return int(value)
elif internal_type == 'DecimalField':
return backend_utils.typecast_decimal(field.format_number(value))
return backend_utils.typecast_decimal(value)
return value
def get_lookup(self, lookup):

View File

@ -1536,7 +1536,7 @@ class DecimalField(Field):
)
def _format(self, value):
if isinstance(value, six.string_types) or value is None:
if isinstance(value, six.string_types):
return value
else:
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
``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
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
``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
=====================

View File

@ -17,7 +17,7 @@ with warnings.catch_warnings(record=True) as w:
from django.test import TestCase
from django.test.utils import Approximate
from django.test.utils import CaptureQueriesContext
from django.utils import six
from django.utils import six, timezone
from django.utils.deprecation import RemovedInDjango20Warning
from .models import Author, Publisher, Book, Store
@ -689,6 +689,19 @@ class BaseAggregateTestCase(TestCase):
self.assertNotIn('order by', qstr)
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):
fixtures = ["aggregation.json"]
@ -755,8 +768,8 @@ class ComplexAggregateTestCase(TestCase):
self.assertEqual(b2.sums, 383.69)
b3 = Book.objects.annotate(sums=Sum(F('rating') + F('pages') + F('price'),
output_field=DecimalField(max_digits=6, decimal_places=2))).get(pk=4)
self.assertEqual(b3.sums, Decimal("383.69"))
output_field=DecimalField())).get(pk=4)
self.assertEqual(b3.sums, Approximate(Decimal("383.69"), places=2))
def test_complex_aggregations_require_kwarg(self):
with six.assertRaisesRegex(self, TypeError, 'Complex expressions require an alias'):

View File

@ -4,7 +4,7 @@ from __future__ import unicode_literals
import copy
import datetime
from decimal import Decimal
from decimal import Decimal, Rounded
import re
import threading
import unittest
@ -1059,6 +1059,22 @@ class BackendUtilTests(TestCase):
'0.1')
equal('0.1234567890', 12, 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):