Johannes Pertl
Johannes Pertl

Reputation: 983

Use different decimal separators for frontend validation in Django Admin interface

I looked through every single similar question on stackoverflow and tried nearly everything. It seems easy to do: I just want to allow , as decimal separator for a FloatField in the Django admin interface. At the moment, it depends on the localization, but I always want to allow it. It would even be ok for me, if it's just a TextInput, but I need , to work. Setting DECIMAL_SEPARATOR in settings.py does not work.

My question is similar to this 6 year old, unanswered one: How to only override DECIMAL_SEPARATOR in django while keeping localization intact?

I managed to use the TextInput widget for FloatFields like this:

class ExampleAdminForm(forms.ModelForm):
    class Meta:
        model = Example

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for key, value in self.fields.items():
            if isinstance(value, FloatField):
                self.fields[key].widget = TextInput()  

The widget works, but an input like 1,23 leads to an error message Enter a number. I'd have to implement my own, custom FloatField that overrides the sanitizing in to_python to allow for differenct decimal separators, but then I'd still need a different widget. This seems awfully complicated, is there a better way? If not, how can I change the frontend validation in the widget to ignore localization and use my own regex pattern?

Upvotes: 1

Views: 338

Answers (1)

aaron
aaron

Reputation: 43083

Patch the to_python method, or use a custom form field class that overrides that method.

One-liner answer:

self.fields[key].to_python = lambda v: self.fields[key].__class__.to_python(self.fields[key], '.'.join(v.rsplit(',', 1)) if len(v.rsplit(',', 1)[-1]) < 3 else v)

As a wrapper function:

self.fields[key].to_python = allow_comma_decimal_separator(self.fields[key].to_python)
def allow_comma_decimal_separator(old_to_python):
    def to_python(value):
        if ',' in value:
            lvalue, decimals = value.rsplit(',', 1)
            if len(decimals) < 3:
                value = '.'.join((lvalue, decimals))
        return old_to_python(value)
    return to_python

i.e. If a comma is in the value, and the substring after the rightmost comma has a length of less than 3 (so we assume it's not a thousand separator), then we replace that comma with a period by joining the substring before and after with a period.

For explicit fields using a reusable form field class

This would be considered less "hacky".

class AllowCommaDecimalSeparatorFloatField(forms.FloatField):

    def to_python(self, value):
        if ',' in value:
            lvalue, decimals = value.rsplit(',', 1)
            if len(decimals) < 3:
                value = '.'.join((lvalue, decimals))
        return super().to_python(value)

In your form's Meta class:

field_classes = {
    'myfield': AllowCommaDecimalSeparatorFloatField,
}

For all FloatFields

To affect all FloatField instances (not instantiated yet), place this at the top of your module:

old_to_python = forms.FloatField.to_python


def to_python(self, value):
    if ',' in value:
        lvalue, decimals = value.rsplit(',', 1)
        if len(decimals) < 3:
            value = '.'.join((lvalue, decimals))
    return old_to_python(self, value)


forms.FloatField.to_python = to_python

Upvotes: 1

Related Questions