More RelatedFieldWidgetWrapper – My Very Own Popup

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.

Setup Django Environment on a New Mac

MacBook Pro

Setting up a Mac for Django development was easy – even a little easier than I thought. My machine is a new MacBook Pro running 10.8.2.

Xcode

Xcode provides the utilities needed for Python libraries and Subversion. It can be installed from the App Store. Afterwards, open the application, open Preferences and select downloads, then install the command line tools.

Python Utilities

In a terminal window:

  1. sudo easy_install pip
  2. sudo easy_install virtualenv

Eclipse

Setting up Eclipse worked pretty much the same as it does on Linux.

  1. Download desired Eclipse package. I used the Java EE package, Indigo (3.7) version.
  2. Unzip the files
  3. Copy Eclipse folder to Applications
  4. Drag the program file to the dock

As I cover in my earlier post, My Eclipse Setup for Django, I installed Aptana Studio 3 to add Python and Django support, and Subclipse to access my Subversion repositories.

That’s It!

Not tough at all. Happy developing!