Permalink
Browse files

Fixed #2365, #3324 -- Renamed FloatField to DecimalField and changed …

…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...
1 parent 03966f0 commit 92c35a0617836b09aef3b6909579ee368004969b @malcolmt malcolmt committed May 21, 2007
Showing with 3,579 additions and 108 deletions.
  1. +1 −1 AUTHORS
  2. +2 −2 django/contrib/admin/templatetags/admin_list.py
  3. +2 −1 django/contrib/admin/views/doc.py
  4. +4 −4 django/core/management.py
  5. +38 −4 django/core/serializers/json.py
  6. +18 −10 django/core/validators.py
  7. +2 −1 django/db/backends/ado_mssql/creation.py
  8. +3 −1 django/db/backends/mysql/base.py
  9. +2 −1 django/db/backends/mysql/creation.py
  10. +1 −1 django/db/backends/mysql/introspection.py
  11. +2 −0 django/db/backends/mysql_old/base.py
  12. +2 −1 django/db/backends/mysql_old/creation.py
  13. +1 −1 django/db/backends/mysql_old/introspection.py
  14. +2 −1 django/db/backends/oracle/creation.py
  15. +1 −1 django/db/backends/oracle/introspection.py
  16. +1 −0 django/db/backends/postgresql/base.py
  17. +2 −1 django/db/backends/postgresql/creation.py
  18. +2 −1 django/db/backends/postgresql/introspection.py
  19. +2 −1 django/db/backends/postgresql_psycopg2/introspection.py
  20. +7 −0 django/db/backends/sqlite3/base.py
  21. +2 −1 django/db/backends/sqlite3/creation.py
  22. +15 −0 django/db/backends/util.py
  23. +69 −4 django/db/models/fields/__init__.py
  24. +67 −1 django/newforms/fields.py
  25. +25 −5 django/oldforms/__init__.py
  26. +3,079 −0 django/utils/_decimal.py
  27. +4 −3 docs/forms.txt
  28. +28 −20 docs/model-api.txt
  29. +7 −1 docs/newforms.txt
  30. +5 −5 tests/modeltests/invalid_models/models.py
  31. +131 −0 tests/regressiontests/forms/tests.py
  32. +11 −5 tests/regressiontests/serializers_regress/models.py
  33. +41 −30 tests/regressiontests/serializers_regress/tests.py
View
@@ -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>
@@ -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
@@ -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:
@@ -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'),
@@ -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]
@@ -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):
@@ -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)):
@@ -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"
@@ -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)
+#
@@ -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)
@@ -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
@@ -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)',
@@ -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__
@@ -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
@@ -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)',
@@ -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',
@@ -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
@@ -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)',
@@ -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',
@@ -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)',
@@ -46,5 +46,5 @@ def get_indexes(cursor, table_name):
1114: 'DateTimeField',
1184: 'DateTimeField',
1266: 'TimeField',
- 1700: 'FloatField',
+ 1700: 'DecimalField',
}
@@ -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',
@@ -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',
@@ -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',
}
@@ -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',
}
@@ -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
@@ -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):
@@ -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)',
Oops, something went wrong.

0 comments on commit 92c35a0

Please sign in to comment.