Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customizing how a form outputs a field to a widget in django

I have a custom field called HourField, which lets you enter "1:30" to represent one and a half hours. The clean() method turns it into 1.5 for the FloatField in the model. This all works fine. Now I'm having a problem where when I want to create a formset, that field gets rendered as "1.5" instead of "1:30" which is what I want.

Is there some kind of method similar to clean() but works the other way around?

edit: using another type of storage method is out of the question. I have a few other custom fields I have made which are stored as one type of object, but are entered by the user as a string. I used the example above because it was the most straight forward.

like image 565
priestc Avatar asked Sep 20 '25 22:09

priestc


1 Answers

To customize the way a field renders, you need to define a widget as well.

The following code shows some responsibilities of widgets in django :

  • render the form tag ( superclass TextInput takes care of this )
  • format the value for displaying inside the form field. there's an gotcha here : the value's class may vary, since you may render cleaned values coming from the field, or uncleaned data coming from a form POST. Hence the test if we have a numeric value in _format_value ; if it's a string, just leave it as-is.
  • tell if the value has changed, compared to initial data. Very important when you use formsets with default values.

Please note that the widget's code is inspired by widgets.TimeInput from the sourcecode of django, which helps being consistent.

from django import forms
from django.forms import widgets
from django.forms.util import ValidationError

class HourWidget(widgets.TextInput):

    def _format_value(self, value):
        if isinstance(value, float) or isinstance(value, int):
            import math

            hours = math.floor(value)
            minutes = (value - hours) * 60
            value = "%d:%02d" % (hours, minutes)

        return value

    def render(self, name, value, attrs=None):
        value = self._format_value(value)
        return super(HourWidget, self).render(name, value, attrs)

    def _has_changed(self, initial, data):
        return super(HourWidget, self)._has_changed(self._format_value(initial), data)

class HourField(forms.Field):
    widget = HourWidget

    def clean(self, value):
        super(HourField, self).clean(value)

        import re
        match = re.match("^([0-9]{1,2}):([0-9]{2})$", value)
        if not match:
            raise ValidationError("Please enter a valid hour ( ex: 12:34 )")

        groups = match.groups()
        hour = float(groups[0])
        minutes = float(groups[1])

        if minutes >= 60:
            raise ValidationError("Invalid value for minutes")

        return hour + minutes/60

Please note that 1:20 becomes 1:19, which is due to the loss of precision induced by the use of a float. You might want to change the data type if you don't want to lose some precision.

like image 200
3 revsvincent Avatar answered Sep 22 '25 12:09

3 revsvincent