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

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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.