11👍
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
15👍
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)
- Disable prefers-color-scheme: dark in django admin
- How to find one month later from a date with django?
- Django attribute error. 'module' object has no attribute 'rindex'
- Python HTML to PDF with full support for CSS3 and HTML5
- New chat message notification Django Channels
3👍
My solution
Based on @elachere answer and the Django documentation, this is the code I am using:
from decimal import Decimal
from django import forms
from django.db import models
def ft_strip(d: Decimal) -> Decimal:
return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
class PercentageField(models.DecimalField):
def from_db_value(self, value, expression, connection) -> Decimal | None:
return value if value is None else ft_strip(Decimal(str(value)) * 100)
def get_db_prep_save(self, value, connection):
if value is not None:
value = Decimal(str(value)) / 100
return super(PercentageField, self).get_db_prep_save(value, connection)
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 theclean
method of the form (as stated in the documentation) and one more time byget_db_prep_save
(mentioned here in the documentation). Indeed, it turns out (in Django source code) that theDecimalField
and theFloatField
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).
Overriding django.db.models.fields.Field.pre_save
could have been an alternative, but there is still an issue with the following code: the attribute of a current instance that has just been saved is 100x too small (due to the division in pre_save
) and you’ll have to call instance.refresh_from_db()
, should you require any further use of it.
class PercentageField(models.DecimalField):
def from_db_value(self, value, expression, connection) -> Decimal | None:
return value if value is None else ft_strip(Decimal(str(value)) * 100)
def pre_save(self, model_instance, add):
value = super().pre_save(model_instance, add)
if value is not None:
updated_value = Decimal(str(value)) / 100
setattr(model_instance, self.attname, updated_value)
return updated_value
return None
2👍
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 (e.g. 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.
- How to add attributes to option tags?
- Multiple lookup_fields for django rest framework
- Sending request.user object to ModelForm from class based generic view in Django
- Lock out users after too many failed login attempts
- Should south migration files be added to source control?