I've built an answer around the blog post I linked to in my comment.
I started by finding a logical way to include the 'add new' link in my form the solution I settled on was to provide a template for the form widget I wanted to have the feature. My form looks like this:
# core/forms.py
class IntranetForm(ModelForm):
def __init__(self, *args, **kwargs):
super(IntranetForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_class = 'form-horizontal'
# app/forms.py
class ComplaintForm(IntranetForm):
def __init__(self, *args, **kwargs):
super(ComplaintForm, self).__init__(*args, **kwargs)
self.helper.layout = Layout(
Field(
'case',
css_class='input-xlarge',
template='complaints/related_case.html',
),
Field('date_received', css_class = 'input-xlarge'),
Field('stage', css_class = 'input-xlarge'),
Field('tags', css_class = 'input-xlarge'),
Field('team', css_class = 'input-xlarge'),
Field('handler', css_class = 'input-xlarge'),
Field('status', css_class = 'input-xlarge'),
FormActions(
Submit(
'save_changes',
'Save changes',
css_class = "btn-primary"
),
Button(
'cancel',
'Cancel',
onclick = 'history.go(-1);'
),
),
)
class Meta:
model = Complaint
fields = (
'case',
'date_received',
'stage',
'tags',
'team',
'handler',
'status',
)
Note the addition of a template parameter in the first field. That template looks like this:
<div id="div_id_case" class="control-group">
<label for="id_case" class="control-label ">Case</label>
<div class="controls">
<select class="input-xlarge select" id="id_case" name="case"></select>
<a href="{% url 'add_case' %}" id="add_id_case" class="add-another btn btn-success" onclick="return showAddAnotherPopup(this);">Add new</a>
</div>
</div>
When django renders my case_form.html
template, the html above is inserted as the relevant form field. The full complaint_form.html
template contains the javascript code that calls the case form. That template looks like this:
{% extends 'complaints/base_complaint.html' %}
{% load crispy_forms_tags %}
{% block extra_headers %}
{{ form.media }}
{% endblock %}
{% block title %}Register complaint{% endblock %}
{% block heading %}Register complaint{% endblock %}
{% block content %}
{% crispy form %}
<script>
$(document).ready(function() {
$( '.add-another' ).click(function(e) {
e.preventDefault( );
showAddAnotherPopup( $( this ) );
});
});
/* Credit: django.contrib.admin (BSD) */
function showAddAnotherPopup(triggeringLink) {
/*
Pause here with Firebug's script debugger.
*/
var name = triggeringLink.attr( 'id' ).replace(/^add_/, '');
name = id_to_windowname(name);
href = triggeringLink.attr( 'href' );
if (href.indexOf('?') == -1) {
href += '?popup=1';
} else {
href += '&popup=1';
}
href += '&winName=' + name;
var win = window.open(href, name, 'height=800,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
function dismissAddAnotherPopup(win, newId, newRepr) {
// newId and newRepr are expected to have previously been escaped by
newId = html_unescape(newId);
newRepr = html_unescape(newRepr);
var name = windowname_to_id(win.name);
var elem = document.getElementById(name);
if (elem) {
if (elem.nodeName == 'SELECT') {
var o = new Option(newRepr, newId);
elem.options[elem.options.length] = o;
o.selected = true;
}
} else {
console.log("Could not get input id for win " + name);
}
win.close();
}
function html_unescape(text) {
// Unescape a string that was escaped using django.utils.html.escape.
text = text.replace(/</g, '');
text = text.replace(/"/g, '"');
text = text.replace(/'/g, "'");
text = text.replace(/&/g, '&');
return text;
}
// IE doesn't accept periods or dashes in the window name, but the element IDs
// we use to generate popup window names may contain them, therefore we map them
// to allowed characters in a reversible way so that we can locate the correct
// element when the popup window is dismissed.
function id_to_windowname(text) {
text = text.replace(/\./g, '__dot__');
text = text.replace(/\-/g, '__dash__');
text = text.replace(/\[/g, '__braceleft__');
text = text.replace(/\]/g, '__braceright__');
return text;
}
function windowname_to_id(text) {
return text;
}
</script>
{% endblock %}
The javascript there is completely copy/pasted from the blog entry I'm linking to, it calls my CaseCreateView
in a popup, which includes the form we've already looked at. Now you need that form to pass a return value to your original page.
I did this by including the following script in the case detail page. This means my form will save properly before calling the script when it redirects to the detail page on save. The detail template looks like:
{% extends 'complaints/base_complaint.html' %}
{% block content %}
<script>
$(document).ready(function() {
opener.dismissAddAnotherPopup( window, "{{ case.pk }}", "{{ case }}" );
});
</script>
{% endblock %}
This closes the page as soon as it's rendered. That may not be ideal for you but you can change that behaviour by, for example, overriding the page your form redirects to on save and using that as a convenient store for the script above but nothing else. Notice how it's passing back the case primary key and it's unicode representation? Those will now populate the selected <option>
field in your original form and you're done.