Ticket #2365: DecimalField-FloatField.diff

File DecimalField-FloatField.diff, 17.1 KB (added by adurdin@…, 9 years ago)

Separate DecimalField and FloatField db and form fields

  • django/db/models/fields/__init__.py

     
    617617    def get_manipulator_field_objs(self):
    618618        return [curry(forms.FilePathField, path=self.path, match=self.match, recursive=self.recursive)]
    619619
    620 class FloatField(Field):
     620class DecimalField(Field):
    621621    empty_strings_allowed = False
    622622    def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs):
    623623        self.max_digits, self.decimal_places = max_digits, decimal_places
    624624        Field.__init__(self, verbose_name, name, **kwargs)
    625625
     626    def to_python(self, value):
     627        try:
     628            import decimal
     629            try:
     630                return decimal.Decimal(value)
     631            except (decimal.InvalidOperation):
     632                raise validators.ValidationError, gettext("This value must be a decimal number.")
     633        except ImportError:
     634            return str(value)           
     635
     636    def _format(self, value):
     637        if isinstance(value, basestring):
     638            return value
     639        else:
     640            return self.format_number(value)
     641   
     642    def format_number(self, value):
     643        """Formats a number into a string with the requisite number of digits and decimal places."""
     644       
     645        num_chars = self.max_digits
     646        # Allow for a decimal point
     647        if self.decimal_places > 0:
     648            num_chars += 1
     649        # Allow for a minus sign
     650        if value < 0:
     651            num_chars += 1
     652       
     653        return "%.*f" % (self.decimal_places, value)
     654
     655    def get_db_prep_save(self, value):
     656        value = self._format(value)
     657
     658        return super(DecimalField, self).get_db_prep_save(value)
     659
     660    def get_db_prep_lookup(self, lookup_type, value):
     661        if lookup_type == 'range':
     662            value = [self._format(v) for v in value]
     663        else:
     664            value = self._format(value)
     665        return super(DecimalField, self).get_db_prep_lookup(lookup_type, value)
     666
    626667    def get_manipulator_field_objs(self):
    627         return [curry(forms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
     668        # TODO change this (because forms.FloatField returns a float)
     669        return [curry(forms.DecimalField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
    628670
     671class FloatField(Field):
     672    empty_strings_allowed = False
     673    def __init__(self, verbose_name=None, name=None, **kwargs):
     674        Field.__init__(self, verbose_name, name, **kwargs)
     675
     676    def get_manipulator_field_objs(self):
     677        return [forms.FloatField]
     678
    629679class ImageField(FileField):
    630680    def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
    631681        self.width_field, self.height_field = width_field, height_field
  • django/db/backends/sqlite3/creation.py

     
    88    'CommaSeparatedIntegerField':   'varchar(%(maxlength)s)',
    99    'DateField':                    'date',
    1010    'DateTimeField':                'datetime',
     11    'DecimalField':                 'numeric(%(max_digits)s, %(decimal_places)s)',
    1112    'FileField':                    'varchar(100)',
    1213    'FilePathField':                'varchar(100)',
    13     'FloatField':                   'numeric(%(max_digits)s, %(decimal_places)s)',
     14    'FloatField':                   'numeric',
    1415    'ImageField':                   'varchar(100)',
    1516    'IntegerField':                 'integer',
    1617    'IPAddressField':               'char(15)',
  • django/db/backends/mysql/base.py

     
    2222    FIELD_TYPE.DATETIME: util.typecast_timestamp,
    2323    FIELD_TYPE.DATE: util.typecast_date,
    2424    FIELD_TYPE.TIME: util.typecast_time,
     25    FIELD_TYPE.DECIMAL: None,
     26    FIELD_TYPE.NEWDECIMAL: None
    2527})
    2628
     29
    2730# This is an extra debug layer over MySQL queries, to display warnings.
    2831# It's only used when DEBUG=True.
    2932class MysqlDebugWrapper:
  • django/db/backends/mysql/introspection.py

     
    7575DATA_TYPES_REVERSE = {
    7676    FIELD_TYPE.BLOB: 'TextField',
    7777    FIELD_TYPE.CHAR: 'CharField',
    78     FIELD_TYPE.DECIMAL: 'FloatField',
     78    FIELD_TYPE.DECIMAL: 'DecimalField',
     79    FIELD_TYPE.NEWDECIMAL: 'DecimalField',
    7980    FIELD_TYPE.DATE: 'DateField',
    8081    FIELD_TYPE.DATETIME: 'DateTimeField',
    8182    FIELD_TYPE.DOUBLE: 'FloatField',
  • django/db/backends/mysql/creation.py

     
    99    'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
    1010    'DateField':         'date',
    1111    'DateTimeField':     'datetime',
     12    'DecimalField':      'decimal(%(max_digits)s, %(decimal_places)s)',
    1213    'FileField':         'varchar(100)',
    1314    'FilePathField':     'varchar(100)',
    14     'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
     15    'FloatField':        'double',
    1516    'ImageField':        'varchar(100)',
    1617    'IntegerField':      'integer',
    1718    'IPAddressField':    'char(15)',
  • django/forms/__init__.py

     
    739739            raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.")
    740740
    741741class FloatField(TextField):
     742    def __init__(self, field_name, is_required=False, validator_list=None):
     743        if validator_list is None: validator_list = []
     744        validator_list = [validators.isValidFloat] + validator_list
     745        TextField.__init__(self, field_name, is_required=is_required, validator_list=validator_list)
     746
     747    def html2python(data):
     748        if data == '' or data is None:
     749            return None
     750        return float(data)
     751    html2python = staticmethod(html2python)
     752
     753class DecimalField(TextField):
    742754    def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None):
    743755        if validator_list is None: validator_list = []
    744756        self.max_digits, self.decimal_places = max_digits, decimal_places
    745         validator_list = [self.isValidFloat] + validator_list
    746         TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list)
     757        validator_list = [self.isValidDecimal] + validator_list
     758        # Initialise the TextField, making sure it's large enough to fit the number with a - sign and a decimal point.
     759        super(DecimalField, self).__init__(field_name, max_digits+2, max_digits+2, is_required, validator_list)
    747760
    748     def isValidFloat(self, field_data, all_data):
    749         v = validators.IsValidFloat(self.max_digits, self.decimal_places)
     761    def isValidDecimal(self, field_data, all_data):
     762        v = validators.IsValidDecimal(self.max_digits, self.decimal_places)
    750763        try:
    751764            v(field_data, all_data)
    752765        except validators.ValidationError, e:
     
    755768    def html2python(data):
    756769        if data == '' or data is None:
    757770            return None
    758         return float(data)
     771        try:
     772            import decimal
     773            try:
     774                return decimal.Decimal(data)
     775            except decimal.InvalidOperation, e:
     776                raise ValueError, e
     777        except ImportError:
     778            return str(data)
    759779    html2python = staticmethod(html2python)
    760780
    761781####################
  • django/core/validators.py

     
    2424    r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"  # dot-atom
    2525    r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
    2626    r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE)  # domain
     27decimal_re = re.compile(r'^-?(?P<digits>\d+)(\.(?P<decimals>\d+))?$')
    2728integer_re = re.compile(r'^-?\d+$')
    2829ip4_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}$')
    2930phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE)
     
    342343        if val != int(val):
    343344            raise ValidationError, gettext("This value must be a power of %s.") % self.power_of
    344345
    345 class IsValidFloat(object):
     346class IsValidDecimal(object):
    346347    def __init__(self, max_digits, decimal_places):
    347348        self.max_digits, self.decimal_places = max_digits, decimal_places
    348349
    349350    def __call__(self, field_data, all_data):
    350         data = str(field_data)
    351         try:
    352             float(data)
    353         except ValueError:
     351        match = decimal_re.search(str(field_data))
     352        if not match:
    354353            raise ValidationError, gettext("Please enter a valid decimal number.")
    355         if len(data) > (self.max_digits + 1):
     354       
     355        digits = len(match.group('digits') or '')
     356        decimals = len(match.group('decimals') or '')
     357       
     358        if digits + decimals > self.max_digits:
    356359            raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.",
    357360                "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits
    358         if (not '.' in data and len(data) > (self.max_digits - self.decimal_places)) or ('.' in data and len(data) > (self.max_digits - (self.decimal_places - len(data.split('.')[1])) + 1)):
     361        if digits > (self.max_digits - self.decimal_places):
    359362            raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.",
    360363                "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)
    361         if '.' in data and len(data.split('.')[1]) > self.decimal_places:
     364        if decimals > self.decimal_places:
    362365            raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.",
    363366                "Please enter a valid decimal number with at most %s decimal places.", self.decimal_places) % self.decimal_places
    364367
     368def isValidFloat(field_data, all_data):
     369    data = str(field_data)
     370    try:
     371        float(data)
     372    except ValueError:
     373        raise ValidationError, gettext("Please enter a valid floating point number.")
     374
    365375class HasAllowableSize(object):
    366376    """
    367377    Checks that the file-upload field data is a certain size. min_size and
  • django/core/management.py

     
    756756                if field_type == 'CharField' and row[3]:
    757757                    extra_params['maxlength'] = row[3]
    758758
    759                 if field_type == 'FloatField':
     759                if field_type == 'DecimalField':
    760760                    extra_params['max_digits'] = row[4]
    761761                    extra_params['decimal_places'] = row[5]
    762762
     
    830830                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)
    831831            if isinstance(f, models.CharField) and f.maxlength in (None, 0):
    832832                e.add(opts, '"%s": CharFields require a "maxlength" attribute.' % f.name)
    833             if isinstance(f, models.FloatField):
     833            if isinstance(f, models.DecimalField):
    834834                if f.decimal_places is None:
    835                     e.add(opts, '"%s": FloatFields require a "decimal_places" attribute.' % f.name)
     835                    e.add(opts, '"%s": DecimalFields require a "decimal_places" attribute.' % f.name)
    836836                if f.max_digits is None:
    837                     e.add(opts, '"%s": FloatFields require a "max_digits" attribute.' % f.name)
     837                    e.add(opts, '"%s": DecimalFields require a "max_digits" attribute.' % f.name)
    838838            if isinstance(f, models.FileField) and not f.upload_to:
    839839                e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name)
    840840            if isinstance(f, models.ImageField):
  • django/contrib/admin/templatetags/admin_list.py

     
    149149            elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
    150150                BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
    151151                result_repr = '<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)
    152             # FloatFields are special: Zero-pad the decimals.
    153             elif isinstance(f, models.FloatField):
     152            # DecimalFields are special: Zero-pad the decimals.
     153            elif isinstance(f, models.DecimalField):
    154154                if field_val is not None:
    155155                    result_repr = ('%%.%sf' % f.decimal_places) % field_val
    156156                else:
  • django/contrib/admin/views/doc.py

     
    294294    'CommaSeparatedIntegerField': _('Comma-separated integers'),
    295295    'DateField'                 : _('Date (without time)'),
    296296    'DateTimeField'             : _('Date (with time)'),
     297    'DecimalField'              : _('Decimal number'),
    297298    'EmailField'                : _('E-mail address'),
    298299    'FileField'                 : _('File path'),
    299300    'FilePathField'             : _('File path'),
    300     'FloatField'                : _('Decimal number'),
     301    'FloatField'                : _('Floating point number'),
    301302    'ForeignKey'                : _('Integer'),
    302303    'ImageField'                : _('File path'),
    303304    'IntegerField'              : _('Integer'),
  • tests/modeltests/invalid_models/models.py

     
    88
    99class FieldErrors(models.Model):
    1010    charfield = models.CharField()
    11     floatfield = models.FloatField()
     11    decimalfield = models.DecimalField()
    1212    filefield = models.FileField()
    1313    prepopulate = models.CharField(maxlength=10, prepopulate_from='bad')
    1414    choices = models.CharField(maxlength=10, choices='bad')
     
    7979
    8080
    8181error_log = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
    82 invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
    83 invalid_models.fielderrors: "floatfield": FloatFields require a "max_digits" attribute.
     82invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
     83invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
    8484invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute.
    8585invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple.
    8686invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
  • docs/forms.txt

     
    503503    * isNotEmpty
    504504    * isOnlyDigits
    505505    * isNotOnlyDigits
    506     * isInteger
     506    * isInteger   
    507507    * isOnlyLetters
    508508    * isValidANSIDate
    509509    * isValidANSITime
    510510    * isValidEmail
     511    * isValidFloat   
    511512    * isValidImage
    512513    * isValidImageURL
    513514    * isValidPhone
     
    588589    Takes an integer argument and when called as a validator, checks that the
    589590    field being validated is a power of the integer.
    590591
    591 ``IsValidFloat``
     592``IsValidDecimal``
    592593    Takes a maximum number of digits and number of decimal places (in that
    593     order) and validates whether the field is a float with less than the
    594     maximum number of digits and decimal place.
     594    order) and validates whether the field is a decimal with no more than the
     595    maximum number of digits and decimal places.
    595596
     597``IsValidFloat``
     598    Takes a string argument and validates whether it can be converted to a
     599    float.
     600
    596601``MatchesRegularExpression``
    597602    Takes a regular expression (a string) as a parameter and validates the
    598603    field value against it.
Back to Top