Implements AJAX form posting.

This is somewhat of a hack for Essex, since the long-term solution
is a reworking of the way AJAX is handled. But it solves problems
in the interim and provides a significantly better experience.

Thanks to Andy Chong for pushing forward with the initial attempts
which lead to this patch.

Fixes bug 943518.

Change-Id: Ia65d926d3d406b07301e23b4c87de60c66ddec75
This commit is contained in:
Gabriel Hurley 2012-03-21 16:14:59 -07:00
parent 0a640c300e
commit c11cd9d1c5
4 changed files with 86 additions and 51 deletions

View File

@ -16,6 +16,7 @@
import os
from django import http
from django.views import generic
@ -62,6 +63,14 @@ class ModalFormView(generic.TemplateView):
self.object = self.get_object(*args, **kwargs)
form, handled = self.maybe_handle()
if handled:
if self.request.is_ajax():
# TODO(gabriel): This is not a long-term solution to how
# AJAX should be handled, but it's an expedient solution
# until the blueprint for AJAX handling is architected
# and implemented.
response = http.HttpResponse()
response['X-Horizon-Location'] = handled['location']
return response
return handled
context = self.get_context_data(**kwargs)
context[self.context_form_name] = form

View File

@ -68,7 +68,7 @@ class HorizonMiddleware(object):
messages.error(request, unicode(exception))
if request.is_ajax():
response_401 = http.HttpResponse(status=401)
response_401["REDIRECT_URL"] = redirect_to
response_401['X-Horizon-Location'] = redirect_to
return response_401
return shortcuts.redirect(redirect_to)

View File

@ -1,9 +1,81 @@
horizon.modals.success = function (data, textStatus, jqXHR) {
$('body').append(data);
$('.modal span.help-block').hide();
$('.modal:last').modal();
horizon.datatables.validate_button();
// TODO(tres): Find some better way to deal with grouped form fields.
var volumeField = $("#id_volume");
if(volumeField) {
var volumeContainer = volumeField.parent().parent();
var deviceContainer = $("#id_device_name").parent().parent();
var deleteOnTermContainer = $("#id_delete_on_terminate").parent().parent();
function toggle_fields(show) {
if(show) {
volumeContainer.removeClass("hide");
deviceContainer.removeClass("hide");
deleteOnTermContainer.removeClass("hide");
} else {
volumeContainer.addClass("hide");
deviceContainer.addClass("hide");
deleteOnTermContainer.addClass("hide");
}
}
if(volumeField.find("option").length == 1) {
toggle_fields(false);
} else {
var disclosureElement = $("<div />").addClass("volume_boot_disclosure").text("Boot From Volume");
volumeContainer.before(disclosureElement);
disclosureElement.click(function() {
if(volumeContainer.hasClass("hide")) {
disclosureElement.addClass("on");
toggle_fields(true);
} else {
disclosureElement.removeClass("on");
toggle_fields(false);
}
});
toggle_fields(false);
}
}
};
horizon.addInitFunction(function() {
$(document).on('click', '.modal:not(.static_page) .cancel', function (evt) {
$(this).closest('.modal').modal('hide');
return false;
});
$(document).on('submit', '.modal:not(.static_page) form', function (evt) {
var $form = $(this);
evt.preventDefault();
$.ajax({
type: "POST",
url: $form.attr('action'),
data: $form.serialize(),
success: function (data, textStatus, jqXHR) {
// TODO(gabriel): This isn't a long-term solution for AJAX redirects.
// https://blueprints.launchpad.net/horizon/+spec/global-ajax-communication
var header = jqXHR.getResponseHeader("X-Horizon-Location");
if (header) {
location.href = header;
}
$form.closest(".modal").modal("hide");
horizon.modals.success(data, textStatus, jqXHR);
},
error: function(jqXHR, status, errorThrown) {
$form.closest(".modal").modal("hide");
horizon.alert("error", "There was an error submitting the form. Please try again.");
}
});
});
// Handle all modal hidden event to remove them as default
$(document).on('hidden', '.modal', function () {
$(this).remove();
@ -12,9 +84,9 @@ horizon.addInitFunction(function() {
$('.ajax-modal').click(function (evt) {
var $this = $(this);
$.ajax($this.attr('href'), {
error: function(jqXHR, status, errorThrown){
error: function(jqXHR, status, errorThrown) {
if (jqXHR.status === 401){
var redir_url = jqXHR.getResponseHeader("REDIRECT_URL");
var redir_url = jqXHR.getResponseHeader("X-Horizon-Location");
if (redir_url){
location.href = redir_url;
} else {
@ -22,53 +94,7 @@ horizon.addInitFunction(function() {
}
}
},
success: function (data, status, jqXHR) {
$('body').append(data);
$('.modal span.help-block').hide();
$('.modal:last').modal();
horizon.datatables.validate_button();
// TODO(tres): Find some better way to deal with grouped form fields.
var volumeField = $("#id_volume");
if(volumeField) {
var volumeContainer = volumeField.parent().parent();
var deviceContainer = $("#id_device_name").parent().parent();
var deleteOnTermContainer = $("#id_delete_on_terminate").parent().parent();
function toggle_fields(show) {
if(show) {
volumeContainer.removeClass("hide");
deviceContainer.removeClass("hide");
deleteOnTermContainer.removeClass("hide");
} else {
volumeContainer.addClass("hide");
deviceContainer.addClass("hide");
deleteOnTermContainer.addClass("hide");
}
}
if(volumeField.find("option").length == 1) {
toggle_fields(false);
} else {
var disclosureElement = $("<div />").addClass("volume_boot_disclosure").text("Boot From Volume");
volumeContainer.before(disclosureElement);
disclosureElement.click(function() {
if(volumeContainer.hasClass("hide")) {
disclosureElement.addClass("on");
toggle_fields(true);
} else {
disclosureElement.removeClass("on");
toggle_fields(false);
}
});
toggle_fields(false);
}
}
}
success: horizon.modals.success
});
evt.preventDefault();
});

View File

@ -213,7 +213,7 @@ class HorizonTests(BaseHorizonTests):
resp = client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Response should be HTTP 401 with redirect header
self.assertEquals(resp.status_code, 401)
self.assertEquals(resp["REDIRECT_URL"],
self.assertEquals(resp["X-Horizon-Location"],
"?".join([urlresolvers.reverse("horizon:auth_login"),
"next=%s" % url]))