Skip to content

Commit

Permalink
Fixed #2365, #3324 -- Renamed FloatField to DecimalField and changed …
Browse files Browse the repository at this point in the history
…the code

to return Decimal instances in Python for this field. Backwards incompatible
change.

Added a real FloatField (stores floats in the database) and support for
FloatField and DecimalField in newforms (analogous to IntegerField).

Included decimal.py module (as django.utils._decimal) from Python 2.4. This is
license compatible with Django and included for Python 2.3 compatibility only.

Large portions of this work are based on patches from Andy Durdin and Jorge
Gajon.



git-svn-id: http://code.djangoproject.com/svn/django/trunk@5302 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
mtredinnick committed May 21, 2007
1 parent 4c1c128 commit 7c07362
Show file tree
Hide file tree
Showing 33 changed files with 3,579 additions and 108 deletions.
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
people who have submitted patches, reported bugs, added translations, helped
answer newbie questions, and generally made Django that much better:

adurdin@gmail.com
alang@bright-green.com
Marty Alchin <gulopine@gamemusic.org>
Daniel Alves Barbosa de Oliveira Vaz <danielvaz@gmail.com>
Expand Down Expand Up @@ -90,6 +89,7 @@ answer newbie questions, and generally made Django that much better:
dne@mayonnaise.net
Maximillian Dornseif <md@hudora.de>
Jeremy Dunck <http://dunck.us/>
Andrew Durdin <adurdin@gmail.com>
Andy Dustman <farcepest@gmail.com>
Clint Ecker
enlight
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/admin/templatetags/admin_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ def items_for_result(cl, result):
# Booleans are special: We use images.
elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
result_repr = _boolean_icon(field_val)
# FloatFields are special: Zero-pad the decimals.
elif isinstance(f, models.FloatField):
# DecimalFields are special: Zero-pad the decimals.
elif isinstance(f, models.DecimalField):
if field_val is not None:
result_repr = ('%%.%sf' % f.decimal_places) % field_val
else:
Expand Down
3 changes: 2 additions & 1 deletion django/contrib/admin/views/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,11 @@ def get_return_data_type(func_name):
'CommaSeparatedIntegerField': _('Comma-separated integers'),
'DateField' : _('Date (without time)'),
'DateTimeField' : _('Date (with time)'),
'DecimalField' : _('Decimal number'),
'EmailField' : _('E-mail address'),
'FileField' : _('File path'),
'FilePathField' : _('File path'),
'FloatField' : _('Decimal number'),
'FloatField' : _('Floating point number'),
'ForeignKey' : _('Integer'),
'ImageField' : _('File path'),
'IntegerField' : _('Integer'),
Expand Down
8 changes: 4 additions & 4 deletions django/core/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ def inspectdb():
if field_type == 'CharField' and row[3]:
extra_params['maxlength'] = row[3]

if field_type == 'FloatField':
if field_type == 'DecimalField':
extra_params['max_digits'] = row[4]
extra_params['decimal_places'] = row[5]

Expand Down Expand Up @@ -945,11 +945,11 @@ def get_validation_errors(outfile, app=None):
e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name)
if isinstance(f, models.CharField) and f.maxlength in (None, 0):
e.add(opts, '"%s": CharFields require a "maxlength" attribute.' % f.name)
if isinstance(f, models.FloatField):
if isinstance(f, models.DecimalField):
if f.decimal_places is None:
e.add(opts, '"%s": FloatFields require a "decimal_places" attribute.' % f.name)
e.add(opts, '"%s": DecimalFields require a "decimal_places" attribute.' % f.name)
if f.max_digits is None:
e.add(opts, '"%s": FloatFields require a "max_digits" attribute.' % f.name)
e.add(opts, '"%s": DecimalFields require a "max_digits" attribute.' % f.name)
if isinstance(f, models.FileField) and not f.upload_to:
e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name)
if isinstance(f, models.ImageField):
Expand Down
42 changes: 38 additions & 4 deletions django/core/serializers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@

import datetime
from django.utils import simplejson
from django.utils.simplejson import decoder
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.python import Deserializer as PythonDeserializer
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
try:
import decimal
except ImportError:
from django.utils import _decimal as decimal # Python 2.3 fallback

class Serializer(PythonSerializer):
"""
Convert a queryset to JSON.
"""
def end_serialization(self):
simplejson.dump(self.objects, self.stream, cls=DateTimeAwareJSONEncoder, **self.options)
simplejson.dump(self.objects, self.stream, cls=DjangoJSONEncoder, **self.options)

def getvalue(self):
if callable(getattr(self.stream, 'getvalue', None)):
Expand All @@ -30,12 +35,13 @@ def Deserializer(stream_or_string, **options):
stream = StringIO(stream_or_string)
else:
stream = stream_or_string
#for obj in PythonDeserializer(simplejson.load(stream, cls=DjangoJSONDecoder)):
for obj in PythonDeserializer(simplejson.load(stream)):
yield obj

class DateTimeAwareJSONEncoder(simplejson.JSONEncoder):
class DjangoJSONEncoder(simplejson.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time types
JSONEncoder subclass that knows how to encode date/time and decimal types.
"""

DATE_FORMAT = "%Y-%m-%d"
Expand All @@ -48,5 +54,33 @@ def default(self, o):
return o.strftime(self.DATE_FORMAT)
elif isinstance(o, datetime.time):
return o.strftime(self.TIME_FORMAT)
elif isinstance(o, decimal.Decimal):
return str(o)
else:
return super(DateTimeAwareJSONEncoder, self).default(o)
return super(DjangoJSONEncoder, self).default(o)

# Older, deprecated class name (for backwards compatibility purposes).
DateTimeAwareJSONEncoder = DjangoJSONEncoder

## Our override for simplejson.JSONNumber, because we want to use decimals in
## preference to floats (we can convert decimal -> float when they stored, if
## needed, but cannot go the other way).
#def DjangoNumber(match, context):
# match = DjangoNumber.regex.match(match.string, *match.span())
# integer, frac, exp = match.groups()
# if exp:
# res = float(integer + (frac or '') + (exp or ''))
# elif frac:
# res = decimal.Decimal(integer + frac)
# else:
# res = int(integer)
# return res, None
#decoder.pattern(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?')(DjangoNumber)
#
#converters = decoder.ANYTHING[:]
#converters[-1] = DjangoNumber
#decoder.JSONScanner = decoder.Scanner(converters)
#
#class DjangoJSONDecoder(simplejson.JSONDecoder):
# _scanner = decoder.Scanner(converters)
#
28 changes: 18 additions & 10 deletions django/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$')
integer_re = re.compile(r'^-?\d+$')
ip4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE)
Expand Down Expand Up @@ -406,28 +407,35 @@ def __call__(self, field_data, all_data):
if val != int(val):
raise ValidationError, gettext("This value must be a power of %s.") % self.power_of

class IsValidFloat(object):
class IsValidDecimal(object):
def __init__(self, max_digits, decimal_places):
self.max_digits, self.decimal_places = max_digits, decimal_places

def __call__(self, field_data, all_data):
data = str(field_data)
try:
float(data)
except ValueError:
match = decimal_re.search(str(field_data))
if not match:
raise ValidationError, gettext("Please enter a valid decimal number.")
# Negative floats require more space to input.
max_allowed_length = data.startswith('-') and (self.max_digits + 2) or (self.max_digits + 1)
if len(data) > max_allowed_length:

digits = len(match.group('digits') or '')
decimals = len(match.group('decimals') or '')

if digits + decimals > self.max_digits:
raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.",
"Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits
if (not '.' in data and len(data) > (max_allowed_length - self.decimal_places - 1)) or ('.' in data and len(data) > (max_allowed_length - (self.decimal_places - len(data.split('.')[1])))):
if digits > (self.max_digits - self.decimal_places):
raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.",
"Please enter a valid decimal number with a whole part of at most %s digits.", str(self.max_digits-self.decimal_places)) % str(self.max_digits-self.decimal_places)
if '.' in data and len(data.split('.')[1]) > self.decimal_places:
if decimals > self.decimal_places:
raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.",
"Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places

def isValidFloat(field_data, all_data):
data = str(field_data)
try:
float(data)
except ValueError:
raise ValidationError, gettext("Please enter a valid floating point number.")

class HasAllowableSize(object):
"""
Checks that the file-upload field data is a certain size. min_size and
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/ado_mssql/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
'DateField': 'smalldatetime',
'DateTimeField': 'smalldatetime',
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FileField': 'varchar(100)',
'FilePathField': 'varchar(100)',
'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'double precision',
'ImageField': 'varchar(100)',
'IntegerField': 'int',
'IPAddressField': 'char(15)',
Expand Down
4 changes: 3 additions & 1 deletion django/db/backends/mysql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# lexicographic ordering in this check because then (1, 2, 1, 'gamma')
# inadvertently passes the version test.
version = Database.version_info
if (version < (1,2,1) or (version[:3] == (1, 2, 1) and
if (version < (1,2,1) or (version[:3] == (1, 2, 1) and
(len(version) < 5 or version[3] != 'final' or version[4] < 2))):
raise ImportError, "MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__

Expand All @@ -36,6 +36,8 @@
django_conversions = conversions.copy()
django_conversions.update({
FIELD_TYPE.TIME: util.typecast_time,
FIELD_TYPE.DECIMAL: util.typecast_decimal,
FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
})

# This should match the numerical portion of the version numbers (we can treat
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/mysql/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
'DateField': 'date',
'DateTimeField': 'datetime',
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FileField': 'varchar(100)',
'FilePathField': 'varchar(100)',
'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'double precision',
'ImageField': 'varchar(100)',
'IntegerField': 'integer',
'IPAddressField': 'char(15)',
Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/mysql/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_indexes(cursor, table_name):
DATA_TYPES_REVERSE = {
FIELD_TYPE.BLOB: 'TextField',
FIELD_TYPE.CHAR: 'CharField',
FIELD_TYPE.DECIMAL: 'FloatField',
FIELD_TYPE.DECIMAL: 'DecimalField',
FIELD_TYPE.DATE: 'DateField',
FIELD_TYPE.DATETIME: 'DateTimeField',
FIELD_TYPE.DOUBLE: 'FloatField',
Expand Down
2 changes: 2 additions & 0 deletions django/db/backends/mysql_old/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
FIELD_TYPE.DATETIME: util.typecast_timestamp,
FIELD_TYPE.DATE: util.typecast_date,
FIELD_TYPE.TIME: util.typecast_time,
FIELD_TYPE.DECIMAL: util.typecast_decimal,
FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
})

# This should match the numerical portion of the version numbers (we can treat
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/mysql_old/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
'DateField': 'date',
'DateTimeField': 'datetime',
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FileField': 'varchar(100)',
'FilePathField': 'varchar(100)',
'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'double precision',
'ImageField': 'varchar(100)',
'IntegerField': 'integer',
'IPAddressField': 'char(15)',
Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/mysql_old/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def get_indexes(cursor, table_name):
DATA_TYPES_REVERSE = {
FIELD_TYPE.BLOB: 'TextField',
FIELD_TYPE.CHAR: 'CharField',
FIELD_TYPE.DECIMAL: 'FloatField',
FIELD_TYPE.DECIMAL: 'DecimalField',
FIELD_TYPE.DATE: 'DateField',
FIELD_TYPE.DATETIME: 'DateTimeField',
FIELD_TYPE.DOUBLE: 'FloatField',
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/oracle/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
'CommaSeparatedIntegerField': 'varchar2(%(maxlength)s)',
'DateField': 'date',
'DateTimeField': 'date',
'DecimalField': 'number(%(max_digits)s, %(decimal_places)s)',
'FileField': 'varchar2(100)',
'FilePathField': 'varchar2(100)',
'FloatField': 'number(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'double precision',
'ImageField': 'varchar2(100)',
'IntegerField': 'integer',
'IPAddressField': 'char(15)',
Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/oracle/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ def get_indexes(cursor, table_name):
1114: 'DateTimeField',
1184: 'DateTimeField',
1266: 'TimeField',
1700: 'FloatField',
1700: 'DecimalField',
}
1 change: 1 addition & 0 deletions django/db/backends/postgresql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ def get_sql_sequence_reset(style, model_list):
Database.register_type(Database.new_type((1083,1266), "TIME", util.typecast_time))
Database.register_type(Database.new_type((1114,1184), "TIMESTAMP", util.typecast_timestamp))
Database.register_type(Database.new_type((16,), "BOOLEAN", util.typecast_boolean))
Database.register_type(Database.new_type((1700,), "NUMERIC", util.typecast_decimal))

OPERATOR_MAPPING = {
'exact': '= %s',
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/postgresql/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
'DateField': 'date',
'DateTimeField': 'timestamp with time zone',
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FileField': 'varchar(100)',
'FilePathField': 'varchar(100)',
'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'double precision',
'ImageField': 'varchar(100)',
'IntegerField': 'integer',
'IPAddressField': 'inet',
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/postgresql/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ def get_indexes(cursor, table_name):
21: 'SmallIntegerField',
23: 'IntegerField',
25: 'TextField',
701: 'FloatField',
869: 'IPAddressField',
1043: 'CharField',
1082: 'DateField',
1083: 'TimeField',
1114: 'DateTimeField',
1184: 'DateTimeField',
1266: 'TimeField',
1700: 'FloatField',
1700: 'DecimalField',
}
3 changes: 2 additions & 1 deletion django/db/backends/postgresql_psycopg2/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ def get_indexes(cursor, table_name):
21: 'SmallIntegerField',
23: 'IntegerField',
25: 'TextField',
701: 'FloatField',
869: 'IPAddressField',
1043: 'CharField',
1082: 'DateField',
1083: 'TimeField',
1114: 'DateTimeField',
1184: 'DateTimeField',
1266: 'TimeField',
1700: 'FloatField',
1700: 'DecimalField',
}
7 changes: 7 additions & 0 deletions django/db/backends/sqlite3/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
module = 'sqlite3'
raise ImproperlyConfigured, "Error loading %s module: %s" % (module, e)

try:
import decimal
except ImportError:
from django.utils import _decimal as decimal # for Python 2.3

DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError

Expand All @@ -26,6 +31,8 @@
Database.register_converter("datetime", util.typecast_timestamp)
Database.register_converter("timestamp", util.typecast_timestamp)
Database.register_converter("TIMESTAMP", util.typecast_timestamp)
Database.register_converter("decimal", util.typecast_decimal)
Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal)

def utf8rowFactory(cursor, row):
def utf8(s):
Expand Down
3 changes: 2 additions & 1 deletion django/db/backends/sqlite3/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
'DateField': 'date',
'DateTimeField': 'datetime',
'DecimalField': 'decimal',
'FileField': 'varchar(100)',
'FilePathField': 'varchar(100)',
'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
'FloatField': 'real',
'ImageField': 'varchar(100)',
'IntegerField': 'integer',
'IPAddressField': 'char(15)',
Expand Down
Loading

0 comments on commit 7c07362

Please sign in to comment.