Making a Django Custom Model Field, Form Field, and Widget

2 minute read

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:

  1. Create a custom Widget (child class of django.contrib.admin.widgets.Input)
  2. Create a custom Form Field (child class of django.forms.Field) that uses your custom widget
  3. Create a custom Model Field (child class of django.db.models.Field) that uses your custom Form Field
  4. 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 kwargsto 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!

Leave a Reply