Replicating Django’s admin form pop-ups
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.
- Include the correct JavaScript file in your form template.
- Override the appropriate Select widget to add the “plus” icon.
- Set the form field to use your custom widget.
- Create a template for your “add new” pop-up form.
- 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
-
def render(self, name, *args, **kwargs):
-
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
-
def render(self, name, *args, **kwargs):
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>
-
<table>
- </form>
- </body>
-
<head>
- </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
-
class Meta:
- #add new tag pop-up
-
class tagForm(forms.ModelForm):
-
class Meta:
- model = Tag
- fields = ['title']
-
class Meta:
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)))
-
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
-
try:
-
else:
- form = addForm()
- pageContext = {'form': form, 'field': field}
- return render_to_response("add/popadd.html", pageContext)
-
if request.method == "POST":
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
-
class Meta:
- #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
-
def render(self, name, *args, **kwargs):
And then, inside of popupplus.html, call the SelectFilter JavaScript:
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
im getting this error when i click on the add-icon. and i dont understand whats wrong in the call. im using django 1.1.1
TypeError at /popadd/topics/
'str' object is not callable
Request Method: GET
Request URL: http://127.0.0.1:8000/de/popadd/topics/?_popup=1
Exception Type: TypeError
Exception Value:
'str' object is not callable
Exception Location: /home/pepe/DEV/FSlabs/parts/django/django/core/handlers/base.py in get_response, line 92
Python Executable: /usr/bin/python
Python Version: 2.6.4
andreas at 8:10 a.m. December 1st, 2009
88z4+
What that means is that Python is getting a string where it's expecting a callable object such as a function or, I think in this case, a request object. The error should list a bunch of other lines in the order that they were called (a stack traceback). As you move further and further from base.py, one of the lines should be in your code rather than in Django's. What line is that?
capvideo at 2:49 p.m. December 1st, 2009
tVAhq
I was getting some error when trying to add the widgets to the form fields. Specificically these lines were causing some issues:
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']
I was able to get it working in this format though:
class projectForm(forms.ModelForm):
class Meta:
model = Task
fields = ['title', 'parent', 'details', 'assignees', 'contact', 'tags']
widgets = {'contact':SelectWithPop, 'tags':MultipleSelectWithPop}
Hopefully this can help someone (I'm new to Django so this took a bit of digging to get working)
dsclementsen at 12:05 a.m. May 25th, 2011
bapvG
Hi,
Really usefull stuff. I didnt see any licensing info for this - can i treat this as i would open-source BSD?
lg at 3:58 p.m. July 27th, 2011
sp5Un
Look at the bottom of the page for the license info; currently, GNU GPL 3.
capvideo at 11:50 p.m. July 27th, 2011
tVAhq
Would be helpful if you said what names you used and what files this stuff belongs in.
anonymous at 11:05 p.m. November 13th, 2011
8ro89
Where I know what file it needs to go in, I do mention it; urls.py, for example. But most of them will go in your custom files, and I can’t know what those are named.
capvideo at 6:19 a.m. November 15th, 2011
tVAhq
Thanks for the guide on the admin pop-up. I just followed it without any problems.
However, seems a little odd to me that recreating this functionality from the admin panel is a so involved though.
Matthew Orlisnki in Manchester, UK at 9:33 a.m. February 26th, 2012
iW3yn
Matthew, I agree it would be great if the various JavaScript features of Django were documented and made public with a supported API.
That would mean supporting it and not changing it in future versions, however, and I don’t think they’re ready to do that yet. For example, I’m not sure, but I think this functionality will be changing slightly in Django 1.4.
Jerry Stratton in San Diego at 4:11 p.m. February 26th, 2012
+g/Ql
This is great. I was wondering is it possible to pass additional variables to the SelectWithPop or MultipleSelectWithPop render functions from the model form. For example I have a number of fields in my form that are ModelChoiceFields that reference the same model, therefore I need to give the fields different names. I also need to pass some additional parameters to the popup url. What I was hoping for was something like
anExampleField = forms.ModelChoiceField(
....
widget=SelectWithPop(attrs={'name': 'modelName', 'anotherAttribute': attributeValue}),
.....
)
Any help or pointers would be greatly appreciated
Neillo at 6:44 a.m. May 15th, 2012
EfMrJ
Neillo, I’m not aware of any way of sending additional information to widgets (which doesn’t mean that there isn’t any).
However, make sure you need to. Django usually can handle that sort of thing for you. Untested, but if I had a form like this:
I would expect it to work, with one form field named “contact”, one form field named “employee”, and both pointing to the model Contact.
But I may be misunderstanding your question.
Jerry Stratton in San Diego at 7:40 p.m. May 16th, 2012
+g/Ql
Hey Jerry,
Thanks for this article, it's tremendously helpful.
I'm having a couple of issues:
1) the pop-up to add the new object does not close when I click "Submit", and
2) the drop-down does not update with the new selection that I added through the pop-up
Are these two things related? Am I missing something extremely simple here?
Thanks again,
M
Matthew Calabresi in United States at 10:18 a.m. May 24th, 2012
vAkLy
Matthew, that sounds like the pop-up window doesn’t have JavaScript running in it. Both of those functions are handled by the same JavaScript file. Try viewing the source of the pop-up window to make sure it is correctly including RelatedObjectLookups.js.
If you are using Django 1.4, the path to admin-related JavaScript and CSS has likely changed. So try loading RelatedObjectLookups.js in another window to make sure it is still accessible via the URL the pop-up window is trying to access it with.
Jerry in San Diego at 10:39 a.m. May 24th, 2012
3eqBR
I looked and it's included correctly. I checked the console when I clicked Submit, and I'm getting this error:
Uncaught ReferenceError: SelectBox is not defined
So it's barfing where it's supposed to update the Select box, and never getting to the window close. I'm going to compare this step-by-step to how the admin functionality works to see if I'm missing anything, but in the meantime, can you think of anything obvious that I might be missing?
(Django 1.3.1 btw)
Thanks,
MC
Matthew Calabresi in United States at 2:23 p.m. May 25th, 2012
vAkLy
Nevermind, I fixed it -- I had a typo in my popupplus.html template. Turns out I only had one opening curly brace for my {{ field }} variable reference in the link ID.
Thanks again for your help!
Hark Thrice at 9:10 p.m. May 25th, 2012
tR//V
Jerry,
Referring to your answer above, the url rendered to the plus button is based on the field name in the Form so
employee = forms.ModelChoiceField(Contact.objects, widget = SelectWithPop)
renders as
www.mysite/add/employee/
whereas one would more than likely prefer
www.mysite/add/contact/
I beleive its because of the name argument in the render function being passed to the field. Unfortunately I'm stumped at how to change this in the render method on forms.Select
Neillo at 9:32 a.m. June 11th, 2012
EfMrJ
Neillo, if you want both /add/ employee and /add/ contact to do the same thing, you should be able to point them to the same function in your urls.py:
Jerry Stratton in San Diego at 5:03 p.m. June 23rd, 2012
+g/Ql
Brilliant, concise, and accurate. Thank you for removing a few hundred SLOC from my project!
NeilMillikin in California, Baby! at 12:43 p.m. January 22nd, 2013
pbO3Y
I had the same problem as Matthew Calabresi,
Upon clicking submit in the popup, it wouldn't close and "SelectBox is not defined" is given as error in the js console.
Turns out my problem was that my select box id in the original page was "id_tags" and the id of my plus sign <a> element was "add_id_tag". It should have been "add_id_tags" (Notice the "s" at the end of tags).
The results was simply that the element could not be found and that led to the undefined SelectBox.
M
Matthys Kroon in Stellenbosch at 6:41 p.m. March 7th, 2013
lvHWO
Everything is working fine with SelectWithPop.
When I have a M2M relationship and I use the MultipleSelectWithPop the new info is saved but the multiple select is not refreshing the data, anyone has the same problem or I miss something
Pablo Carpio at 8:28 p.m. June 10th, 2013
+27pf
Firstly, thanks very much for the tutorial Jerry, excellent.
I'm using this technique in a Django 1.6 project - works well in a basic form.
I am however having an issue, my form contains a formset, and I want to apply this technique to a field in the formset also.
The formset prepends a prefix (e.g. prefix=foo) and form number to the form fields, this seems to propogate; the link to open the popup windows has an href of "/add/[prefix]-[form_no]-[field]" (e.g. "/add/foo-0-category" and not the expected "/add/category".
I'm guessing this is happening the in the overriding of the forms.Select?
Just wondering if you've come across this and/or know of a fix?
Jamie at 1:28 p.m. August 17th, 2014
z/BgT
Jamie, yes, that would make sense. The form link is created with:
So if the field name is foo-0-category then the link would be /add/foo-0-category, which won’t work in the examples above. One solution would be to pass in something other than the field name. Instead of:
Maybe:
And add an if/else where if the field contains dashes (or if there is some other way to recognize when a field is part of a field set) then split the field name and put the third part into rawfield; otherwise, pass the full field name into rawfield.
Alternatively, urls.py uses regular expressions. So instead of
you should be able to use something like
There are probably other ways, too.
Jerry Stratton in Round Rock, TX at 5:13 a.m. August 18th, 2014
ZGpVP
Hi Jerry, you are a champion, thanks for the reply! Your rawfield idea got me thinking, but not before I tried your suggestion about a url conf pattern match.
Hopefully this is easy to follow.
The url conf idea had some strange outcomes, where I could render the view but when submitted it tried posting to '/add/[field_name]' and not '/add/[prefix]-[form_id]-[field_name]'. Anyway, adding another URL conf seemed like a bed idea. I decided to table this and try another approach.
So I decided to investigate the rawfield suggestion. I modified my 'SelectWithPop' class to look like this;
The line `rawname = name.split('-')[-1];` simply splits name on '-' (dash) and returns the last segment, I figured this would work for the cases like [field], [form_id]-[field] and [prefix]-[form_id]-[field]. The later two being possibilities when working with formsets.
Anyway, I give the page an F5, hovered over the link an saw exactly what I was after '/add/[filed]' (no more prefix and form id). Clicking the link popped open the edit form, beautiful! But when I click save it just left me a blank form (new record was added tho).
I studied the admin form a little more and figured out that the element couldn't find a select field with the new rawname; it seems 'id' in the href for the + link was important so the new value could be populated back to the correct field.
So with some edits this is my new SelectWithPop class;
Where field is the new rawname and id is the original field id (unmodified). I then updated my popuppluse template;
I then hit F5 and was able to add new related records in pop-ups for fields in both my normal form and formset. Thanks heaps for your help mate!
Jamie at 10:52 a.m. August 18th, 2014
z/BgT
Great post! But how to investigate the "Uncaught ReferenceError: SelectBox is not defined" error in the popup screen? I have tried all kinds of stuff and also checked the code, but I keep getting that error and then the window does not close and also the underlying form is not updated until I hit F5. Thank you!
Hark Thrice at 4 p.m. September 10th, 2014
zkyLn
Have you looked at Matthys Kroon’s solution in the comments above? I haven’t personally seen this error, but some of the other commenters have; his is the only solution I’ve seen.
Jerry Stratton in Round Rock, TX at 8:56 p.m. September 10th, 2014
ZGpVP
Thank you. I have found it! I had to add:
html = super(SelectWithPop, self).render(name, value, attrs={'id':"id_%s" % name}, *args, **kwargs)
to the render of the SelectWithPop. This is because Django automatically adds "id_%S" % name to the id field when using a modelform!
Thank you so much!
Hark Thrice at 6:30 p.m. September 12th, 2014
zkyLn
Hi, great post! I have only one question, does anybody have csrf_token error?
I couldn't submit my form and I put the {% csrf_token %} line, thanks!
Fedde Murua at 12:45 a.m. January 6th, 2018
dwVqI
Someone needs to make a new guide for django 3....
Hark Thrice at 8:25 a.m. November 15th, 2020
gNJFG
Or Django 5
Null at 5:11 p.m. September 29th, 2024
gvq99