For a recent Django task, I wanted to create a custom field that used a new widget by default to display the input differently. The basic steps to do this are:
- Create a custom Widget (child class of
django.contrib.admin.widgets.Input
) - Create a custom Form Field (child class of
django.forms.Field
) that uses your custom widget - Create a custom Model Field (child class of
django.db.models.Field
) that uses your custom Form Field - Use the new Model Field in your models
I was on Django 1.10, so it might be easier to newer (ie, supported 😖) versions.
So, say you want a new form field for dates that get reported to a government body, so there are some special considerations when a user wants ot change them. So it needs to be rendered with an extra HTML attribute to indicate the original value, and some javascript to advise the user what changes are ok and which aren’t.
This is what the code would look like:
Custom Widget
from django.contrib.admin import widgets as adminwidgets
class ReportedDateWidget(adminwidgets.AdminDateWidget):
@property
def media(self):
parent_media = super(ReportedDateWidget, self).media
parent_media._js.append('/static/admin/js/jquery.min.js')
parent_media._js.append('/static/js/reported_date_widget.js')
return parent_media
def render(self, name, value, attrs = {}):
if value:
attrs['original_value'] = value
return super(ReportedDateWidget, self).render(name, value, attrs)
So it declares some additional javascript (beyond what the parent, AdminDateWidget
, uses), and when rendering, adds an extra HTML attribute called “original_value” which stores the field’s original value.
Custom Form Field
from django import forms
from common.widgets import ReportedDateWidget
class ReportedDateField(forms.DateField):
def __init__(self, input_formats=None, *args, **kwargs):
super(ReportedDateField, self).__init__(*args, **kwargs)
self.widget = ReportedDateWidget()
This custom form field uses the custom widget. I needed to override the __init__
method, not just declare the property. Go figure.
Custom Model Field
from django.db import models
from common.forms import fields as formfields
class ReportedDateField(models.DateField):
def formfield(self, **kwargs):
defaults = {'form_class': formfields.ReportedDateField}
defaults.update(kwargs)
return models.Field.formfield(self,**defaults)
This makes the custom model field use the custom form field. This was a bit tricky: I couldn’t just update kwargs
to use a different “form_class”, because its parent, DateField
rather rudely overrides it, without checking to see if a different “form_class” was provided to it.
So, to get around that, I needed to **not** call the parent function DateField.formfield()
, but the grandparent function, Field.formfield()
after I set the “form_class”.
Use It in Models
from django.db import models
from common.fields import ReportedDateField
class CustomModel(models.Model):
start_date = ReportedDateField()
That’s It
So, that’s probably a few more steps than you’d have liked, but now you have
- a widget you can use for any form fields,
- a form field you can use for any model fields. It can also have different default validation,
- a model field which you can use from anywhere. It can have custom model logic based on the field type.
Questions or suggestions? Please let me know!