mirror of https://github.com/django/django.git
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
This commit is contained in:
parent
03966f077b
commit
92c35a0617
2
AUTHORS
2
AUTHORS
|
@ -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 @@ DATA_TYPE_MAPPING = {
|
|||
'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 @@ Serialize data to/from JSON
|
|||
|
||||
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 @@ class DateTimeAwareJSONEncoder(simplejson.JSONEncoder):
|
|||
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 @@ email_re = re.compile(
|
|||
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 @@ class IsAPowerOf(object):
|
|||
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 @@ DATA_TYPES = {
|
|||
'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 @@ except ImportError, e:
|
|||
# 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 @@ IntegrityError = Database.IntegrityError
|
|||
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 @@ DATA_TYPES = {
|
|||
'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 @@ django_conversions.update({
|
|||
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 @@ DATA_TYPES = {
|
|||
'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 @@ DATA_TYPES = {
|
|||
'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 @@ DATA_TYPES_REVERSE = {
|
|||
1114: 'DateTimeField',
|
||||
1184: 'DateTimeField',
|
||||
1266: 'TimeField',
|
||||
1700: 'FloatField',
|
||||
1700: 'DecimalField',
|
||||
}
|
||||
|
|
|
@ -249,6 +249,7 @@ except AttributeError:
|
|||
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 @@ DATA_TYPES = {
|
|||
'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,6 +72,7 @@ DATA_TYPES_REVERSE = {
|
|||
21: 'SmallIntegerField',
|
||||
23: 'IntegerField',
|
||||
25: 'TextField',
|
||||
701: 'FloatField',
|
||||
869: 'IPAddressField',
|
||||
1043: 'CharField',
|
||||
1082: 'DateField',
|
||||
|
@ -79,5 +80,5 @@ DATA_TYPES_REVERSE = {
|
|||
1114: 'DateTimeField',
|
||||
1184: 'DateTimeField',
|
||||
1266: 'TimeField',
|
||||
1700: 'FloatField',
|
||||
1700: 'DecimalField',
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ DATA_TYPES_REVERSE = {
|
|||
21: 'SmallIntegerField',
|
||||
23: 'IntegerField',
|
||||
25: 'TextField',
|
||||
701: 'FloatField',
|
||||
869: 'IPAddressField',
|
||||
1043: 'CharField',
|
||||
1082: 'DateField',
|
||||
|
@ -79,5 +80,5 @@ DATA_TYPES_REVERSE = {
|
|||
1114: 'DateTimeField',
|
||||
1184: 'DateTimeField',
|
||||
1266: 'TimeField',
|
||||
1700: 'FloatField',
|
||||
1700: 'DecimalField',
|
||||
}
|
||||
|
|
|
@ -17,6 +17,11 @@ except ImportError, e:
|
|||
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("date", util.typecast_date)
|
|||
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 @@ DATA_TYPES = {
|
|||
'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)',
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import datetime
|
||||
from time import time
|
||||
|
||||
try:
|
||||
import decimal
|
||||
except ImportError:
|
||||
from django.utils import _decimal as decimal # for Python 2.3
|
||||
|
||||
class CursorDebugWrapper(object):
|
||||
def __init__(self, cursor, db):
|
||||
self.cursor = cursor
|
||||
|
@ -85,6 +90,11 @@ def typecast_boolean(s):
|
|||
if not s: return False
|
||||
return str(s)[0].lower() == 't'
|
||||
|
||||
def typecast_decimal(s):
|
||||
if s is None:
|
||||
return None
|
||||
return decimal.Decimal(s)
|
||||
|
||||
###############################################
|
||||
# Converters from Python to database (string) #
|
||||
###############################################
|
||||
|
@ -92,6 +102,11 @@ def typecast_boolean(s):
|
|||
def rev_typecast_boolean(obj, d):
|
||||
return obj and '1' or '0'
|
||||
|
||||
def rev_typecast_decimal(d):
|
||||
if d is None:
|
||||
return None
|
||||
return str(d)
|
||||
|
||||
##################################################################################
|
||||
# Helper functions for dictfetch* for databases that don't natively support them #
|
||||
##################################################################################
|
||||
|
|
|
@ -10,6 +10,10 @@ from django.utils.itercompat import tee
|
|||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
import datetime, os, time
|
||||
try:
|
||||
import decimal
|
||||
except ImportError:
|
||||
from django.utils import _decimal as decimal # for Python 2.3
|
||||
|
||||
class NOT_PROVIDED:
|
||||
pass
|
||||
|
@ -573,6 +577,65 @@ class DateTimeField(DateField):
|
|||
defaults.update(kwargs)
|
||||
return super(DateTimeField, self).formfield(**defaults)
|
||||
|
||||
class DecimalField(Field):
|
||||
empty_strings_allowed = False
|
||||
def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
Field.__init__(self, verbose_name, name, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
return decimal.Decimal(value)
|
||||
except decimal.InvalidOperation:
|
||||
raise validators.ValidationError, gettext("This value must be a decimal number.")
|
||||
|
||||
def _format(self, value):
|
||||
if isinstance(value, basestring):
|
||||
return value
|
||||
else:
|
||||
return self.format_number(value)
|
||||
|
||||
def format_number(self, value):
|
||||
"""
|
||||
Formats a number into a string with the requisite number of digits and
|
||||
decimal places.
|
||||
"""
|
||||
num_chars = self.max_digits
|
||||
# Allow for a decimal point
|
||||
if self.decimal_places > 0:
|
||||
num_chars += 1
|
||||
# Allow for a minus sign
|
||||
if value < 0:
|
||||
num_chars += 1
|
||||
|
||||
return "%.*f" % (self.decimal_places, value)
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
if value is not None:
|
||||
value = self._format(value)
|
||||
return super(DecimalField, self).get_db_prep_save(value)
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
if lookup_type == 'range':
|
||||
value = [self._format(v) for v in value]
|
||||
else:
|
||||
value = self._format(value)
|
||||
return super(DecimalField, self).get_db_prep_lookup(lookup_type, value)
|
||||
|
||||
def get_manipulator_field_objs(self):
|
||||
return [curry(oldforms.DecimalField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'max_digits': self.max_digits,
|
||||
'decimal_places': self.decimal_places,
|
||||
'form_class': forms.DecimalField,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(DecimalField, self).formfield(**defaults)
|
||||
|
||||
class EmailField(CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['maxlength'] = 75
|
||||
|
@ -683,12 +746,14 @@ class FilePathField(Field):
|
|||
|
||||
class FloatField(Field):
|
||||
empty_strings_allowed = False
|
||||
def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
Field.__init__(self, verbose_name, name, **kwargs)
|
||||
|
||||
def get_manipulator_field_objs(self):
|
||||
return [curry(oldforms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
|
||||
return [oldforms.FloatField]
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.FloatField}
|
||||
defaults.update(kwargs)
|
||||
return super(FloatField, self).formfield(**defaults)
|
||||
|
||||
class ImageField(FileField):
|
||||
def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
|
||||
|
|
|
@ -19,7 +19,7 @@ __all__ = (
|
|||
'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
|
||||
'RegexField', 'EmailField', 'URLField', 'BooleanField',
|
||||
'ChoiceField', 'NullBooleanField', 'MultipleChoiceField',
|
||||
'ComboField', 'MultiValueField',
|
||||
'ComboField', 'MultiValueField', 'FloatField', 'DecimalField',
|
||||
'SplitDateTimeField',
|
||||
)
|
||||
|
||||
|
@ -31,6 +31,11 @@ try:
|
|||
except NameError:
|
||||
from sets import Set as set # Python 2.3 fallback
|
||||
|
||||
try:
|
||||
from decimal import Decimal
|
||||
except ImportError:
|
||||
from django.utils._decimal import Decimal # Python 2.3 fallback
|
||||
|
||||
class Field(object):
|
||||
widget = TextInput # Default widget to use when rendering this type of Field.
|
||||
hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden".
|
||||
|
@ -134,6 +139,67 @@ class IntegerField(Field):
|
|||
raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value)
|
||||
return value
|
||||
|
||||
class FloatField(Field):
|
||||
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
|
||||
self.max_value, self.min_value = max_value, min_value
|
||||
Field.__init__(self, *args, **kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
"""
|
||||
Validates that float() can be called on the input. Returns a float.
|
||||
Returns None for empty values.
|
||||
"""
|
||||
super(FloatField, self).clean(value)
|
||||
if not self.required and value in EMPTY_VALUES:
|
||||
return None
|
||||
try:
|
||||
value = float(value)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError(gettext('Enter a number.'))
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value)
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value)
|
||||
return value
|
||||
|
||||
decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$')
|
||||
|
||||
class DecimalField(Field):
|
||||
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
|
||||
self.max_value, self.min_value = max_value, min_value
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
Field.__init__(self, *args, **kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
"""
|
||||
Validates that the input is a decimal number. Returns a Decimal
|
||||
instance. Returns None for empty values. Ensures that there are no more
|
||||
than max_digits in the number, and no more than decimal_places digits
|
||||
after the decimal point.
|
||||
"""
|
||||
super(DecimalField, self).clean(value)
|
||||
if not self.required and value in EMPTY_VALUES:
|
||||
return None
|
||||
value = value.strip()
|
||||
match = decimal_re.search(value)
|
||||
if not match:
|
||||
raise ValidationError(gettext('Enter a number.'))
|
||||
else:
|
||||
value = Decimal(value)
|
||||
digits = len(match.group('digits') or '')
|
||||
decimals = len(match.group('decimals') or '')
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
raise ValidationError(gettext('Ensure this value is less than or equal to %s.') % self.max_value)
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
raise ValidationError(gettext('Ensure this value is greater than or equal to %s.') % self.min_value)
|
||||
if self.max_digits is not None and (digits + decimals) > self.max_digits:
|
||||
raise ValidationError(gettext('Ensure that there are no more than %s digits in total.') % self.max_digits)
|
||||
if self.decimal_places is not None and decimals > self.decimal_places:
|
||||
raise ValidationError(gettext('Ensure that there are no more than %s decimal places.') % self.decimal_places)
|
||||
if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places):
|
||||
raise ValidationError(gettext('Ensure that there are no more than %s digits before the decimal point.') % (self.max_digits - self.decimal_places))
|
||||
return value
|
||||
|
||||
DEFAULT_DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
|
||||
'%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
|
||||
|
|
|
@ -750,14 +750,27 @@ class PositiveSmallIntegerField(IntegerField):
|
|||
raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.")
|
||||
|
||||
class FloatField(TextField):
|
||||
def __init__(self, field_name, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
validator_list = [validators.isValidFloat] + validator_list
|
||||
TextField.__init__(self, field_name, is_required=is_required, validator_list=validator_list)
|
||||
|
||||
def html2python(data):
|
||||
if data == '' or data is None:
|
||||
return None
|
||||
return float(data)
|
||||
html2python = staticmethod(html2python)
|
||||
|
||||
class DecimalField(TextField):
|
||||
def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None):
|
||||
if validator_list is None: validator_list = []
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
validator_list = [self.isValidFloat] + validator_list
|
||||
TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list)
|
||||
validator_list = [self.isValidDecimal] + validator_list
|
||||
# Initialise the TextField, making sure it's large enough to fit the number with a - sign and a decimal point.
|
||||
super(DecimalField, self).__init__(field_name, max_digits+2, max_digits+2, is_required, validator_list)
|
||||
|
||||
def isValidFloat(self, field_data, all_data):
|
||||
v = validators.IsValidFloat(self.max_digits, self.decimal_places)
|
||||
def isValidDecimal(self, field_data, all_data):
|
||||
v = validators.IsValidDecimal(self.max_digits, self.decimal_places)
|
||||
try:
|
||||
v(field_data, all_data)
|
||||
except validators.ValidationError, e:
|
||||
|
@ -766,7 +779,14 @@ class FloatField(TextField):
|
|||
def html2python(data):
|
||||
if data == '' or data is None:
|
||||
return None
|
||||
return float(data)
|
||||
try:
|
||||
import decimal
|
||||
except ImportError:
|
||||
from django.utils import decimal
|
||||
try:
|
||||
return decimal.Decimal(data)
|
||||
except decimal.InvalidOperation, e:
|
||||
raise ValueError, e
|
||||
html2python = staticmethod(html2python)
|
||||
|
||||
####################
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -567,6 +567,7 @@ check for the given property:
|
|||
* isValidANSIDate
|
||||
* isValidANSITime
|
||||
* isValidEmail
|
||||
* isValidFloat
|
||||
* isValidImage
|
||||
* isValidImageURL
|
||||
* isValidPhone
|
||||
|
@ -664,10 +665,10 @@ fails. If no message is passed in, a default message is used.
|
|||
Takes an integer argument and when called as a validator, checks that the
|
||||
field being validated is a power of the integer.
|
||||
|
||||
``IsValidFloat``
|
||||
``IsValidDecimal``
|
||||
Takes a maximum number of digits and number of decimal places (in that
|
||||
order) and validates whether the field is a float with less than the
|
||||
maximum number of digits and decimal place.
|
||||
order) and validates whether the field is a decimal with no more than the
|
||||
maximum number of digits and decimal places.
|
||||
|
||||
``MatchesRegularExpression``
|
||||
Takes a regular expression (a string) as a parameter and validates the
|
||||
|
|
|
@ -184,6 +184,33 @@ A date and time field. Takes the same extra options as ``DateField``.
|
|||
The admin represents this as two ``<input type="text">`` fields, with
|
||||
JavaScript shortcuts.
|
||||
|
||||
``DecimalField``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
A fixed-precision decimal number, represented in Python by a ``Decimal`` instance.
|
||||
Has two **required** arguments:
|
||||
|
||||
====================== ===================================================
|
||||
Argument Description
|
||||
====================== ===================================================
|
||||
``max_digits`` The maximum number of digits allowed in the number.
|
||||
|
||||
``decimal_places`` The number of decimal places to store with the
|
||||
number.
|
||||
====================== ===================================================
|
||||
|
||||
For example, to store numbers up to 999 with a resolution of 2 decimal places,
|
||||
you'd use::
|
||||
|
||||
models.DecimalField(..., max_digits=5, decimal_places=2)
|
||||
|
||||
And to store numbers up to approximately one billion with a resolution of 10
|
||||
decimal places::
|
||||
|
||||
models.DecimalField(..., max_digits=19, decimal_places=10)
|
||||
|
||||
The admin represents this as an ``<input type="text">`` (a single-line input).
|
||||
|
||||
``EmailField``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -290,26 +317,7 @@ because the ``match`` applies to the base filename (``foo.gif`` and
|
|||
``FloatField``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
A floating-point number. Has two **required** arguments:
|
||||
|
||||
====================== ===================================================
|
||||
Argument Description
|
||||
====================== ===================================================
|
||||
``max_digits`` The maximum number of digits allowed in the number.
|
||||
|
||||
``decimal_places`` The number of decimal places to store with the
|
||||
number.
|
||||
====================== ===================================================
|
||||
|
||||
For example, to store numbers up to 999 with a resolution of 2 decimal places,
|
||||
you'd use::
|
||||
|
||||
models.FloatField(..., max_digits=5, decimal_places=2)
|
||||
|
||||
And to store numbers up to approximately one billion with a resolution of 10
|
||||
decimal places::
|
||||
|
||||
models.FloatField(..., max_digits=19, decimal_places=10)
|
||||
A floating-point number represented in Python by a ``float`` instance.
|
||||
|
||||
The admin represents this as an ``<input type="text">`` (a single-line input).
|
||||
|
||||
|
|
|
@ -1253,10 +1253,11 @@ the full list of conversions:
|
|||
``CommaSeparatedIntegerField`` ``CharField``
|
||||
``DateField`` ``DateField``
|
||||
``DateTimeField`` ``DateTimeField``
|
||||
``DecimalField`` ``DecimalField``
|
||||
``EmailField`` ``EmailField``
|
||||
``FileField`` ``CharField``
|
||||
``FilePathField`` ``CharField``
|
||||
``FloatField`` ``CharField``
|
||||
``FloatField`` ``FloatField``
|
||||
``ForeignKey`` ``ModelChoiceField`` (see below)
|
||||
``ImageField`` ``CharField``
|
||||
``IntegerField`` ``IntegerField``
|
||||
|
@ -1281,6 +1282,11 @@ the full list of conversions:
|
|||
``XMLField`` ``CharField`` with ``widget=Textarea``
|
||||
=============================== ========================================
|
||||
|
||||
|
||||
.. note::
|
||||
The ``FloatField`` form field and ``DecimalField`` model and form fields
|
||||
are new in the development version.
|
||||
|
||||
As you might expect, the ``ForeignKey`` and ``ManyToManyField`` model field
|
||||
types are special cases:
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.db import models
|
|||
|
||||
class FieldErrors(models.Model):
|
||||
charfield = models.CharField()
|
||||
floatfield = models.FloatField()
|
||||
decimalfield = models.DecimalField()
|
||||
filefield = models.FileField()
|
||||
prepopulate = models.CharField(maxlength=10, prepopulate_from='bad')
|
||||
choices = models.CharField(maxlength=10, choices='bad')
|
||||
|
@ -87,10 +87,10 @@ class SelfClashM2M(models.Model):
|
|||
src_safe = models.CharField(maxlength=10)
|
||||
selfclashm2m = models.CharField(maxlength=10)
|
||||
|
||||
# Non-symmetrical M2M fields _do_ have related accessors, so
|
||||
# Non-symmetrical M2M fields _do_ have related accessors, so
|
||||
# there is potential for clashes.
|
||||
selfclashm2m_set = models.ManyToManyField("SelfClashM2M", symmetrical=False)
|
||||
|
||||
|
||||
m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id', symmetrical=False)
|
||||
m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe', symmetrical=False)
|
||||
|
||||
|
@ -108,8 +108,8 @@ class Car(models.Model):
|
|||
model = models.ForeignKey(Model)
|
||||
|
||||
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
|
||||
invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
|
||||
invalid_models.fielderrors: "floatfield": FloatFields require a "max_digits" attribute.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
|
||||
invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute.
|
||||
invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple.
|
||||
invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
|
||||
|
|
|
@ -7,6 +7,10 @@ form_tests = r"""
|
|||
>>> import datetime
|
||||
>>> import time
|
||||
>>> import re
|
||||
>>> try:
|
||||
... from decimal import Decimal
|
||||
... except ImportError:
|
||||
... from django.utils._decimal import Decimal
|
||||
|
||||
###########
|
||||
# Widgets #
|
||||
|
@ -1046,6 +1050,133 @@ Traceback (most recent call last):
|
|||
...
|
||||
ValidationError: [u'Ensure this value is less than or equal to 20.']
|
||||
|
||||
# FloatField ##################################################################
|
||||
|
||||
>>> f = FloatField()
|
||||
>>> f.clean('')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'This field is required.']
|
||||
>>> f.clean(None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'This field is required.']
|
||||
>>> f.clean('1')
|
||||
1.0
|
||||
>>> isinstance(f.clean('1'), float)
|
||||
True
|
||||
>>> f.clean('23')
|
||||
23.0
|
||||
>>> f.clean('3.14')
|
||||
3.1400000000000001
|
||||
>>> f.clean('a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Enter a number.']
|
||||
>>> f.clean('1.0 ')
|
||||
1.0
|
||||
>>> f.clean(' 1.0')
|
||||
1.0
|
||||
>>> f.clean(' 1.0 ')
|
||||
1.0
|
||||
>>> f.clean('1.0a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Enter a number.']
|
||||
|
||||
>>> f = FloatField(required=False)
|
||||
>>> f.clean('')
|
||||
|
||||
>>> f.clean(None)
|
||||
|
||||
>>> f.clean('1')
|
||||
1.0
|
||||
|
||||
FloatField accepts min_value and max_value just like IntegerField:
|
||||
>>> f = FloatField(max_value=1.5, min_value=0.5)
|
||||
|
||||
>>> f.clean('1.6')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure this value is less than or equal to 1.5.']
|
||||
>>> f.clean('0.4')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure this value is greater than or equal to 0.5.']
|
||||
>>> f.clean('1.5')
|
||||
1.5
|
||||
>>> f.clean('0.5')
|
||||
0.5
|
||||
|
||||
# DecimalField ################################################################
|
||||
|
||||
>>> f = DecimalField(max_digits=4, decimal_places=2)
|
||||
>>> f.clean('')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'This field is required.']
|
||||
>>> f.clean(None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'This field is required.']
|
||||
>>> f.clean('1')
|
||||
Decimal("1")
|
||||
>>> isinstance(f.clean('1'), Decimal)
|
||||
True
|
||||
>>> f.clean('23')
|
||||
Decimal("23")
|
||||
>>> f.clean('3.14')
|
||||
Decimal("3.14")
|
||||
>>> f.clean('a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Enter a number.']
|
||||
>>> f.clean('1.0 ')
|
||||
Decimal("1.0")
|
||||
>>> f.clean(' 1.0')
|
||||
Decimal("1.0")
|
||||
>>> f.clean(' 1.0 ')
|
||||
Decimal("1.0")
|
||||
>>> f.clean('1.0a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Enter a number.']
|
||||
>>> f.clean('123.45')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure that there are no more than 4 digits in total.']
|
||||
>>> f.clean('1.234')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure that there are no more than 2 decimal places.']
|
||||
>>> f.clean('123.4')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure that there are no more than 2 digits before the decimal point.']
|
||||
>>> f = DecimalField(max_digits=4, decimal_places=2, required=False)
|
||||
>>> f.clean('')
|
||||
|
||||
>>> f.clean(None)
|
||||
|
||||
>>> f.clean('1')
|
||||
Decimal("1")
|
||||
|
||||
DecimalField accepts min_value and max_value just like IntegerField:
|
||||
>>> f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5'))
|
||||
|
||||
>>> f.clean('1.6')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure this value is less than or equal to 1.5.']
|
||||
>>> f.clean('0.4')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValidationError: [u'Ensure this value is greater than or equal to 0.5.']
|
||||
>>> f.clean('1.5')
|
||||
Decimal("1.5")
|
||||
>>> f.clean('0.5')
|
||||
Decimal("0.5")
|
||||
|
||||
# DateField ###################################################################
|
||||
|
||||
>>> import datetime
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
A test spanning all the capabilities of all the serializers.
|
||||
|
||||
This class sets up a model for each model field type
|
||||
This class sets up a model for each model field type
|
||||
(except for image types, because of the PIL dependency).
|
||||
"""
|
||||
|
||||
|
@ -9,12 +9,12 @@ from django.db import models
|
|||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# The following classes are for testing basic data
|
||||
# The following classes are for testing basic data
|
||||
# marshalling, including NULL values.
|
||||
|
||||
class BooleanData(models.Model):
|
||||
data = models.BooleanField(null=True)
|
||||
|
||||
|
||||
class CharData(models.Model):
|
||||
data = models.CharField(maxlength=30, null=True)
|
||||
|
||||
|
@ -24,6 +24,9 @@ class DateData(models.Model):
|
|||
class DateTimeData(models.Model):
|
||||
data = models.DateTimeField(null=True)
|
||||
|
||||
class DecimalData(models.Model):
|
||||
data = models.DecimalField(null=True, decimal_places=3, max_digits=5)
|
||||
|
||||
class EmailData(models.Model):
|
||||
data = models.EmailField(null=True)
|
||||
|
||||
|
@ -34,7 +37,7 @@ class FilePathData(models.Model):
|
|||
data = models.FilePathField(null=True)
|
||||
|
||||
class FloatData(models.Model):
|
||||
data = models.FloatField(null=True, decimal_places=3, max_digits=5)
|
||||
data = models.FloatField(null=True)
|
||||
|
||||
class IntegerData(models.Model):
|
||||
data = models.IntegerField(null=True)
|
||||
|
@ -145,6 +148,9 @@ class CharPKData(models.Model):
|
|||
# class DateTimePKData(models.Model):
|
||||
# data = models.DateTimeField(primary_key=True)
|
||||
|
||||
class DecimalPKData(models.Model):
|
||||
data = models.DecimalField(primary_key=True, decimal_places=3, max_digits=5)
|
||||
|
||||
class EmailPKData(models.Model):
|
||||
data = models.EmailField(primary_key=True)
|
||||
|
||||
|
@ -155,7 +161,7 @@ class FilePathPKData(models.Model):
|
|||
data = models.FilePathField(primary_key=True)
|
||||
|
||||
class FloatPKData(models.Model):
|
||||
data = models.FloatField(primary_key=True, decimal_places=3, max_digits=5)
|
||||
data = models.FloatField(primary_key=True)
|
||||
|
||||
class IntegerPKData(models.Model):
|
||||
data = models.IntegerField(primary_key=True)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
A test spanning all the capabilities of all the serializers.
|
||||
|
||||
This class defines sample data and a dynamically generated
|
||||
test case that is capable of testing the capabilities of
|
||||
test case that is capable of testing the capabilities of
|
||||
the serializers. This includes all valid data values, plus
|
||||
forward, backwards and self references.
|
||||
"""
|
||||
|
@ -16,13 +16,17 @@ from django.db import transaction
|
|||
from django.core import management
|
||||
|
||||
from models import *
|
||||
try:
|
||||
import decimal
|
||||
except ImportError:
|
||||
from django.utils import _decimal as decimal
|
||||
|
||||
# A set of functions that can be used to recreate
|
||||
# test data objects of various kinds
|
||||
def data_create(pk, klass, data):
|
||||
instance = klass(id=pk)
|
||||
instance.data = data
|
||||
instance.save()
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def generic_create(pk, klass, data):
|
||||
|
@ -32,13 +36,13 @@ def generic_create(pk, klass, data):
|
|||
for tag in data[1:]:
|
||||
instance.tags.create(data=tag)
|
||||
return instance
|
||||
|
||||
|
||||
def fk_create(pk, klass, data):
|
||||
instance = klass(id=pk)
|
||||
setattr(instance, 'data_id', data)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
def m2m_create(pk, klass, data):
|
||||
instance = klass(id=pk)
|
||||
instance.save()
|
||||
|
@ -61,14 +65,14 @@ def pk_create(pk, klass, data):
|
|||
# test data objects of various kinds
|
||||
def data_compare(testcase, pk, klass, data):
|
||||
instance = klass.objects.get(id=pk)
|
||||
testcase.assertEqual(data, instance.data,
|
||||
testcase.assertEqual(data, instance.data,
|
||||
"Objects with PK=%d not equal; expected '%s' (%s), got '%s' (%s)" % (pk,data, type(data), instance.data, type(instance.data)))
|
||||
|
||||
def generic_compare(testcase, pk, klass, data):
|
||||
instance = klass.objects.get(id=pk)
|
||||
testcase.assertEqual(data[0], instance.data)
|
||||
testcase.assertEqual(data[1:], [t.data for t in instance.tags.all()])
|
||||
|
||||
|
||||
def fk_compare(testcase, pk, klass, data):
|
||||
instance = klass.objects.get(id=pk)
|
||||
testcase.assertEqual(data, instance.data_id)
|
||||
|
@ -84,7 +88,7 @@ def o2o_compare(testcase, pk, klass, data):
|
|||
def pk_compare(testcase, pk, klass, data):
|
||||
instance = klass.objects.get(data=data)
|
||||
testcase.assertEqual(data, instance.data)
|
||||
|
||||
|
||||
# Define some data types. Each data type is
|
||||
# actually a pair of functions; one to create
|
||||
# and one to compare objects of that type
|
||||
|
@ -96,7 +100,7 @@ o2o_obj = (o2o_create, o2o_compare)
|
|||
pk_obj = (pk_create, pk_compare)
|
||||
|
||||
test_data = [
|
||||
# Format: (data type, PK value, Model Class, data)
|
||||
# Format: (data type, PK value, Model Class, data)
|
||||
(data_obj, 1, BooleanData, True),
|
||||
(data_obj, 2, BooleanData, False),
|
||||
(data_obj, 10, CharData, "Test Char Data"),
|
||||
|
@ -115,10 +119,14 @@ test_data = [
|
|||
(data_obj, 51, FileData, None),
|
||||
(data_obj, 60, FilePathData, "/foo/bar/whiz.txt"),
|
||||
(data_obj, 61, FilePathData, None),
|
||||
(data_obj, 70, FloatData, 12.345),
|
||||
(data_obj, 71, FloatData, -12.345),
|
||||
(data_obj, 72, FloatData, 0.0),
|
||||
(data_obj, 73, FloatData, None),
|
||||
(data_obj, 70, DecimalData, decimal.Decimal('12.345')),
|
||||
(data_obj, 71, DecimalData, decimal.Decimal('-12.345')),
|
||||
(data_obj, 72, DecimalData, decimal.Decimal('0.0')),
|
||||
(data_obj, 73, DecimalData, None),
|
||||
(data_obj, 74, FloatData, 12.345),
|
||||
(data_obj, 75, FloatData, -12.345),
|
||||
(data_obj, 76, FloatData, 0.0),
|
||||
(data_obj, 77, FloatData, None),
|
||||
(data_obj, 80, IntegerData, 123456789),
|
||||
(data_obj, 81, IntegerData, -123456789),
|
||||
(data_obj, 82, IntegerData, 0),
|
||||
|
@ -137,10 +145,10 @@ test_data = [
|
|||
(data_obj, 131, PositiveSmallIntegerData, None),
|
||||
(data_obj, 140, SlugData, "this-is-a-slug"),
|
||||
(data_obj, 141, SlugData, None),
|
||||
(data_obj, 150, SmallData, 12),
|
||||
(data_obj, 151, SmallData, -12),
|
||||
(data_obj, 152, SmallData, 0),
|
||||
(data_obj, 153, SmallData, None),
|
||||
(data_obj, 150, SmallData, 12),
|
||||
(data_obj, 151, SmallData, -12),
|
||||
(data_obj, 152, SmallData, 0),
|
||||
(data_obj, 153, SmallData, None),
|
||||
(data_obj, 160, TextData, """This is a long piece of text.
|
||||
It contains line breaks.
|
||||
Several of them.
|
||||
|
@ -188,7 +196,7 @@ The end."""),
|
|||
(fk_obj, 450, FKDataToField, "UAnchor 1"),
|
||||
(fk_obj, 451, FKDataToField, "UAnchor 2"),
|
||||
(fk_obj, 452, FKDataToField, None),
|
||||
|
||||
|
||||
(data_obj, 500, Anchor, "Anchor 3"),
|
||||
(data_obj, 501, Anchor, "Anchor 4"),
|
||||
(data_obj, 502, UniqueAnchor, "UAnchor 2"),
|
||||
|
@ -201,9 +209,12 @@ The end."""),
|
|||
(pk_obj, 640, EmailPKData, "hovercraft@example.com"),
|
||||
(pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
|
||||
(pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"),
|
||||
(pk_obj, 670, FloatPKData, 12.345),
|
||||
(pk_obj, 671, FloatPKData, -12.345),
|
||||
(pk_obj, 672, FloatPKData, 0.0),
|
||||
(pk_obj, 670, DecimalPKData, decimal.Decimal('12.345')),
|
||||
(pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')),
|
||||
(pk_obj, 672, DecimalPKData, decimal.Decimal('0.0')),
|
||||
(pk_obj, 673, FloatPKData, 12.345),
|
||||
(pk_obj, 674, FloatPKData, -12.345),
|
||||
(pk_obj, 675, FloatPKData, 0.0),
|
||||
(pk_obj, 680, IntegerPKData, 123456789),
|
||||
(pk_obj, 681, IntegerPKData, -123456789),
|
||||
(pk_obj, 682, IntegerPKData, 0),
|
||||
|
@ -215,9 +226,9 @@ The end."""),
|
|||
(pk_obj, 720, PositiveIntegerPKData, 123456789),
|
||||
(pk_obj, 730, PositiveSmallIntegerPKData, 12),
|
||||
(pk_obj, 740, SlugPKData, "this-is-a-slug"),
|
||||
(pk_obj, 750, SmallPKData, 12),
|
||||
(pk_obj, 751, SmallPKData, -12),
|
||||
(pk_obj, 752, SmallPKData, 0),
|
||||
(pk_obj, 750, SmallPKData, 12),
|
||||
(pk_obj, 751, SmallPKData, -12),
|
||||
(pk_obj, 752, SmallPKData, 0),
|
||||
# (pk_obj, 760, TextPKData, """This is a long piece of text.
|
||||
# It contains line breaks.
|
||||
# Several of them.
|
||||
|
@ -226,7 +237,7 @@ The end."""),
|
|||
(pk_obj, 780, USStatePKData, "MA"),
|
||||
# (pk_obj, 790, XMLPKData, "<foo></foo>"),
|
||||
]
|
||||
|
||||
|
||||
# Dynamically create serializer tests to ensure that all
|
||||
# registered serializers are automatically tested.
|
||||
class SerializerTests(unittest.TestCase):
|
||||
|
@ -234,7 +245,7 @@ class SerializerTests(unittest.TestCase):
|
|||
|
||||
def serializerTest(format, self):
|
||||
# Clear the database first
|
||||
management.flush(verbosity=0, interactive=False)
|
||||
management.flush(verbosity=0, interactive=False)
|
||||
|
||||
# Create all the objects defined in the test data
|
||||
objects = []
|
||||
|
@ -245,14 +256,14 @@ def serializerTest(format, self):
|
|||
transaction.commit()
|
||||
transaction.leave_transaction_management()
|
||||
|
||||
# Add the generic tagged objects to the object list
|
||||
# Add the generic tagged objects to the object list
|
||||
objects.extend(Tag.objects.all())
|
||||
|
||||
|
||||
# Serialize the test database
|
||||
serialized_data = serializers.serialize(format, objects, indent=2)
|
||||
|
||||
# Flush the database and recreate from the serialized data
|
||||
management.flush(verbosity=0, interactive=False)
|
||||
management.flush(verbosity=0, interactive=False)
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
for obj in serializers.deserialize(format, serialized_data):
|
||||
|
@ -260,10 +271,10 @@ def serializerTest(format, self):
|
|||
transaction.commit()
|
||||
transaction.leave_transaction_management()
|
||||
|
||||
# Assert that the deserialized data is the same
|
||||
# Assert that the deserialized data is the same
|
||||
# as the original source
|
||||
for (func, pk, klass, datum) in test_data:
|
||||
func[1](self, pk, klass, datum)
|
||||
|
||||
|
||||
for format in serializers.get_serializer_formats():
|
||||
setattr(SerializerTests, 'test_'+format+'_serializer', curry(serializerTest, format))
|
||||
|
|
Loading…
Reference in New Issue