简体   繁体   中英

Django percentage field

I'm trying to create a percentage field in Django, where the user just fills in 40 for 40%. There will be a percentage sign on the right of the input box so that they know they should fill in a percentage. 0.4 must be stored in the DB. So far I've tried the following:

class PercentageField(fields.FloatField):
    widget = fields.TextInput(attrs={"class": "percentInput"})

    def to_python(self, value):
        val = super(PercentageField, self).to_python(value)
        if is_number(val):
            return val/100
        return val

    def prepare_value(self, value):
        val = super(PercentageField, self).prepare_value(value)
        if is_number(val):
            return str((float(val)*100))
        return val

def is_number(s):
    if s is None:
        return False
    try:
        float(s)
        return True
    except ValueError:
        return False

It works, but the problem is, when I post invalid data and the form is rendered again, it displays the 40 as 4000. In other words it multiplies the number again with 100 without dividing it as well.

Any suggestions how I can fix it?

I've tried this solution, but it repeats the value 100 times. It also has the same problem after I've corrected that.

I'm using Python3.5

I found the solution. I have to check whether the incoming value is a string. If it is, I don't multiply by 100 since it came from the form. See below:

class PercentageField(fields.FloatField):
    widget = fields.TextInput(attrs={"class": "percentInput"})

    def to_python(self, value):
        val = super(PercentageField, self).to_python(value)
        if is_number(val):
            return val/100
        return val

    def prepare_value(self, value):
        val = super(PercentageField, self).prepare_value(value)
        if is_number(val) and not isinstance(val, str):
            return str((float(val)*100))
        return val

From the documentation, to_python() is supposed to be used in case you're dealing with complex data types, to help you interact with your database. A more accurate approach I think is to override the pre_save() Field method. From the documentation:

pre_save(model_instance, add)

Method called prior to get_db_prep_save() to prepare the value before being saved (eg for DateField.auto_now).

In the end, it looks like this:

def validate_ratio(value):
    try:
        if not (0 <= value <= 100):
            raise ValidationError(
                f'{value} must be between 0 and 100', params={'value': value}
            )
    except TypeError:
        raise ValidationError(
            f'{value} must be a number', params={'value': value}
        )


class RatioField(FloatField):
    description = 'A ratio field to represent a percentage value as a float'

    def __init__(self, *args, **kwargs):
        kwargs['validators'] = [validate_ratio]
        super().__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        value = getattr(model_instance, self.attname)
        if value > 1:
            value /= 100
        setattr(model_instance, self.attname, value)
        return value

My case is a bit different, I want a ratio and not a percentage so I'm allowing only values between 0 and 100, that's why I need a validator, but the idea is here.

There's an easy alternative for this task. You can use MaxValueValidator and MinValueValidator for this.

Here's how you can do this:

from django.db import models    
from django.core.validators import MinValueValidator, MaxValueValidator
        
PERCENTAGE_VALIDATOR = [MinValueValidator(0), MaxValueValidator(100)]
        
class RatingModel(models.Model):
    ...
    rate_field = models.DecimalField(max_digits=3, decimal_places=0, default=Decimal(0), validators=PERCENTAGE_VALIDATOR)

My solution

Based on @elachere answer and the Django documentation , this is the code I used:

from decimal import Decimal

from django import forms
from django.db import models


class PercentageField(models.DecimalField):
    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return Decimal(value) * Decimal("100")

    def pre_save(self, model_instance, add):
        value = super().pre_save(model_instance, add)
        setattr(model_instance, self.attname, Decimal(value) * Decimal(".01"))
        return Decimal(value) * Decimal(".01")

Why the accepted answer did not work for me

I ran into a similar issue but I wanted my PercentageField to be based on a DecimalField instead of a FloatField , in accordance with recommendations when it comes to currencies. In this context, the currently accepted answer did not work for me with Django 4.0, for 2 reasons:

  • to_python is called twice, once by the clean method of the form (as stated in the documentation ) and one more time by get_db_prep_save (mentioned here in the documentation). Indeed, it turns out (in Django source code ) that the DecimalField and the FloatField differ on this point.
  • prepare_value isn't run at all for forms based on an existing instance (that users might be willing to edit, for instance).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM