Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatically round Django's DecimalField according to the max_digits and decimal_places attributes before calling save()

Tags:

python

django

I want to automatically round Django's DecimalField according to the max_digits and decimal_places attributes before calling save() function in ModelForm.

currently using the following:

  • django 1.8
  • python 2.7

What I have tried so far.

https://djangosnippets.org/snippets/10554/


models.py

amount = models.DecimalField(max_digits = 19, decimal_places = 2)

views.py

P.S. gonna apply it in different fields and in different models

data = {"amount" : 100.1234,"name":"John Doe",...}
form = My_form(data)
if form.is_valid(): //the error throws from here.
    form.save()
else:
    raise ValueError(form.errors)

forms.py

I plan to clean the fields in clean() function and do the rounding off of all decimal fields but when I try to print the raw_data, there's no 'amount field'.

class My_form(forms.ModelForm):
    Class Meta:
        model = My_model
        fields = ('amount','name')
    def clean(self):
        raw_data = self.cleaned_data
        print(raw_data) //only prints {'name' : 'John Doe'}
like image 954
aldesabido Avatar asked Jun 22 '16 03:06

aldesabido


3 Answers

You are mainly getting the error because forms.DecimalField has separate validators from models.DecimalField:

data = {'amount': 1.12345 }

class NormalForm(forms.Form):
    amount = forms.DecimalField(max_digits = 19, decimal_places = 2)

normal_form = NormalForm(data)
normal_form.is_valid()  # returns False
normal_form.cleaned_data  # returns {}

and forms.DecimalField is used by default for forms for models with fields of class models.DecimalField. You could do something like this:

from django import forms
from django.db import models
from decimal import Decimal

def round_decimal(value, places):
    if value is not None:
        # see https://docs.python.org/2/library/decimal.html#decimal.Decimal.quantize for options
        return value.quantize(Decimal(10) ** -places)
    return value

class RoundingDecimalFormField(forms.DecimalField):
    def to_python(self, value):
        value = super(RoundingDecimalFormField, self).to_python(value)
        return round_decimal(value, self.decimal_places)

class RoundingDecimalModelField(models.DecimalField):
    def to_python(self, value):
        # you could actually skip implementing this
        value = super(RoundingDecimalModelField, self).to_python(value)
        return round_decimal(value, self.decimal_places)

    def formfield(self, **kwargs):
        defaults = { 'form_class': RoundingDecimalFormField }
        defaults.update(kwargs)
        return super(RoundingDecimalModelField, self).formfield(**kwargs)

Now anywhere you are using models.DecimalField, use RoundingDecimalModelField instead. Any form you use with those models will now also use the custom form field.

class RoundingForm(forms.Form):
    amount = RoundingDecimalFormField(max_digits = 19, decimal_places = 2)

data = {'amount': 1.12345 }

rounding_form = RoundingForm(data)
rounding_form.is_valid()  # returns True
rounding_form.cleaned_data  # returns {'amount': Decimal('1.12')}
like image 80
Vin-G Avatar answered Nov 08 '22 07:11

Vin-G


If you are assigning directly to a model instance, you don't need to worry about it. The field object will quantize the value (rounding it) to the decimal point level you set in your model definition.

If you are dealing with a ModelForm, the default DecimalField will require that any input match the model field's decimal points. The easiest way to handle this in general is probably to subclass the model DecimalField, removing the decimal-specific validator and relying on the underlying conversion to quantize the data, with something like this:

from django.db.models.fields import DecimalField

class RoundingDecimalField(DecimalField):

    @cached_property
    def validators(self):
        return super(DecimalField, self).validators

    def formfield(self, **kwargs):
        defaults = {
            'max_digits': self.max_digits,
            'decimal_places': 4, # or whatever number of decimal places you want your form to accept, make it a param if you like
            'form_class': forms.DecimalField,
        }
        defaults.update(kwargs)
        return super(RoundingDecimalField, self).formfield(**defaults)

Then in your models:

amount = RoundingDecimalField(max_digits = 19, decimal_places = 2)

(Don't actually put the field class in the same field as the model, that's just for example.)

This is probably less correct in absolute terms than defining a custom field form, which was my first suggestion, but is less work to use.

like image 31
Peter DeGlopper Avatar answered Nov 08 '22 07:11

Peter DeGlopper


if you want to set widget to limit decimal number input form render just using this code

from django import forms

class DecimalNumberInput(forms.NumberInput):
    def get_context(self, name, value, attrs):

        context = super().get_context(name, value, attrs)
        try:
            if self.attrs['decimal_places'] and isinstance(self.attrs['decimal_places'], int) :
                context['widget']['value'] = str(round(float(context['widget']['value']),self.attrs['decimal_places']))
        except Exception as e:
            pass
        return context
class NormalForm(forms.Form):
    amount = forms.DecimalField(max_digits = 19, decimal_places = 2 , widget=DecimalNumberInput(attrs={'decimal_places':2}))
like image 1
Aneesh Usman Avatar answered Nov 08 '22 07:11

Aneesh Usman