Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Replicating Django’s admin form pop-ups

Jerry Stratton, October 11, 2008

Django’s built-in admin interface comes with a neat feature that lets you dynamically add new related items—ForeignKey fields or ManyToManyField fields—to the pull-down menu (or multiple-select in the case of ManyToManyFields). There’s no documentation on this, and the pieces are spread across a couple of admin and form files in the source, but it turns out to be pretty easy to leverage this functionality for your own custom forms.

  1. Include the correct JavaScript file in your form template.
  2. Override the appropriate Select widget to add the “plus” icon.
  3. Set the form field to use your custom widget.
  4. Create a template for your “add new” pop-up form.
  5. Make a view to go along with the template.

I’m assuming in these instructions that you already have a working form, but there’s a SELECT on it that you’d like to have an “add new” option.

Include the correct JavaScript file

You can re-use the JavaScript file that Django admin uses to perform the pop-up and object insertion. Just include RelatedObjectLookups.js in each page that either needs a pop-up or is a pop-up.

  • <script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>

This (and the remaining steps) assume that you’ve set up the admin app so that you have the admin’s /media files available. It also probably assumes Django 1.0.

You’ll need to look in your settings.py file for ADMIN_MEDIA_PREFIX to see where your set up expects the admin’s /media files to be. Here, I’m assuming that they’re in /media.

Override forms.Select or forms.SelectMultiple

You’ll need to create one template file to hold the pop-up button. The pop-up button is the same for each type of select; the only difference between these two widgets is their parent widget. (In fact, as far as I can tell, the admin pages use a wrapper so that it doesn’t need two basically duplicate inheritors.)

[toggle code]

  • <a
    • href="/add/{{ field }}"
    • class="add-another"
    • id="add_id_{{ field }}"
    • onclick="return showAddAnotherPopup(this);">
      • <img src="/media/img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/>
  • </a>

In the above example, the URL for adding an item is /add/FIELDNAME. For example, if the field name is “contact”, the URL for adding a new contact is /add/contact. Obviously, you can modify the URL for your project. Other than that, this is a standard IMG tag, referencing the “plus” icon from the admin app. I pulled this straight from an example admin page for my project.

Now, we need to override forms.Select and/or forms.SelectMultiple to append this pop-up button to the HTML returned by those widgets.

[toggle code]

  • from django.template.loader import render_to_string
  • import django.forms as forms
  • class SelectWithPop(forms.Select):
    • def render(self, name, *args, **kwargs):
      • html = super(SelectWithPop, self).render(name, *args, **kwargs)
      • popupplus = render_to_string("form/popupplus.html", {'field': name})
      • return html+popupplus
  • class MultipleSelectWithPop(forms.SelectMultiple):
    • def render(self, name, *args, **kwargs):
      • html = super(MultipleSelectWithPop, self).render(name, *args, **kwargs)
      • popupplus = render_to_string("form/popupplus.html", {'field': name})
      • return html+popupplus

This assumes that you’ve placed “popupplus.html” (the IMG template above) into your templates folder in a folder called “form”. These widgets can be used in place of the regular forms.Select and forms.SelectMultiple.

Use your new custom widget.

When you set up a field, you can specify a widget other than the default widget that goes with that field type. Here’s an example using forms.ModelForm. All of the fields except contact and tags use the standard widgets, but contact is going to use our new SelectWithPop, and tags will use our new MultipleSelectWithPop. They’re for selecting from existing or add new new Contact or Tag instances from the project called “projects”.

In my views.py for projects, I modify the Task ModelForm so that tags and contact use the new widgets:

[toggle code]

  • from myapps.projects.models import Task, Contact, Tag
  • class projectForm(forms.ModelForm):
    • contact = forms.ModelChoiceField(Contact.objects, widget=SelectWithPop)
    • tags = forms.ModelMultipleChoiceField(Tag.objects, required=False, widget=MultipleSelectWithPop)
    • class Meta:
      • model = Task
      • fields = ['title', 'parent', 'details', 'assignees', 'contact', 'tags']

If you do this to your own form, then at this point you should be able to view your form and it will have the standard admin interface “+” graphic next to each of the fields you used the new widgets with.

Create a pop-up template

If you try to use the pop-up button, however, it won’t work because we need to make a template and view for it. The pop-up form is just like any other form in Django, but you’ll need to include RelatedObjectLookups.js just like you did for the main form.

Normally, you’ll also want this form to be a very simple form, without all of the navigation, footer, and headers that your “real” pages have.

[toggle code]

  • <html>
    • <head>
      • <title>Add {{ field }}</title>
      • <script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
    • </head>
    • <body>
      • <h1>Add {{ field }}</h1>
      • <form method="POST" action="/add/{{ field }}">
        • <table>
          • {{ form }}
        • </table>
        • <p><input type="submit" /> | <a href="javascript:window.close()">Cancel</a></p>
      • </form>
    • </body>
  • </html>

You’ll probably want to add your own style sheet to this and make it not quite as simple as that. Notice that I’ll be able to use the same template for adding any item, as long as I pass the fieldname in “field”.

Make a view for adding new items

The add new item views are just like any other view. You’ll need a form object to go along with them. Here’s an example for my Contact and Tag add forms:

[toggle code]

  • #add new contact pop-up
  • class contactForm(forms.ModelForm):
    • class Meta:
      • model = Contact
  • #add new tag pop-up
  • class tagForm(forms.ModelForm):
    • class Meta:
      • model = Tag
      • fields = ['title']

The tricky bit when saving a pop-up is that we want the pop-up to disappear after a successful save. We can do this by returning a javascript snippet. I’m not sure how this works. It appears that after getting the new page, the old JavaScript files must still be accessible. That seems very strange to me, not to mention dangerous. But that’s how it works in the admin, and it works to do it here, too.

The pop-up save is exactly the same for both the Contact and Tag models, so I’ve made a single function for handling the popup; it needs the form that’s being displayed and the name of the field.

[toggle code]

  • from django.utils.html import escape
  • def handlePopAdd(request, addForm, field):
    • if request.method == "POST":
      • form = addForm(request.POST)
      • if form.is_valid():
        • try:
          • newObject = form.save()
        • except forms.ValidationError, error:
          • newObject = None
        • if newObject:
          • return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
            • (escape(newObject._get_pk_val()), escape(newObject)))
    • else:
      • form = addForm()
    • pageContext = {'form': form, 'field': field}
    • return render_to_response("add/popadd.html", pageContext)

And then, the actual views:

[toggle code]

  • from django.contrib.auth.decorators import login_required
  • @login_required
  • def newContact(request):
    • return handlePopAdd(request, contactForm, 'contact')
  • @login_required
  • def newTag(request):
    • return handlePopAdd(request, tagForm, 'tags')

And the views need to be added to urls.py, probably as something like:

  • (r'^add/contact/?$', 'projects.views.newContact'),
  • (r'^add/tags/?$', 'projects.views.newTag'),

And that’s it. If you’ve done this to something in your own form, you should now be able to click the pop-up button, add a new item, save it, and see the new item in the pull-down or select-multiple on the main form.

Take it further

They are stackable. So if your pop-up also needs a pop-up, you can just repeat these steps for your pop-up view. For example, if (as it so happens, it does), my Contact pop-up can have a Department pop-up, that’s not a problem.

[toggle code]

  • from myapps.projects.models import Department
  • #add new department pop-up
  • class departmentForm(forms.ModelForm):
    • class Meta:
      • model = Department
  • #add new contact pop-up
  • class contactForm(forms.ModelForm):
    • department = forms.ModelChoiceField(Department.objects, required=False, widget=SelectWithPop)
    • class Meta:
      • model = Contact
  • @login_required
  • def newDepartment(request):
    • return handlePopAdd(request, departmentForm, 'department')

In urls.py, add:

  • (r'^add/department/?$', 'projects.views.newDepartment'),

You can have pretty much as many levels as you want. The only oddity (at least for me) is that the new pop-ups tend to be right on top of the previous one, making it look as though it’s been replaced rather than stacked.

Modified to include the import of “escape” from django.utils.html. Also, noting that you can find the location of your admin media directory in your settings.py file.

January 7, 2010: Reusing Django’s filter_horizontal

Django’s admin site documentation describes ModelAdmin’s filter_horizontal option as a “nifty unobtrusive JavaScript” to use in place of “the usability-challenged <select multiple>”. HTML’s multiple select does indeed suck for any number of options that require scrolling. Inevitably, when editing an existing entry, you or your users will eventually erase an existing option without knowing it.

Django’s horizontal and vertical filter solutions change these select boxes into one box of unselected options and one box of selected options, making the selected options much more obvious, and making it pretty much impossible to accidentally remove an existing selection.

You can use this JavaScript in your own forms. It consists of several JavaScript files, one CSS file, and a snippet of HTML right next to the pop-up button.

JavaScript and CSS

Assuming that you’ve made use of Django’s popup add form, you already have RelatedObjectLookups.js on your template somewhere. Add several more JavaScript files as well as one CSS file from Django’s built-in library:

  • <script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
  • <script type="text/javascript" src="/admin/jsi18n/"></script>
  • <script type="text/javascript" src="/media/js/core.js"></script>
  • <script type="text/javascript" src="/media/js/SelectBox.js"></script>
  • <script type="text/javascript" src="/media/js/SelectFilter2.js"></script>
  • <link rel="stylesheet" type="text/css" href="/media/css/widgets.css" />

Call the JavaScript

If you’re using the admin-form pop-ups as I described earlier in Replicating Django’s admin form pop-ups, you have a template snippet called “form/popupplus.html”. This template is called by both SelectWithPop and MultipleSelectWithPop. Only MultipleSelectWithPop needs filter_horizontal, so add a flag to that class’s render method’s context:

[toggle code]

  • class MultipleSelectWithPop(forms.SelectMultiple):
    • def render(self, name, *args, **kwargs):
      • html = super(MultipleSelectWithPop, self).render(name, *args, **kwargs)
      • popupplus = render_to_string("form/popupplus.html", {'field': name, 'multiple': True})
      • return html+popupplus

And then, inside of popupplus.html, call the SelectFilter JavaScript:

  1. <- Upgrading Django
  2. Nisus HTML ->