ajaxdjangotwitter-bootstrapmodal-dialogdjango-crispy-forms

AJAX feedback form using django crispy forms in Bootstrap Modal


There are quite a few moving parts to this question, but if you have any insight to any piece of it, it would be appreciated.

I want to build a feedback form that acts as one would expect. When the user clicks the feedback button at the bottom right of the page, it launches a bootstrap modal. The modal has a django crispy form that submits or returns the fields that are invalid when the submit button is pressed.

First, I have my feedback button:

{% load crispy_forms_tags %}

.feedback-button {
    position: fixed;
    bottom: 0;
    right: 30px;
}

<div class='feedback-button'>
    <a class="btn btn-info" href="#feedbackModal" data-toggle="modal" title="Leave feedback" target="_blank">
        <i class="icon-comment icon-white"></i>
        Leave feedback
    </a>
</div>
<div class="modal hide" id="feedbackModal" tabindex="-1" role="dialog" aria-labelledby="feedbackModalLabel" aria-hidden="true">
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
        <h3 id="feedbackModalLabel">Contact Form</h3>
    </div>
    <div class="modal-body">
        {% crispy feedback_form feedback_form.helper %}
    </div>
    <div class="modal-footer">
        <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
        <button class="btn btn-primary">Submit</button>
    </div>
</div>

Next, I have my form:

class Feedback(models.Model):
    creation_date = models.DateTimeField("Creation Date", default=datetime.now)
    topic = models.CharField("Topic", choices = TOPIC_CHOICES, max_length=50)
    subject = models.CharField("Subject", max_length=100)
    message = models.TextField("Message", blank=True)
    sender = models.CharField("Sender", max_length=50, blank=True, null=True)

    def __unicode__(self):
        return "%s - %s" % (self.subject, self.creation_date)

    class Meta:
            ordering = ["creation_date"]
        verbose_name = "Feedback"
        verbose_name_plural = "Feedback"

class Crispy_ContactForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Fieldset(
                Field('topic', placeholder='Topic', css_class='input-medium'),
                Field('subject', placeholder='Subject', css_class='input-xlarge'),
                Field('message', placeholder='Message', rows='5', css_class='input-xlarge'),
                Field('sender', placeholder='Sender', css_class='input-xlarge'),
            ),
        )
        self.helper.form_id = 'id-Crispy_ContactForm'
        self.helper.form_method = 'post'

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

    class Meta:
        model = Feedback
        exclude = ['creation_date']

I tried to omit the legend in the crispy form because if I include it, the modal appears to have two form titles. But omitting the legend in the crispy form layout resulted in the fields appearing out of order.

So I'm left with a few questions:

  1. Overall, am I going about this the right way?
  2. If I hook up the modal's submit button to AJAX, how do I go about error checking the form?
  3. Is there a better way to display the crispy form in the bootstrap modal?

Solution

  • I found a partial solution on this page. In my base template, I created the button and the form:

    <div class='feedback-button'><a class="btn btn-info" href="#feedbackModal" data-toggle="modal" title="Leave feedback" target="_blank"><i class="icon-comment icon-white"></i> Leave feedback</a></div>
    {% include "_feedback_form.html" with feedback_form=feedback_form %}
    

    Then I created two feedback forms

    <div class="modal hide" id="feedbackModal" tabindex="-1" role="dialog" aria-labelledby="feedbackModalLabel" aria-hidden="true">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
            <h3 id="feedbackModalLabel">Contact Form</h3>
        </div>
        {% include "_feedback_form_two.html" with feedback_form=feedback_form %}
    </div>
    

    and

    {% load crispy_forms_tags %}

    <form action="{% url feedback %}" method="post" id="id-Crispy_ContactForm" class="form ajax" data-replace="#id-Crispy_ContactForm">
        <div class="modal-body">
        {% crispy feedback_form %}  
        </div>
        <div class="modal-footer">
            <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
            <input type="submit" name="submit_feedback" value="Submit" class="btn btn-primary" id="submit-id-submit_feedback" />
        </div>
    </form>
    

    I broke the feedback forms into two because the bootstrap-ajax.js file that I'm leveraging from the above link replaces the html from the one template. If I use a combined feedback form, it will have class="modal hide". I need it to just have class="modal" so that if the form is refreshing with errors, the modal doesn't disappear.

    In my view, I have

    @login_required
    def feedback_ajax(request):
        feedback_form = Crispy_ContactForm(request.POST)
        dismiss_modal = False
        if feedback_form.is_valid():
            message = feedback_form.save()
            feedback_form = Crispy_ContactForm()
            dismiss_modal = True
        data = {
            "html": render_to_string("_feedback_form_two.html", {
                "feedback_form": feedback_form
            }, context_instance=RequestContext(request)),
            "dismiss_modal": dismiss_modal
        }
        return HttpResponse(json.dumps(data), mimetype="application/json")
    

    And then in the bootstrap-ajax.js file (again from the above link), I made a few alterations. In the processData function, I defined:

    var $el_parent = $el.parent();
    

    and I added

    if (data.dismiss_modal) {
        var msg = '<div class="alert alert-success" id="' + $(replace_selector).attr('id') + '">Feedback Submitted</div>'
        $(replace_selector).replaceWith(msg);
        $el_parent.modal('hide');
        $(replace_selector).replaceWith(data.html);
    }
    

    This isn't fully functional yet because the Success Message disappears with the modal immediately. I want the modal to display the message and disappear after maybe 3 seconds. haven't figured this out yet, but it works well enough for now.

    I'm still tinkering, but this addresses most of my questions:

    It submits data with AJAX and returns with error checking if needed. The form displays fairly well in the modal.

    I have a few remaining issues. I need to figure out a way to suppress the legend in the crispy form, and I need to find a way to display the modal crispy form and not interfere with another crispy form that appears elsewhere on the site.