How do you change the default widget for all Django date fields in a ModelForm?

20,866

Solution 1

You can declare an attribute on your ModelForm class, called formfield_callback. This should be a function which takes a Django model Field instance as an argument, and returns a form Field instance to represent it in the form.

Then all you have to do is look to see if the model field passed in is an instance of DateField and, if so, return your custom field/widget. If not, the model field will have a method named formfield that you can call to return its default form field.

So, something like:

def make_custom_datefield(f):
    if isinstance(f, models.DateField):
        # return form field with your custom widget here...
    else:
        return f.formfield(**kwargs)

class SomeForm(forms.ModelForm)
    formfield_callback = make_custom_datefield

    class Meta:
        # normal modelform stuff here...

Solution 2

This article has helped me numerous times.

The meat of it involves overriding the ModelForm's __init__ method, then calling the super class' __init__ method, then adjusting the fields individually.

class PollForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(PollForm, self).__init__(*args, **kwargs)
        self.fields['question'].widget = forms.Textarea()

    class Meta:
        model = Poll

This method may seem more complicated than Vasil's, but it offers the additional benefit of being able to precisely override any attribute on a field without resetting any other attributes by re-declaring it.

UPDATE: Suggested approach could be generalized to change all date fields without typing each name strictly:

from django.forms import fields as formfields
from django.contrib.admin import widgets

class PollForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(PollForm, self).__init__(*args, **kwargs)
        for field_name in self.fields:
            field = self.fields[field_name]
            if isinstance(field, formfields.DateField):
                field.widget = widgets.AdminDateWidget()

    class Meta:
        model = Poll

That worked for me on python3 and django 1.11

Solution 3

Well, making a custom model field just to change it's default form widget is not really the obvious place to start.

You can make your own form widget and override the field in the form, specifying your own widget like in Soviut's answer.

There's also a shorter way:

class ArticleForm(ModelForm):
     pub_date = DateField(widget=MyDateWidget())

     class Meta:
         model = Article

There is an example of how to write form widgets, it's somewhere in the forms package of Django. It's a datepicker with 3 dropdowns.

What I usually do when I just want to add some JavaScript to a standard HTML input element is leave it the way it is and modify it by referencing it's id later with JavaScript. You can easily catch the naming convention for the ids of the input fields Django generates.

You can also just provide the class for the widget when you override it in the form. Then catch them all with jQuery by the class name.

Solution 4

I use JQuery. You only have to look for the 'id' of the fields you want to associate with the date picker and bind them with JQuery and the right display format:

models.py

class ObjectForm(ModelForm):
    class Meta:
        model = Object        
        fields = ['FieldName1','FieldName2']

at the top of the page you render with your view:

<head>
    <link type="text/css" href="/media/css/ui-darkness/jquery-ui-1.8.2.custom.css" rel="Stylesheet" /> 
    <script type="text/javascript" src="/media/js/jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="/media/js/jquery-ui-1.8.2.custom.min.js"></script>
</head>
<script type="text/javascript">
 $(function() {
        $("#id_FieldName1").datepicker({ dateFormat: 'yy-mm-dd' });
        $("#id_FieldName2").datepicker({ dateFormat: 'yy-mm-dd' });
 });
</script>
...
{{form}}

Solution 5

Some might frown at this but to replace the date picker with your custom widget I would crate a monkeypatch app for your project and patch Django itself at runtime. Benefit of this is any 3rd-party apps will be effected as well and so present a uniform interface to the end user without having to modify the third party code:

from django.forms.widgets import DateInput , DateTimeInput, TimeInput
from FOO.widgets import MyjQueryWidget

# be nice and tell you are patching
logger.info("Patching 'DateInput.widget = MyjQueryWidget': Replaces django DateInput to use my new super  'MyjQueryWidget'")

# be nicer and confirm signature of code we are patching and warn if it has changed - uncomment below to get the current hash fingerprint
# raise Exception(hashlib.md5(inspect.getsource(DateInput.widget)).hexdigest()) # uncommet to find latest hash
if not '<enter hexdigest fingerprint here>' == \
        hashlib.md5(inspect.getsource(DateInput.widget)).hexdigest():
    logger.warn("md5 signature of 'DateInput.widget' does not match Django 1.5. There is a slight chance patch "
                    "might be broken so please compare and update this monkeypatch.")

# be nicest and also update __doc__
DateInput.__doc__ = "*Monkeypatched by <app name>*: Replaced django DateInput.widget with my new super  'MyjQueryWidget'" + DateInput.__doc__ 

DateInput.widget = MyjQueryWidget

The above is inspired from my html5monkeypatch I use as part of my projects, take a look at patch_widgets.py and patch_fields.py.

Share:
20,866

Related videos on Youtube

Brian M. Hunt
Author by

Brian M. Hunt

CTO and founder of MinuteBox.

Updated on January 14, 2020

Comments

  • Brian M. Hunt
    Brian M. Hunt over 4 years

    Given a set of typical models:

    # Application A
    from django.db import models
    class TypicalModelA(models.Model):
        the_date = models.DateField()
    
     # Application B
    from django.db import models
    class TypicalModelB(models.Model):
        another_date = models.DateField()
    
    ...
    

    How might one change the default widget for all DateFields to a custom MyDateWidget?

    I'm asking because I want my application to have a jQueryUI datepicker for inputting dates.

    I've considered a custom field that extends django.db.models.DateField with my custom widget. Is this the best way to implement this sort of across-the-board change? Such a change will require specifically importing a special MyDateField into every model, which is labour intensive, prone to developer error (i.e. a few models.DateField's will get through), and in my mind seems like unnecessary duplication of effort. On the other hand, I don't like modifying what could be considered the canonical version of models.DateField.

    Thoughts and input is appreciated.

  • Brian M. Hunt
    Brian M. Hunt about 15 years
    Thanks for the reply. The solution you suggest, leaving the standard HTML, is interesting but still labour intensive, subject to developer error, and requires lots of code duplication. I'm aiming for a solution that eliminates those issues. Any thoughts?
  • Brian M. Hunt
    Brian M. Hunt about 15 years
    Nice reference. Could that be done in a way that doesn't require the "self.fields['question']" line being manually entered for each form? (e.g. for field in self.fields: if isinstance(field,models.DateField): field.widget = mywidget? Cheers
  • ILIA BROUDNO
    ILIA BROUDNO about 15 years
    Well I haven't had the need to something on the scale you are trying (for lots of inputs in the projects) but django admin does it with the datepicker widget and you can peek into the code for django.contrib.admin to see how it does it.
  • Carl Meyer
    Carl Meyer about 15 years
    +1 if this is common behavior you need across multiple forms with DateTimeFields, this is the DRY way to do it.
  • Brian M. Hunt
    Brian M. Hunt about 15 years
    Great stuff. Where is formfield_callback documented?
  • ramusus
    ramusus almost 15 years
    Thanks for this great idea! Inside SomeForm class I have self.instance property with model's object of the current form. Does anyone know how I can get this object inside make_custom_datefield function?
  • jordivador
    jordivador over 14 years
    I suggest that when all you want to do is use your custom form field class, rather than return your own instance, you should do: return f.formfield(form_class=MyFormFieldClass) This will ensure that Django still takes care of initializing the form field using the proper labels, help_text, required-status etc based on the model field; rather than you having to do that yourself.
  • voodoogiant
    voodoogiant about 13 years
    Thanks, James. That helped a lot. I threw up a full example of my implementation on my blog (strattonbrazil.blogspot.com/2011/03/…).
  • KobeJohn
    KobeJohn over 12 years
    Thanks to James for the DRY solution and thanks to voodoogiant for the full implementation on your blog. Works great.
  • Yabi Abdou
    Yabi Abdou over 11 years
    Quick warning to save others time. If you inherit from a custom base form class that defines formfield_callback, BaseForm.formfield_callback won't be called. This is because formfield_callback is called as part of new (i.e., in ModelFormMetaClass). I created a decent workaround for this issue that you can find described here if you're interested: stackoverflow.com/questions/7342925/….
  • Serj Zaharchenko
    Serj Zaharchenko about 9 years
    One more quick notice - default formfield_callback implementation accepts named arguments; so, it is better to add *args and **kwargs in your custom formfield_callback argument list and pass named ones to f.formfield(**kwargs)
  • logicOnAbstractions
    logicOnAbstractions about 3 years
    @BrianM.Hunt (or more likely others wondering) the doc for that is here: docs.djangoproject.com/en/3.1/ref/forms/models. Although frankly it doesn't say much, this answer is at least as useful as the docs.