Jul 29, 2014

Django adding custom widget to Django Admin

Sometimes you need to get out of standard behaviour of your Django admin. It means expanding its functionality with custom things. In our case we have a model that we want to add a custom html element. Button that must load something via AJAX from another place to be exact.
We have a typical Django polls application for simplicity. Here is its structure:

First of all we need to create a structure. So here comes our model:
# models.py
from django.db import models

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
We have to have our django application admin configuration to display that model:
# admin.py
from django.contrib import admin
from polls.models import Poll


admin.site.register(Poll)
It will look in Django admin like so:
You can see polls available here in admin. You can do all the typical things with Polls model. Add/Delete/Edit... Using standard Django features. Imagine we have something on that polls model that we need to expand. Say, we need a button to visit external site here, made like an additional edit field. Imagine we need to take something on external website. We can post something to that website from our filled django admin form. To do that we need to create a custom form field. I have not found any standard widgets to do that. It is required to write our own field and attach it to the Django admin form.
We do not need own field here. Just a widget. Widgets are standard Django types. It is possible to read wide description of them in the official documentation: Django Widgets. I wont discuss here best ways to implement this task and there might be better ones. However I have chosen a form widget for simplicity.

It is required to have a model expansion form and a custom widget in order to do so. Registration fo that for m in django admin setup is also required. Lets implement this.
Starting with a form and a widget:
# forms.py
from django import forms
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string

from polls.models import Poll


class ButtonWidget(forms.Widget):
    template_name = 'auth_button_widget.html'

    def render(self, name, value, attrs=None):
        context = {
            'url': '/'
        }
        return mark_safe(render_to_string(self.template_name, context))


class PollsForm(forms.ModelForm):
    button = forms.CharField(widget=ButtonWidget)

    class Meta:
        model = Poll
It is located in a newly created file - forms.py at the django app - polls directory. Here we have a custom written widget that inherits a typical default Django widget (forms.Widget). we have provided it a template - auth_button_widget.html and a custom render() method that provides extra context for that template.
Next comes the PollsForm class with a custom button field that uses that widget. Note here Meta for that model is specified, indicating we are adding that field to the Polls model add/edit form.
Template, residing in all django templates directory will look like so:
{# auth_button_widget.html #}
<a href="{{ url }}">Go Button</a>
We have our form and widget and a template to render that form element. Time to use that form.

Using this form means modifying our admin.py file and registering custom model admin there. It will use our custom form and a widget. Resulting admin.py will look like this:
# admin.py
from django.contrib import admin
from polls.models import Poll
from polls.forms import PollsForm


class PollsAdmin(admin.ModelAdmin):
    form = PollsForm

admin.site.register(Poll, PollsAdmin)
Resulting change will add a custom form to edit or add the Polls model, containing our custom button field. It will look something like so:

Notice the button we have add here. Its just a simple HTML element that needs to be styled and animated by some CSS and JS. It can be done adding Media to our form widget. Resulting new widget will look like this:
# forms.py (partially)
class ButtonWidget(forms.Widget):
    template_name = 'auth_button_widget.html'

    class Media:
        js = (
            'js/admin_button_script.js',
        )
        css = {
            'all': (
                'css/admin_button_widget.css',
            )
        }

    def render(self, name, value, attrs=None):
        context = {
            'url': '/'
        }
        return mark_safe(render_to_string(self.template_name, context))
Its a registration of static files to use in your widget. They will represent a scripting and styling for that field and will be included to the page of django admin form automatically.
For this to function you have to have django static files configured

Content of scripts and styles are out of the scope of this article. Main idea here is to highlight a proper way to add and write a custom widget from the backend perspective.

Comments, suggestions?

2 comments:

  1. Bro thanks a lot. I followed your example and change the widget to a ForeignKey field. The widget is show correctly, but everything is being wrapped around a "", any idea on how I can remove this?

    ReplyDelete
  2. Hey i am getting an Error like TemplateDoesNotExist: auth_button_widget.html
    What to do now?

    ReplyDelete