I've implemented a custom DurationField
in Django that stores an integer that represents a duration in number of seconds. I've also defined a custom multiwidget that takes in five different number inputs (representing the number of weeks, days, hours, minutes, and seconds).
I've managed to get it working in the admin, and decompress
appears to be working as expected; when I alter the values in the interpreter and save them, they appear correctly in the five fields. However, compress
doesn't seem to be getting called; when I attempt to change the values (or simply save the form without changing them), I get the Enter a whole number.
error from IntegerField
. It appears as though the entire list of values (e.g. [u'0', u'1', u'2', u'0', u'0']
) is being passed to to_python
.
I feel like I'm missing/overlooking something small, but I think I've been staring at my code for too long to figure out what it could be.
Here's my model_fields.py
:
from datetime import timedelta
from django.db.models import PositiveIntegerField
from django.forms import IntegerField, MultiWidget
from django.forms.fields import MultiValueField
from django.forms.widgets import TextInput # NumberInput in Django 1.6
DURATION_FORM_FIELDS = ('weeks', 'days', 'hours', 'minutes', 'seconds')
class DurationMultiWidget(MultiWidget):
def __init__(self, attrs=None):
_widgets = tuple(
[TextInput(attrs=attrs) for field in DURATION_FORM_FIELDS]
)
super(DurationMultiWidget, self).__init__(_widgets, attrs)
def decompress(self, value):
if value:
td = timedelta(seconds=value)
return [
getattr(td, label, 0) for label in DURATION_FORM_FIELDS
]
return [None for field in DURATION_FORM_FIELDS]
class DurationMultiValueField(MultiValueField):
def __init__(self, *args, **kwargs):
fields = tuple(
[IntegerField(label=field_label) for field_label in DURATION_FORM_FIELDS]
)
super(DurationMultiValueField, self).__init__(fields=fields, *args, **kwargs)
def compress(self, data_list):
duration_dict = dict(zip(DURATION_FORM_FIELDS, data_list))
timedelta_object = timedelta(**duration_dict)
return int(timedelta_object.total_seconds())
class DurationField(PositiveIntegerField):
def get_internal_type(self):
return 'DurationField'
def formfield(self, **kwargs):
defaults = {'form_class': DurationMultiValueField}
defaults.update(kwargs)
return super(DurationField, self).formfield(**kwargs)
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^polls\.model_fields\.DurationField"])
Here's admin.py
:
from django import forms
from django.contrib import admin
from .models import Poll
from .model_fields import DurationMultiWidget
class PollAdminForm(forms.ModelForm):
class Meta:
model = Poll
widgets = {
'limit': DurationMultiWidget(),
}
...
class PollAdmin(admin.ModelAdmin):
form = PollAdminForm
admin.site.register(Poll, PollAdmin)
And here's models.py
, just in case:
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Poll(models.Model):
...
limit = DurationField(_('limit'), default=0)
Ugh, it was a typo: under the formfield()
method, I accidentally passed in **kwargs
instead of **defaults
.
As an aside, in the rare instance that someone is trying to do the exact same thing, you have to do a kwargs.pop('min_value')
in the DurationMultiValueField.__init__
(since it's a PositiveIntegerField).