Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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...
commit 92c35a0617836b09aef3b6909579ee368004969b 1 parent 03966f0
Malcolm Tredinnick authored May 21, 2007

Showing 33 changed files with 3,579 additions and 108 deletions. Show diff stats Hide diff stats

  1. 2  AUTHORS
  2. 4  django/contrib/admin/templatetags/admin_list.py
  3. 3  django/contrib/admin/views/doc.py
  4. 8  django/core/management.py
  5. 42  django/core/serializers/json.py
  6. 28  django/core/validators.py
  7. 3  django/db/backends/ado_mssql/creation.py
  8. 4  django/db/backends/mysql/base.py
  9. 3  django/db/backends/mysql/creation.py
  10. 2  django/db/backends/mysql/introspection.py
  11. 2  django/db/backends/mysql_old/base.py
  12. 3  django/db/backends/mysql_old/creation.py
  13. 2  django/db/backends/mysql_old/introspection.py
  14. 3  django/db/backends/oracle/creation.py
  15. 2  django/db/backends/oracle/introspection.py
  16. 1  django/db/backends/postgresql/base.py
  17. 3  django/db/backends/postgresql/creation.py
  18. 3  django/db/backends/postgresql/introspection.py
  19. 3  django/db/backends/postgresql_psycopg2/introspection.py
  20. 7  django/db/backends/sqlite3/base.py
  21. 3  django/db/backends/sqlite3/creation.py
  22. 15  django/db/backends/util.py
  23. 73  django/db/models/fields/__init__.py
  24. 68  django/newforms/fields.py
  25. 30  django/oldforms/__init__.py
  26. 3,079  django/utils/_decimal.py
  27. 7  docs/forms.txt
  28. 48  docs/model-api.txt
  29. 8  docs/newforms.txt
  30. 10  tests/modeltests/invalid_models/models.py
  31. 131  tests/regressiontests/forms/tests.py
  32. 16  tests/regressiontests/serializers_regress/models.py
  33. 71  tests/regressiontests/serializers_regress/tests.py
2  AUTHORS
@@ -41,7 +41,6 @@ And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
41 41
 people who have submitted patches, reported bugs, added translations, helped
42 42
 answer newbie questions, and generally made Django that much better:
43 43
 
44  
-    adurdin@gmail.com
45 44
     alang@bright-green.com
46 45
     Marty Alchin <gulopine@gamemusic.org>
47 46
     Daniel Alves Barbosa de Oliveira Vaz <danielvaz@gmail.com>
@@ -90,6 +89,7 @@ answer newbie questions, and generally made Django that much better:
90 89
     dne@mayonnaise.net
91 90
     Maximillian Dornseif <md@hudora.de>
92 91
     Jeremy Dunck <http://dunck.us/>
  92
+    Andrew Durdin <adurdin@gmail.com>
93 93
     Andy Dustman <farcepest@gmail.com>
94 94
     Clint Ecker
95 95
     enlight
4  django/contrib/admin/templatetags/admin_list.py
@@ -166,8 +166,8 @@ def items_for_result(cl, result):
166 166
             # Booleans are special: We use images.
167 167
             elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
168 168
                 result_repr = _boolean_icon(field_val)
169  
-            # FloatFields are special: Zero-pad the decimals.
170  
-            elif isinstance(f, models.FloatField):
  169
+            # DecimalFields are special: Zero-pad the decimals.
  170
+            elif isinstance(f, models.DecimalField):
171 171
                 if field_val is not None:
172 172
                     result_repr = ('%%.%sf' % f.decimal_places) % field_val
173 173
                 else:
3  django/contrib/admin/views/doc.py
@@ -294,10 +294,11 @@ def get_return_data_type(func_name):
294 294
     'CommaSeparatedIntegerField': _('Comma-separated integers'),
295 295
     'DateField'                 : _('Date (without time)'),
296 296
     'DateTimeField'             : _('Date (with time)'),
  297
+    'DecimalField'              : _('Decimal number'),
297 298
     'EmailField'                : _('E-mail address'),
298 299
     'FileField'                 : _('File path'),
299 300
     'FilePathField'             : _('File path'),
300  
-    'FloatField'                : _('Decimal number'),
  301
+    'FloatField'                : _('Floating point number'),
301 302
     'ForeignKey'                : _('Integer'),
302 303
     'ImageField'                : _('File path'),
303 304
     'IntegerField'              : _('Integer'),
8  django/core/management.py
@@ -870,7 +870,7 @@ def inspectdb():
870 870
                 if field_type == 'CharField' and row[3]:
871 871
                     extra_params['maxlength'] = row[3]
872 872
 
873  
-                if field_type == 'FloatField':
  873
+                if field_type == 'DecimalField':
874 874
                     extra_params['max_digits'] = row[4]
875 875
                     extra_params['decimal_places'] = row[5]
876 876
 
@@ -945,11 +945,11 @@ def get_validation_errors(outfile, app=None):
945 945
                 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)
946 946
             if isinstance(f, models.CharField) and f.maxlength in (None, 0):
947 947
                 e.add(opts, '"%s": CharFields require a "maxlength" attribute.' % f.name)
948  
-            if isinstance(f, models.FloatField):
  948
+            if isinstance(f, models.DecimalField):
949 949
                 if f.decimal_places is None:
950  
-                    e.add(opts, '"%s": FloatFields require a "decimal_places" attribute.' % f.name)
  950
+                    e.add(opts, '"%s": DecimalFields require a "decimal_places" attribute.' % f.name)
951 951
                 if f.max_digits is None:
952  
-                    e.add(opts, '"%s": FloatFields require a "max_digits" attribute.' % f.name)
  952
+                    e.add(opts, '"%s": DecimalFields require a "max_digits" attribute.' % f.name)
953 953
             if isinstance(f, models.FileField) and not f.upload_to:
954 954
                 e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name)
955 955
             if isinstance(f, models.ImageField):
42  django/core/serializers/json.py
@@ -4,19 +4,24 @@
4 4
 
5 5
 import datetime
6 6
 from django.utils import simplejson
  7
+from django.utils.simplejson import decoder
7 8
 from django.core.serializers.python import Serializer as PythonSerializer
8 9
 from django.core.serializers.python import Deserializer as PythonDeserializer
9 10
 try:
10 11
     from cStringIO import StringIO
11 12
 except ImportError:
12 13
     from StringIO import StringIO
  14
+try:
  15
+    import decimal
  16
+except ImportError:
  17
+    from django.utils import _decimal as decimal    # Python 2.3 fallback
13 18
 
14 19
 class Serializer(PythonSerializer):
15 20
     """
16 21
     Convert a queryset to JSON.
17 22
     """
18 23
     def end_serialization(self):
19  
-        simplejson.dump(self.objects, self.stream, cls=DateTimeAwareJSONEncoder, **self.options)
  24
+        simplejson.dump(self.objects, self.stream, cls=DjangoJSONEncoder, **self.options)
20 25
 
21 26
     def getvalue(self):
22 27
         if callable(getattr(self.stream, 'getvalue', None)):
@@ -30,12 +35,13 @@ def Deserializer(stream_or_string, **options):
30 35
         stream = StringIO(stream_or_string)
31 36
     else:
32 37
         stream = stream_or_string
  38
+    #for obj in PythonDeserializer(simplejson.load(stream, cls=DjangoJSONDecoder)):
33 39
     for obj in PythonDeserializer(simplejson.load(stream)):
34 40
         yield obj
35 41
 
36  
-class DateTimeAwareJSONEncoder(simplejson.JSONEncoder):
  42
+class DjangoJSONEncoder(simplejson.JSONEncoder):
37 43
     """
38  
-    JSONEncoder subclass that knows how to encode date/time types
  44
+    JSONEncoder subclass that knows how to encode date/time and decimal types.
39 45
     """
40 46
 
41 47
     DATE_FORMAT = "%Y-%m-%d"
@@ -48,5 +54,33 @@ def default(self, o):
48 54
             return o.strftime(self.DATE_FORMAT)
49 55
         elif isinstance(o, datetime.time):
50 56
             return o.strftime(self.TIME_FORMAT)
  57
+        elif isinstance(o, decimal.Decimal):
  58
+            return str(o)
51 59
         else:
52  
-            return super(DateTimeAwareJSONEncoder, self).default(o)
  60
+            return super(DjangoJSONEncoder, self).default(o)
  61
+
  62
+# Older, deprecated class name (for backwards compatibility purposes).
  63
+DateTimeAwareJSONEncoder = DjangoJSONEncoder
  64
+
  65
+## Our override for simplejson.JSONNumber, because we want to use decimals in
  66
+## preference to floats (we can convert decimal -> float when they stored, if
  67
+## needed, but cannot go the other way).
  68
+#def DjangoNumber(match, context):
  69
+#    match = DjangoNumber.regex.match(match.string, *match.span())
  70
+#    integer, frac, exp = match.groups()
  71
+#    if exp:
  72
+#        res = float(integer + (frac or '') + (exp or ''))
  73
+#    elif frac:
  74
+#        res = decimal.Decimal(integer + frac)
  75
+#    else:
  76
+#        res = int(integer)
  77
+#    return res, None
  78
+#decoder.pattern(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?')(DjangoNumber)
  79
+#
  80
+#converters = decoder.ANYTHING[:]
  81
+#converters[-1] = DjangoNumber
  82
+#decoder.JSONScanner = decoder.Scanner(converters)
  83
+#
  84
+#class DjangoJSONDecoder(simplejson.JSONDecoder):
  85
+#    _scanner = decoder.Scanner(converters)
  86
+#
28  django/core/validators.py
@@ -25,6 +25,7 @@
25 25
     r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
26 26
     r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
27 27
     r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
  28
+decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$')
28 29
 integer_re = re.compile(r'^-?\d+$')
29 30
 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}$')
30 31
 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):
406 407
         if val != int(val):
407 408
             raise ValidationError, gettext("This value must be a power of %s.") % self.power_of
408 409
 
409  
-class IsValidFloat(object):
  410
+class IsValidDecimal(object):
410 411
     def __init__(self, max_digits, decimal_places):
411 412
         self.max_digits, self.decimal_places = max_digits, decimal_places
412 413
 
413 414
     def __call__(self, field_data, all_data):
414  
-        data = str(field_data)
415  
-        try:
416  
-            float(data)
417  
-        except ValueError:
  415
+        match = decimal_re.search(str(field_data))
  416
+        if not match:
418 417
             raise ValidationError, gettext("Please enter a valid decimal number.")
419  
-        # Negative floats require more space to input.
420  
-        max_allowed_length = data.startswith('-') and (self.max_digits + 2) or (self.max_digits + 1)
421  
-        if len(data) > max_allowed_length:
  418
+        
  419
+        digits = len(match.group('digits') or '')
  420
+        decimals = len(match.group('decimals') or '')
  421
+        
  422
+        if digits + decimals > self.max_digits:
422 423
             raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.",
423 424
                 "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits
424  
-        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])))):
  425
+        if digits > (self.max_digits - self.decimal_places):
425 426
             raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.",
426 427
                 "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)
427  
-        if '.' in data and len(data.split('.')[1]) > self.decimal_places:
  428
+        if decimals > self.decimal_places:
428 429
             raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.",
429 430
                 "Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places
430 431
 
  432
+def isValidFloat(field_data, all_data):
  433
+    data = str(field_data)
  434
+    try:
  435
+        float(data)
  436
+    except ValueError:
  437
+        raise ValidationError, gettext("Please enter a valid floating point number.")
  438
+
431 439
 class HasAllowableSize(object):
432 440
     """
433 441
     Checks that the file-upload field data is a certain size. min_size and
3  django/db/backends/ado_mssql/creation.py
@@ -5,9 +5,10 @@
5 5
     'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
6 6
     'DateField':         'smalldatetime',
7 7
     'DateTimeField':     'smalldatetime',
  8
+    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)',
8 9
     'FileField':         'varchar(100)',
9 10
     'FilePathField':     'varchar(100)',
10  
-    'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
  11
+    'FloatField':        'double precision',
11 12
     'ImageField':        'varchar(100)',
12 13
     'IntegerField':      'int',
13 14
     'IPAddressField':    'char(15)',
4  django/db/backends/mysql/base.py
@@ -15,7 +15,7 @@
15 15
 # lexicographic ordering in this check because then (1, 2, 1, 'gamma')
16 16
 # inadvertently passes the version test.
17 17
 version = Database.version_info
18  
-if (version < (1,2,1) or (version[:3] == (1, 2, 1) and 
  18
+if (version < (1,2,1) or (version[:3] == (1, 2, 1) and
19 19
         (len(version) < 5 or version[3] != 'final' or version[4] < 2))):
20 20
     raise ImportError, "MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__
21 21
 
@@ -36,6 +36,8 @@
36 36
 django_conversions = conversions.copy()
37 37
 django_conversions.update({
38 38
     FIELD_TYPE.TIME: util.typecast_time,
  39
+    FIELD_TYPE.DECIMAL: util.typecast_decimal,
  40
+    FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
39 41
 })
40 42
 
41 43
 # This should match the numerical portion of the version numbers (we can treat
3  django/db/backends/mysql/creation.py
@@ -9,9 +9,10 @@
9 9
     'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
10 10
     'DateField':         'date',
11 11
     'DateTimeField':     'datetime',
  12
+    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)',
12 13
     'FileField':         'varchar(100)',
13 14
     'FilePathField':     'varchar(100)',
14  
-    'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
  15
+    'FloatField':        'double precision',
15 16
     'ImageField':        'varchar(100)',
16 17
     'IntegerField':      'integer',
17 18
     'IPAddressField':    'char(15)',
2  django/db/backends/mysql/introspection.py
@@ -76,7 +76,7 @@ def get_indexes(cursor, table_name):
76 76
 DATA_TYPES_REVERSE = {
77 77
     FIELD_TYPE.BLOB: 'TextField',
78 78
     FIELD_TYPE.CHAR: 'CharField',
79  
-    FIELD_TYPE.DECIMAL: 'FloatField',
  79
+    FIELD_TYPE.DECIMAL: 'DecimalField',
80 80
     FIELD_TYPE.DATE: 'DateField',
81 81
     FIELD_TYPE.DATETIME: 'DateTimeField',
82 82
     FIELD_TYPE.DOUBLE: 'FloatField',
2  django/db/backends/mysql_old/base.py
@@ -24,6 +24,8 @@
24 24
     FIELD_TYPE.DATETIME: util.typecast_timestamp,
25 25
     FIELD_TYPE.DATE: util.typecast_date,
26 26
     FIELD_TYPE.TIME: util.typecast_time,
  27
+    FIELD_TYPE.DECIMAL: util.typecast_decimal,
  28
+    FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
27 29
 })
28 30
 
29 31
 # This should match the numerical portion of the version numbers (we can treat
3  django/db/backends/mysql_old/creation.py
@@ -9,9 +9,10 @@
9 9
     'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
10 10
     'DateField':         'date',
11 11
     'DateTimeField':     'datetime',
  12
+    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)',
12 13
     'FileField':         'varchar(100)',
13 14
     'FilePathField':     'varchar(100)',
14  
-    'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
  15
+    'FloatField':        'double precision',
15 16
     'ImageField':        'varchar(100)',
16 17
     'IntegerField':      'integer',
17 18
     'IPAddressField':    'char(15)',
2  django/db/backends/mysql_old/introspection.py
@@ -76,7 +76,7 @@ def get_indexes(cursor, table_name):
76 76
 DATA_TYPES_REVERSE = {
77 77
     FIELD_TYPE.BLOB: 'TextField',
78 78
     FIELD_TYPE.CHAR: 'CharField',
79  
-    FIELD_TYPE.DECIMAL: 'FloatField',
  79
+    FIELD_TYPE.DECIMAL: 'DecimalField',
80 80
     FIELD_TYPE.DATE: 'DateField',
81 81
     FIELD_TYPE.DATETIME: 'DateTimeField',
82 82
     FIELD_TYPE.DOUBLE: 'FloatField',
3  django/db/backends/oracle/creation.py
@@ -5,9 +5,10 @@
5 5
     'CommaSeparatedIntegerField': 'varchar2(%(maxlength)s)',
6 6
     'DateField':         'date',
7 7
     'DateTimeField':     'date',
  8
+    'DecimalField':      'number(%(max_digits)s, %(decimal_places)s)',
8 9
     'FileField':         'varchar2(100)',
9 10
     'FilePathField':     'varchar2(100)',
10  
-    'FloatField':        'number(%(max_digits)s, %(decimal_places)s)',
  11
+    'FloatField':        'double precision',
11 12
     'ImageField':        'varchar2(100)',
12 13
     'IntegerField':      'integer',
13 14
     'IPAddressField':    'char(15)',
2  django/db/backends/oracle/introspection.py
@@ -46,5 +46,5 @@ def get_indexes(cursor, table_name):
46 46
     1114: 'DateTimeField',
47 47
     1184: 'DateTimeField',
48 48
     1266: 'TimeField',
49  
-    1700: 'FloatField',
  49
+    1700: 'DecimalField',
50 50
 }
1  django/db/backends/postgresql/base.py
@@ -249,6 +249,7 @@ def get_sql_sequence_reset(style, model_list):
249 249
 Database.register_type(Database.new_type((1083,1266), "TIME", util.typecast_time))
250 250
 Database.register_type(Database.new_type((1114,1184), "TIMESTAMP", util.typecast_timestamp))
251 251
 Database.register_type(Database.new_type((16,), "BOOLEAN", util.typecast_boolean))
  252
+Database.register_type(Database.new_type((1700,), "NUMERIC", util.typecast_decimal))
252 253
 
253 254
 OPERATOR_MAPPING = {
254 255
     'exact': '= %s',
3  django/db/backends/postgresql/creation.py
@@ -9,9 +9,10 @@
9 9
     'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
10 10
     'DateField':         'date',
11 11
     'DateTimeField':     'timestamp with time zone',
  12
+    'DecimalField':      'numeric(%(max_digits)s, %(decimal_places)s)',
12 13
     'FileField':         'varchar(100)',
13 14
     'FilePathField':     'varchar(100)',
14  
-    'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
  15
+    'FloatField':        'double precision',
15 16
     'ImageField':        'varchar(100)',
16 17
     'IntegerField':      'integer',
17 18
     'IPAddressField':    'inet',
3  django/db/backends/postgresql/introspection.py
@@ -72,6 +72,7 @@ def get_indexes(cursor, table_name):
72 72
     21: 'SmallIntegerField',
73 73
     23: 'IntegerField',
74 74
     25: 'TextField',
  75
+    701: 'FloatField',
75 76
     869: 'IPAddressField',
76 77
     1043: 'CharField',
77 78
     1082: 'DateField',
@@ -79,5 +80,5 @@ def get_indexes(cursor, table_name):
79 80
     1114: 'DateTimeField',
80 81
     1184: 'DateTimeField',
81 82
     1266: 'TimeField',
82  
-    1700: 'FloatField',
  83
+    1700: 'DecimalField',
83 84
 }
3  django/db/backends/postgresql_psycopg2/introspection.py
@@ -72,6 +72,7 @@ def get_indexes(cursor, table_name):
72 72
     21: 'SmallIntegerField',
73 73
     23: 'IntegerField',
74 74
     25: 'TextField',
  75
+    701: 'FloatField',
75 76
     869: 'IPAddressField',
76 77
     1043: 'CharField',
77 78
     1082: 'DateField',
@@ -79,5 +80,5 @@ def get_indexes(cursor, table_name):
79 80
     1114: 'DateTimeField',
80 81
     1184: 'DateTimeField',
81 82
     1266: 'TimeField',
82  
-    1700: 'FloatField',
  83
+    1700: 'DecimalField',
83 84
 }
7  django/db/backends/sqlite3/base.py
@@ -17,6 +17,11 @@
17 17
         module = 'sqlite3'
18 18
     raise ImproperlyConfigured, "Error loading %s module: %s" % (module, e)
19 19
 
  20
+try:
  21
+    import decimal
  22
+except ImportError:
  23
+    from django.utils import _decimal as decimal # for Python 2.3
  24
+
20 25
 DatabaseError = Database.DatabaseError
21 26
 IntegrityError = Database.IntegrityError
22 27
 
@@ -26,6 +31,8 @@
26 31
 Database.register_converter("datetime", util.typecast_timestamp)
27 32
 Database.register_converter("timestamp", util.typecast_timestamp)
28 33
 Database.register_converter("TIMESTAMP", util.typecast_timestamp)
  34
+Database.register_converter("decimal", util.typecast_decimal)
  35
+Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal)
29 36
 
30 37
 def utf8rowFactory(cursor, row):
31 38
     def utf8(s):
3  django/db/backends/sqlite3/creation.py
@@ -8,9 +8,10 @@
8 8
     'CommaSeparatedIntegerField':   'varchar(%(maxlength)s)',
9 9
     'DateField':                    'date',
10 10
     'DateTimeField':                'datetime',
  11
+    'DecimalField':                 'decimal',
11 12
     'FileField':                    'varchar(100)',
12 13
     'FilePathField':                'varchar(100)',
13  
-    'FloatField':                   'numeric(%(max_digits)s, %(decimal_places)s)',
  14
+    'FloatField':                   'real',
14 15
     'ImageField':                   'varchar(100)',
15 16
     'IntegerField':                 'integer',
16 17
     'IPAddressField':               'char(15)',
15  django/db/backends/util.py
... ...
@@ -1,6 +1,11 @@
1 1
 import datetime
2 2
 from time import time
3 3
 
  4
+try:
  5
+    import decimal
  6
+except ImportError:
  7
+    from django.utils import _decimal as decimal    # for Python 2.3
  8
+
4 9
 class CursorDebugWrapper(object):
5 10
     def __init__(self, cursor, db):
6 11
         self.cursor = cursor
@@ -85,6 +90,11 @@ def typecast_boolean(s):
85 90
     if not s: return False
86 91
     return str(s)[0].lower() == 't'
87 92
 
  93
+def typecast_decimal(s):
  94
+    if s is None:
  95
+        return None
  96
+    return decimal.Decimal(s)
  97
+
88 98
 ###############################################
89 99
 # Converters from Python to database (string) #
90 100
 ###############################################
@@ -92,6 +102,11 @@ def typecast_boolean(s):
92 102
 def rev_typecast_boolean(obj, d):
93 103
     return obj and '1' or '0'
94 104
 
  105
+def rev_typecast_decimal(d):
  106
+    if d is None:
  107
+        return None
  108
+    return str(d)
  109
+
95 110
 ##################################################################################
96 111
 # Helper functions for dictfetch* for databases that don't natively support them #
97 112
 ##################################################################################
73  django/db/models/fields/__init__.py
@@ -10,6 +10,10 @@
10 10
 from django.utils.text import capfirst
11 11
 from django.utils.translation import gettext, gettext_lazy
12 12
 import datetime, os, time
  13
+try:
  14
+    import decimal
  15
+except ImportError:
  16
+    from django.utils import _decimal as decimal    # for Python 2.3
13 17
 
14 18
 class NOT_PROVIDED:
15 19
     pass
@@ -573,6 +577,65 @@ def formfield(self, **kwargs):
573 577
         defaults.update(kwargs)
574 578
         return super(DateTimeField, self).formfield(**defaults)
575 579
 
  580
+class DecimalField(Field):
  581
+    empty_strings_allowed = False
  582
+    def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
  583
+        self.max_digits, self.decimal_places = max_digits, decimal_places
  584
+        Field.__init__(self, verbose_name, name, **kwargs)
  585
+
  586
+    def to_python(self, value):
  587
+        if value is None:
  588
+            return value
  589
+        try:
  590
+            return decimal.Decimal(value)
  591
+        except decimal.InvalidOperation:
  592
+            raise validators.ValidationError, gettext("This value must be a decimal number.")
  593
+
  594
+    def _format(self, value):
  595
+        if isinstance(value, basestring):
  596
+            return value
  597
+        else:
  598
+            return self.format_number(value)
  599
+
  600
+    def format_number(self, value):
  601
+        """
  602
+        Formats a number into a string with the requisite number of digits and
  603
+        decimal places.
  604
+        """
  605
+        num_chars = self.max_digits
  606
+        # Allow for a decimal point
  607
+        if self.decimal_places > 0:
  608
+            num_chars += 1
  609
+        # Allow for a minus sign
  610
+        if value < 0:
  611
+            num_chars += 1
  612
+
  613
+        return "%.*f" % (self.decimal_places, value)
  614
+
  615
+    def get_db_prep_save(self, value):
  616
+        if value is not None:
  617
+            value = self._format(value)
  618
+        return super(DecimalField, self).get_db_prep_save(value)
  619
+
  620
+    def get_db_prep_lookup(self, lookup_type, value):
  621
+        if lookup_type == 'range':
  622
+            value = [self._format(v) for v in value]
  623
+        else:
  624
+            value = self._format(value)
  625
+        return super(DecimalField, self).get_db_prep_lookup(lookup_type, value)
  626
+
  627
+    def get_manipulator_field_objs(self):
  628
+        return [curry(oldforms.DecimalField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
  629
+
  630
+    def formfield(self, **kwargs):
  631
+        defaults = {
  632
+            'max_digits': self.max_digits,
  633
+            'decimal_places': self.decimal_places,
  634
+            'form_class': forms.DecimalField,
  635
+        }
  636
+        defaults.update(kwargs)
  637
+        return super(DecimalField, self).formfield(**defaults)
  638
+
576 639
 class EmailField(CharField):
577 640
     def __init__(self, *args, **kwargs):
578 641
         kwargs['maxlength'] = 75
@@ -683,12 +746,14 @@ def get_manipulator_field_objs(self):
683 746
 
684 747
 class FloatField(Field):
685 748
     empty_strings_allowed = False
686  
-    def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
687  
-        self.max_digits, self.decimal_places = max_digits, decimal_places
688  
-        Field.__init__(self, verbose_name, name, **kwargs)
689 749
 
690 750
     def get_manipulator_field_objs(self):
691  
-        return [curry(oldforms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
  751
+        return [oldforms.FloatField]
  752
+
  753
+    def formfield(self, **kwargs):
  754
+        defaults = {'form_class': forms.FloatField}
  755
+        defaults.update(kwargs)
  756
+        return super(FloatField, self).formfield(**defaults)
692 757
 
693 758
 class ImageField(FileField):
694 759
     def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
68  django/newforms/fields.py
@@ -19,7 +19,7 @@
19 19
     'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
20 20
     'RegexField', 'EmailField', 'URLField', 'BooleanField',
21 21
     'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
22  
-    'ComboField', 'MultiValueField',
  22
+    'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
23 23
     'SplitDateTimeField',
24 24
 )
25 25
 
@@ -31,6 +31,11 @@
31 31
 except NameError:
32 32
     from sets import Set as set # Python 2.3 fallback
33 33
 
  34
+try:
  35
+    from decimal import Decimal
  36
+except ImportError:
  37
+    from django.utils._decimal import Decimal   # Python 2.3 fallback
  38
+
34 39
 class Field(object):
35 40
     widget = TextInput # Default widget to use when rendering this type of Field.
36 41
     hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
@@ -134,6 +139,67 @@ def clean(self, value):
134 139
             raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value)
135 140
         return value
136 141
 
  142
+class FloatField(Field):
  143
+    def __init__(self, max_value=None, min_value=None, *args, **kwargs):
  144
+        self.max_value, self.min_value = max_value, min_value
  145
+        Field.__init__(self, *args, **kwargs)
  146
+
  147
+    def clean(self, value):
  148
+        """
  149
+        Validates that float() can be called on the input. Returns a float.
  150
+        Returns None for empty values.
  151
+        """
  152
+        super(FloatField, self).clean(value)
  153
+        if not self.required and value in EMPTY_VALUES:
  154
+            return None
  155
+        try:
  156
+            value = float(value)
  157
+        except (ValueError, TypeError):
  158
+            raise ValidationError(gettext('Enter a number.'))
  159
+        if self.max_value is not None and value > self.max_value:
  160
+            raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value)
  161
+        if self.min_value is not None and value < self.min_value:
  162
+            raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value)
  163
+        return value
  164
+
  165
+decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$')
  166
+
  167
+class DecimalField(Field):
  168
+    def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
  169
+        self.max_value, self.min_value = max_value, min_value
  170
+        self.max_digits, self.decimal_places = max_digits, decimal_places
  171
+        Field.__init__(self, *args, **kwargs)
  172
+
  173
+    def clean(self, value):
  174
+        """
  175
+        Validates that the input is a decimal number. Returns a Decimal
  176
+        instance. Returns None for empty values. Ensures that there are no more
  177
+        than max_digits in the number, and no more than decimal_places digits
  178
+        after the decimal point.
  179
+        """
  180
+        super(DecimalField, self).clean(value)
  181
+        if not self.required and value in EMPTY_VALUES:
  182
+            return None
  183
+        value = value.strip()
  184
+        match = decimal_re.search(value)
  185
+        if not match:
  186
+            raise ValidationError(gettext('Enter a number.'))
  187
+        else:
  188
+            value = Decimal(value)
  189
+        digits = len(match.group('digits') or '')
  190
+        decimals = len(match.group('decimals') or '')
  191
+        if self.max_value is not None and value > self.max_value:
  192
+            raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value)
  193
+        if self.min_value is not None and value < self.min_value:
  194
+            raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value)
  195
+        if self.max_digits is not None and (digits + decimals) > self.max_digits:
  196
+            raise ValidationError(gettext('Ensure that there are no more than %s digits in total.') % self.max_digits)
  197
+        if self.decimal_places is not None and decimals > self.decimal_places:
  198
+            raise ValidationError(gettext('Ensure that there are no more than %s decimal places.') % self.decimal_places)
  199
+        if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places):
  200
+            raise ValidationError(gettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places))
  201
+        return value
  202
+
137 203
 DEFAULT_DATE_INPUT_FORMATS = (
138 204
     '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
139 205
     '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006'
30  django/oldforms/__init__.py
@@ -750,14 +750,27 @@ def isPositiveSmall(self, field_data, all_data):
750 750
             raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.")
751 751
 
752 752
 class FloatField(TextField):
  753
+    def __init__(self, field_name, is_required=False, validator_list=None): 
  754
+        if validator_list is None: validator_list = [] 
  755
+        validator_list = [validators.isValidFloat] + validator_list 
  756
+        TextField.__init__(self, field_name, is_required=is_required, validator_list=validator_list) 
  757
+ 
  758
+    def html2python(data): 
  759
+        if data == '' or data is None: 
  760
+            return None 
  761
+        return float(data) 
  762
+    html2python = staticmethod(html2python) 
  763
+ 
  764
+class DecimalField(TextField): 
753 765
     def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None):
754 766
         if validator_list is None: validator_list = []
755 767
         self.max_digits, self.decimal_places = max_digits, decimal_places
756  
-        validator_list = [self.isValidFloat] + validator_list
757  
-        TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list)
  768
+        validator_list = [self.isValidDecimal] + validator_list 
  769
+        # Initialise the TextField, making sure it's large enough to fit the number with a - sign and a decimal point. 
  770
+        super(DecimalField, self).__init__(field_name, max_digits+2, max_digits+2, is_required, validator_list) 
758 771
 
759  
-    def isValidFloat(self, field_data, all_data):
760  
-        v = validators.IsValidFloat(self.max_digits, self.decimal_places)
  772
+    def isValidDecimal(self, field_data, all_data): 
  773
+        v = validators.IsValidDecimal(self.max_digits, self.decimal_places) 
761 774
         try:
762 775
             v(field_data, all_data)
763 776
         except validators.ValidationError, e:
@@ -766,7 +779,14 @@ def isValidFloat(self, field_data, all_data):
766 779
     def html2python(data):
767 780
         if data == '' or data is None:
768 781
             return None
769  
-        return float(data)
  782
+        try: 
  783
+            import decimal 
  784
+        except ImportError:
  785
+            from django.utils import decimal
  786
+        try: 
  787
+            return decimal.Decimal(data) 
  788
+        except decimal.InvalidOperation, e: 
  789
+            raise ValueError, e 
770 790
     html2python = staticmethod(html2python)
771 791
 
772 792
 ####################
3,079  django/utils/_decimal.py
3079 additions, 0 deletions not shown
7  docs/forms.txt
@@ -567,6 +567,7 @@ check for the given property:
567 567
     * isValidANSIDate
568 568
     * isValidANSITime
569 569
     * isValidEmail
  570
+    * isValidFloat
570 571
     * isValidImage
571 572
     * isValidImageURL
572 573
     * isValidPhone
@@ -664,10 +665,10 @@ fails. If no message is passed in, a default message is used.
664 665
     Takes an integer argument and when called as a validator, checks that the
665 666
     field being validated is a power of the integer.
666 667
 
667  
-``IsValidFloat``
  668
+``IsValidDecimal``
668 669
     Takes a maximum number of digits and number of decimal places (in that
669  
-    order) and validates whether the field is a float with less than the
670  
-    maximum number of digits and decimal place.
  670
+    order) and validates whether the field is a decimal with no more than the
  671
+    maximum number of digits and decimal places.
671 672
 
672 673
 ``MatchesRegularExpression``
673 674
     Takes a regular expression (a string) as a parameter and validates the
48  docs/model-api.txt
@@ -184,6 +184,33 @@ A date and time field. Takes the same extra options as ``DateField``.
184 184
 The admin represents this as two ``<input type="text">`` fields, with
185 185
 JavaScript shortcuts.
186 186
 
  187
+``DecimalField``
  188
+~~~~~~~~~~~~~~
  189
+
  190
+A fixed-precision decimal number, represented in Python by a ``Decimal`` instance.
  191
+Has two **required** arguments:
  192
+
  193
+    ======================  ===================================================
  194
+    Argument                Description
  195
+    ======================  ===================================================
  196
+    ``max_digits``          The maximum number of digits allowed in the number.
  197
+
  198
+    ``decimal_places``      The number of decimal places to store with the
  199
+                            number.
  200
+    ======================  ===================================================
  201
+
  202
+For example, to store numbers up to 999 with a resolution of 2 decimal places,
  203
+you'd use::
  204
+
  205
+    models.DecimalField(..., max_digits=5, decimal_places=2)
  206
+
  207
+And to store numbers up to approximately one billion with a resolution of 10
  208
+decimal places::
  209
+
  210
+    models.DecimalField(..., max_digits=19, decimal_places=10)
  211
+
  212
+The admin represents this as an ``<input type="text">`` (a single-line input).
  213
+
187 214
 ``EmailField``
188 215
 ~~~~~~~~~~~~~~
189 216
 
@@ -290,26 +317,7 @@ because the ``match`` applies to the base filename (``foo.gif`` and
290 317
 ``FloatField``
291 318
 ~~~~~~~~~~~~~~
292 319
 
293  
-A floating-point number. Has two **required** arguments:
294  
-
295  
-    ======================  ===================================================
296  
-    Argument                Description
297  
-    ======================  ===================================================
298  
-    ``max_digits``          The maximum number of digits allowed in the number.
299  
-
300  
-    ``decimal_places``      The number of decimal places to store with the
301  
-                            number.
302  
-    ======================  ===================================================
303  
-
304  
-For example, to store numbers up to 999 with a resolution of 2 decimal places,
305  
-you'd use::
306  
-
307  
-    models.FloatField(..., max_digits=5, decimal_places=2)
308  
-
309  
-And to store numbers up to approximately one billion with a resolution of 10
310  
-decimal places::
311  
-
312  
-    models.FloatField(..., max_digits=19, decimal_places=10)
  320
+A floating-point number represented in Python by a ``float`` instance.
313 321
 
314 322
 The admin represents this as an ``<input type="text">`` (a single-line input).
315 323
 
8  docs/newforms.txt
@@ -1253,10 +1253,11 @@ the full list of conversions:
1253 1253
     ``CommaSeparatedIntegerField``   ``CharField``
1254 1254
     ``DateField``                    ``DateField``
1255 1255
     ``DateTimeField``                ``DateTimeField``
  1256
+    ``DecimalField``                 ``DecimalField``
1256 1257
     ``EmailField``                   ``EmailField``
1257 1258
     ``FileField``                    ``CharField``
1258 1259
     ``FilePathField``                ``CharField``
1259  
-    ``FloatField``                   ``CharField``
  1260
+    ``FloatField``                   ``FloatField``
1260 1261
     ``ForeignKey``                   ``ModelChoiceField`` (see below)
1261 1262
     ``ImageField``                   ``CharField``
1262 1263
     ``IntegerField``                 ``IntegerField``
@@ -1281,6 +1282,11 @@ the full list of conversions:
1281 1282
     ``XMLField``                     ``CharField`` with ``widget=Textarea``
1282 1283
     ===============================  ========================================
1283 1284
 
  1285
+
  1286
+.. note::
  1287
+    The ``FloatField`` form field and ``DecimalField`` model and form fields
  1288
+    are new in the development version.
  1289
+
1284 1290
 As you might expect, the ``ForeignKey`` and ``ManyToManyField`` model field
1285 1291
 types are special cases:
1286 1292
 
10  tests/modeltests/invalid_models/models.py
@@ -8,7 +8,7 @@
8 8
 
9 9
 class FieldErrors(models.Model):
10 10
     charfield = models.CharField()
11  
-    floatfield = models.FloatField()
  11
+    decimalfield = models.DecimalField()
12 12
     filefield = models.FileField()
13 13
     prepopulate = models.CharField(maxlength=10, prepopulate_from='bad')
14 14
     choices = models.CharField(maxlength=10, choices='bad')
@@ -87,10 +87,10 @@ class SelfClashM2M(models.Model):
87 87
     src_safe = models.CharField(maxlength=10)
88 88
     selfclashm2m = models.CharField(maxlength=10)
89 89
 
90  
-    # Non-symmetrical M2M fields _do_ have related accessors, so 
  90
+    # Non-symmetrical M2M fields _do_ have related accessors, so
91 91
     # there is potential for clashes.
92 92
     selfclashm2m_set = models.ManyToManyField("SelfClashM2M", symmetrical=False)
93  
-    
  93
+
94 94
     m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id', symmetrical=False)
95 95
     m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe', symmetrical=False)
96 96
 
@@ -108,8 +108,8 @@ class Car(models.Model):
108 108
     model = models.ForeignKey(Model)
109 109
 
110 110
 model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
111  
-invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
112  
-invalid_models.fielderrors: "floatfield": FloatFields require a "max_digits" attribute.
  111
+invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
  112
+invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
113 113
 invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute.
114 114
 invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple.
115 115
 invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
131  tests/regressiontests/forms/tests.py
@@ -7,6 +7,10 @@
7 7
 >>> import datetime
8 8
 >>> import time
9 9
 >>> import re
  10
+>>> try:
  11
+...     from decimal import Decimal
  12
+... except ImportError:
  13
+...     from django.utils._decimal import Decimal
10 14
 
11 15
 ###########
12 16
 # Widgets #
@@ -1046,6 +1050,133 @@
1046 1050
 ...
1047 1051
 ValidationError: [u'Ensure this value is less than or equal to 20.']
1048 1052
 
  1053
+# FloatField ##################################################################
  1054
+
  1055
+>>> f = FloatField()
  1056
+>>> f.clean('')
  1057
+Traceback (most recent call last):
  1058
+...
  1059
+ValidationError: [u'This field is required.']
  1060
+>>> f.clean(None)
  1061
+Traceback (most recent call last):
  1062
+...
  1063
+ValidationError: [u'This field is required.']
  1064
+>>> f.clean('1')
  1065
+1.0
  1066
+>>> isinstance(f.clean('1'), float)
  1067
+True
  1068
+>>> f.clean('23')
  1069
+23.0
  1070
+>>> f.clean('3.14')
  1071
+3.1400000000000001
  1072
+>>> f.clean('a')
  1073
+Traceback (most recent call last):
  1074
+...
  1075
+ValidationError: [u'Enter a number.']
  1076
+>>> f.clean('1.0 ')
  1077
+1.0
  1078
+>>> f.clean(' 1.0')
  1079
+1.0
  1080
+>>> f.clean(' 1.0 ')
  1081
+1.0
  1082
+>>> f.clean('1.0a')
  1083
+Traceback (most recent call last):
  1084
+...
  1085
+ValidationError: [u'Enter a number.']
  1086
+
  1087
+>>> f = FloatField(required=False)
  1088
+>>> f.clean('')
  1089
+
  1090
+>>> f.clean(None)
  1091
+
  1092
+>>> f.clean('1')
  1093
+1.0
  1094
+
  1095
+FloatField accepts min_value and max_value just like IntegerField:
  1096
+>>> f = FloatField(max_value=1.5, min_value=0.5)
  1097
+
  1098
+>>> f.clean('1.6')
  1099
+Traceback (most recent call last):
  1100
+...
  1101
+ValidationError: [u'Ensure this value is less than or equal to 1.5.']
  1102
+>>> f.clean('0.4')
  1103
+Traceback (most recent call last):
  1104
+...
  1105
+ValidationError: [u'Ensure this value is greater than or equal to 0.5.']
  1106
+>>> f.clean('1.5')
  1107
+1.5
  1108
+>>> f.clean('0.5')
  1109
+0.5
  1110
+
  1111
+# DecimalField ################################################################
  1112
+
  1113
+>>> f = DecimalField(max_digits=4, decimal_places=2)
  1114
+>>> f.clean('')
  1115
+Traceback (most recent call last):
  1116
+...
  1117
+ValidationError: [u'This field is required.']
  1118
+>>> f.clean(None)
  1119
+Traceback (most recent call last):
  1120
+...
  1121
+ValidationError: [u'This field is required.']
  1122
+>>> f.clean('1')
  1123
+Decimal("1")
  1124
+>>> isinstance(f.clean('1'), Decimal)
  1125
+True
  1126
+>>> f.clean('23')
  1127
+Decimal("23")
  1128
+>>> f.clean('3.14')
  1129
+Decimal("3.14")
  1130
+>>> f.clean('a')
  1131
+Traceback (most recent call last):
  1132
+...
  1133
+ValidationError: [u'Enter a number.']
  1134
+>>> f.clean('1.0 ')
  1135
+Decimal("1.0")
  1136
+>>> f.clean(' 1.0')
  1137
+Decimal("1.0")
  1138
+>>> f.clean(' 1.0 ')
  1139
+Decimal("1.0")
  1140
+>>> f.clean('1.0a')
  1141
+Traceback (most recent call last):
  1142
+...
  1143
+ValidationError: [u'Enter a number.']
  1144
+>>> f.clean('123.45')
  1145
+Traceback (most recent call last):
  1146
+...
  1147
+ValidationError: [u'Ensure that there are no more than 4 digits in total.']
  1148
+>>> f.clean('1.234')
  1149
+Traceback (most recent call last):
  1150
+...
  1151
+ValidationError: [u'Ensure that there are no more than 2 decimal places.']
  1152
+>>> f.clean('123.4')
  1153
+Traceback (most recent call last):
  1154
+...
  1155
+ValidationError: [u'Ensure that there are no more than 2 digits before the decimal point.']
  1156
+>>> f = DecimalField(max_digits=4, decimal_places=2, required=False)
  1157
+>>> f.clean('')
  1158
+
  1159
+>>> f.clean(None)
  1160
+
  1161
+>>> f.clean('1')
  1162
+Decimal("1")
  1163
+
  1164
+DecimalField accepts min_value and max_value just like IntegerField:
  1165
+>>> f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5'))
  1166
+
  1167
+>>> f.clean('1.6')
  1168
+Traceback (most recent call last):
  1169
+...
  1170
+ValidationError: [u'Ensure this value is less than or equal to 1.5.']
  1171
+>>> f.clean('0.4')
  1172
+Traceback (most recent call last):
  1173
+...
  1174
+ValidationError: [u'Ensure this value is greater than or equal to 0.5.']
  1175
+>>> f.clean('1.5')
  1176
+Decimal("1.5")
  1177
+>>> f.clean('0.5')
  1178
+Decimal("0.5")
  1179
+
1049 1180
 # DateField ###################################################################
1050 1181
 
1051 1182
 >>> import datetime
16  tests/regressiontests/serializers_regress/models.py
... ...
@@ -1,7 +1,7 @@
1 1
 """
2 2
 A test spanning all the capabilities of all the serializers.
3 3
 
4  
-This class sets up a model for each model field type 
  4
+This class sets up a model for each model field type
5 5
 (except for image types, because of the PIL dependency).
6 6
 """
7 7
 
@@ -9,12 +9,12 @@
9 9
 from django.contrib.contenttypes import generic
10 10
 from django.contrib.contenttypes.models import ContentType
11 11
 
12  
-# The following classes are for testing basic data 
  12
+# The following classes are for testing basic data
13 13
 # marshalling, including NULL values.
14 14
 
15 15
 class BooleanData(models.Model):
16 16
     data = models.BooleanField(null=True)
17  
-    
  17
+
18 18
 class CharData(models.Model):
19 19
     data = models.CharField(maxlength=30, null=True)
20 20
 
@@ -24,6 +24,9 @@ class DateData(models.Model):
24 24
 class DateTimeData(models.Model):
25 25
     data = models.DateTimeField(null=True)
26 26
 
  27
+class DecimalData(models.Model):
  28
+    data = models.DecimalField(null=True, decimal_places=3, max_digits=5)
  29
+
27 30
 class EmailData(models.Model):
28 31
     data = models.EmailField(null=True)
29 32
 
@@ -34,7 +37,7 @@ class FilePathData(models.Model):
34 37
     data = models.FilePathField(null=True)
35 38
 
36 39
 class FloatData(models.Model):
37  
-    data = models.FloatField(null=True, decimal_places=3, max_digits=5)
  40
+    data = models.FloatField(null=True)
38 41
 
39 42
 class IntegerData(models.Model):
40 43
     data = models.IntegerField(null=True)
@@ -145,6 +148,9 @@ class CharPKData(models.Model):
145 148
 # class DateTimePKData(models.Model):
146 149
 #    data = models.DateTimeField(primary_key=True)
147 150
 
  151
+class DecimalPKData(models.Model):
  152
+    data = models.DecimalField(primary_key=True, decimal_places=3, max_digits=5)
  153
+
148 154
 class EmailPKData(models.Model):
149 155
     data = models.EmailField(primary_key=True)
150 156
 
@@ -155,7 +161,7 @@ class FilePathPKData(models.Model):
155 161
     data = models.FilePathField(primary_key=True)
156 162
 
157 163
 class FloatPKData(models.Model):
158  
-    data = models.FloatField(primary_key=True, decimal_places=3, max_digits=5)
  164
+    data = models.FloatField(primary_key=True)
159 165
 
160 166
 class IntegerPKData(models.Model):
161 167
     data = models.IntegerField(primary_key=True)
71  tests/regressiontests/serializers_regress/tests.py
@@ -2,7 +2,7 @@
2 2
 A test spanning all the capabilities of all the serializers.
3 3
 
4 4
 This class defines sample data and a dynamically generated
5  
-test case that is capable of testing the capabilities of 
  5
+test case that is capable of testing the capabilities of
6 6
 the serializers. This includes all valid data values, plus
7 7
 forward, backwards and self references.
8 8
 """
@@ -16,13 +16,17 @@
16 16
 from django.core import management
17 17
 
18 18
 from models import *
  19
+try:
  20
+    import decimal
  21
+except ImportError:
  22
+    from django.utils import _decimal as decimal
19 23
 
20 24
 # A set of functions that can be used to recreate
21 25
 # test data objects of various kinds
22 26
 def data_create(pk, klass, data):
23 27
     instance = klass(id=pk)
24 28
     instance.data = data
25  
-    instance.save()    
  29
+    instance.save()
26 30
     return instance
27 31
 
28 32
 def generic_create(pk, klass, data):
@@ -32,13 +36,13 @@ def generic_create(pk, klass, data):
32 36
     for tag in data[1:]:
33 37
         instance.tags.create(data=tag)
34 38
     return instance
35  
-    
  39
+
36 40
 def fk_create(pk, klass, data):
37 41
     instance = klass(id=pk)
38 42
     setattr(instance, 'data_id', data)
39 43
     instance.save()
40 44
     return instance
41  
-    
  45
+
42 46
 def m2m_create(pk, klass, data):
43 47
     instance = klass(id=pk)
44 48
     instance.save()
@@ -61,14 +65,14 @@ def pk_create(pk, klass, data):
61 65
 # test data objects of various kinds
62 66
 def data_compare(testcase, pk, klass, data):