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)
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")
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.