pythondjangodjango-formsdjango-modelsdjango-multiwidget

Why is the `compress` method not being called on my subclass of MultiValueField?


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) 

Solution

  • 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).