I posted back in July on how to use the RelatedFieldWidgetWrapper, but it has only been in the last few weeks that I have actually used it in a project outside of the admin.
When setting up the RelatedFieldWidgetWrapper (RFWW) around a FilteredMultipeSelect widget on my own form, the code changes slightly from what is used on a admin page – basically the pointer to the admin site is no longer needed, nor is the relationship information. Here is the code used for a custom page.
First, I subclassed the RFWW to work outside of the admin, basically stripping out much of the init() code that wasn’t needed. (I’ll leave an examination of how this differs from the original to the reader).
widgets.py
class CustomRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
"""
Based on RelatedFieldWidgetWrapper, this does the same thing
outside of the admin interface
the parameters for a relation and the admin site are replaced
by a url for the add operation
"""
def __init__(self, widget, add_url,permission=True):
self.is_hidden = widget.is_hidden
self.needs_multipart_form = widget.needs_multipart_form
self.attrs = widget.attrs
self.choices = widget.choices
self.widget = widget
self.add_url = add_url
self.permission = permission
def render(self, name, value, *args, **kwargs):
self.widget.choices = self.choices
output = [self.widget.render(name, value, *args, **kwargs)]
if self.permission:
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
(self.add_url, name))
output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another')))
return mark_safe(u''.join(output))
Next, I used this new widget wrapper in my form definition:
forms.py
class EndowmentForm(ModelForm):
support_accounts = ModelMultipleChoiceField(queryset=None,
label=('Select Support Accounts'),
required=False)
def __init__(self, *args, **kwargs):
super(EndowmentForm,self).__init__(*args, **kwargs)
# set the widget with wrapper
self.fields['support_accounts'].widget = CustomRelatedFieldWidgetWrapper(
FilteredSelectMultiple(('Support Accounts'),False,),
reverse('support_create'),
True)
self.fields['support_accounts'].queryset = SupportAccount.objects.all()
class Media:
## media for the FilteredSelectMultiple widget
css = {
'all':(ADMIN_MEDIA_PREFIX + 'css/widgets.css',),
}
# jsi18n is required by the widget
js = ( ADMIN_MEDIA_PREFIX + 'js/admin/RelatedObjectLookups.js',)
class Meta:
model = Endowment
And include the javascript files in the template:
endow_edit.html
(somewhere in the <head> section)
{{ form.media }}
<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
Also be sure to include javascript.
The parent side is ready. Now let’s tackle the child side.
To add a new related record, the widget will open the page in a popup window. (Try this in the admin to see how it should work.) When the save button is pressed, the record is added, the popup closes, and the new value is added to the chosen side of the FilteredSelectMultiple control. Pretty cool!
But … how can I make my form do that?
After digging around a little in the Django admin code, I have figured it out.
Luckily, all of the heavy lifting is performed by the javascript provided with Django. I just needed to add a little code to make it all work.
First, I added a key in the context to let me know if the form is a popup. Notice that the RelatedFieldWidgetWrapper appends ‘?=_popup=1’ to the URL. My code looks for that key in the GET data and adds the context variable if found:
views.py
SupportAccountCreateView(CreateView)
def get_context_data(self, **kwargs):
context = super(SupportAccountCreateView,self).get_context_data(**kwargs)
if ('_popup' in self.request.GET):
context['popup'] = self.request.GET['_popup']
return context
Next, in the template, the presence of this context key triggers the addition of a hidden field:
support_edit.html
(somewhere in the <form>)
{% if popup %}<input type="hidden" name="_popup" value="1">{% endif %}
Now that I know I’m dealing with a popup form, my post() code can look for it and fire off the javascript:
views.py
SupportAccountCreateView(CreateView)
def post(self, request, *args, **kwargs):
## Save the normal response
response = super(SupportAccountCreateView,self).post(request, *args, **kwargs)
## This will fire the script to close the popup and update the list
if "_popup" in request.POST:
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
(escape(self.object.pk), escapejs(self.object)))
## No popup, so return the normal response
return response
(This little snippet was pretty much copied from the admin code.)
Finally, I need to include the javascript in the form:
forms.py
SupportAccountEditForm(ModelForm)
class Media:
## media for the Related Field Wrapper
# jsi18n is required by the widget
js = ( ADMIN_MEDIA_PREFIX + 'js/admin/RelatedObjectLookups.js',)
support_edit.html
(somewhere in the <head> section)
{{ form.media }}
<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
Be sure to also include jquery in the template.
That’s it! Give it a try sometime.