Question

By default CreateView/UpdateView just includes a dropdown for selecting an already existing ForeignKey-related object.

Using django-crispy-forms, how do I have a CreateView or UpdateView that not only includes fields for my model, but fields for creating a new model related via ForeignKey?

Would I be better off using CreateView/UpdateView and using a regular FBV? If so, how would I go about that?

I didn't have much problem getting up to speed on learning much of Django, but wrapping my mind around how views/forms/models interact is not coming easily.

class Property(models.Model):
    name = models.CharField(max_length=128)
    address = models.ForeignKey(PostalAddress, blank=True, null=True)

class PostalAddress(models.Model):
    street_address = models.CharField(max_length=500)
    city = models.CharField(max_length=500)
    state = USStateField()
    zip_code = models.CharField(max_length=10)

class PropertyUpdateView(UpdateView):
    model = Property

class PropertyCreateView(CreateView):
    model = Property

I've been experimenting with adding form_class = PropertyForm to the CreateView/UpdateView, and using something like:

class PropertyForm(ModelForm):

    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.form_id = 'id-propertyForm'
        self.helper.form_method = 'post'

        self.helper.layout = Layout(
            Fieldset(
                'Edit Property',
                'name',
            ),
            ButtonHolder(
                Submit('submit', 'Submit')
            )
        )

        super(PropertyForm, self).__init__(*args, **kwargs)

    class Meta:
        model = Property

...but I don't know where to go from here.

Was it helpful?

Solution

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>
&nbsp;<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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top