mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
Fixed #29087 -- Added delete buttons for unsaved admin inlines on validation error.
This commit is contained in:
parent
6ea3aadd17
commit
24e540fbd7
1
AUTHORS
1
AUTHORS
@ -293,6 +293,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
flavio.curella@gmail.com
|
flavio.curella@gmail.com
|
||||||
Florian Apolloner <florian@apolloner.eu>
|
Florian Apolloner <florian@apolloner.eu>
|
||||||
Florian Moussous <florian.moussous@gmail.com>
|
Florian Moussous <florian.moussous@gmail.com>
|
||||||
|
Fran Hrženjak <fran.hrzenjak@gmail.com>
|
||||||
Francisco Albarran Cristobal <pahko.xd@gmail.com>
|
Francisco Albarran Cristobal <pahko.xd@gmail.com>
|
||||||
Francisco Couzo <franciscouzo@gmail.com>
|
Francisco Couzo <franciscouzo@gmail.com>
|
||||||
François Freitag <mail@franek.fr>
|
François Freitag <mail@franek.fr>
|
||||||
|
@ -284,11 +284,14 @@ tr.alt {
|
|||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row1 {
|
.row1, .row-form-errors {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row2 {
|
.row2,
|
||||||
|
.row2 .errorlist,
|
||||||
|
.row1 + .row-form-errors,
|
||||||
|
.row1 + .row-form-errors .errorlist {
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
|
var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
|
||||||
var nextIndex = parseInt(totalForms.val(), 10);
|
var nextIndex = parseInt(totalForms.val(), 10);
|
||||||
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
|
var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
|
||||||
|
var minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off");
|
||||||
var addButton;
|
var addButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +80,9 @@
|
|||||||
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
|
if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
|
||||||
addButton.parent().hide();
|
addButton.parent().hide();
|
||||||
}
|
}
|
||||||
|
// Show the remove buttons if there are more than min_num.
|
||||||
|
toggleDeleteButtonVisibility(row.closest('.inline-group'));
|
||||||
|
|
||||||
// Pass the new form to the post-add callback, if provided.
|
// Pass the new form to the post-add callback, if provided.
|
||||||
if (options.added) {
|
if (options.added) {
|
||||||
options.added(row);
|
options.added(row);
|
||||||
@ -112,7 +116,13 @@
|
|||||||
e1.preventDefault();
|
e1.preventDefault();
|
||||||
var deleteButton = $(e1.target);
|
var deleteButton = $(e1.target);
|
||||||
var row = deleteButton.closest('.' + options.formCssClass);
|
var row = deleteButton.closest('.' + options.formCssClass);
|
||||||
// Remove the parent form containing this button:
|
var inlineGroup = row.closest('.inline-group');
|
||||||
|
// Remove the parent form containing this button,
|
||||||
|
// and also remove the relevant row with non-field errors:
|
||||||
|
var prevRow = row.prev();
|
||||||
|
if (prevRow.length && prevRow.hasClass('row-form-errors')) {
|
||||||
|
prevRow.remove();
|
||||||
|
}
|
||||||
row.remove();
|
row.remove();
|
||||||
nextIndex -= 1;
|
nextIndex -= 1;
|
||||||
// Pass the deleted form to the post-delete callback, if provided.
|
// Pass the deleted form to the post-delete callback, if provided.
|
||||||
@ -127,6 +137,8 @@
|
|||||||
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
|
if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
|
||||||
addButton.parent().show();
|
addButton.parent().show();
|
||||||
}
|
}
|
||||||
|
// Hide the remove buttons if at min_num.
|
||||||
|
toggleDeleteButtonVisibility(inlineGroup);
|
||||||
// Also, update names and ids for all remaining form controls so
|
// Also, update names and ids for all remaining form controls so
|
||||||
// they remain in sequence:
|
// they remain in sequence:
|
||||||
var i, formCount;
|
var i, formCount;
|
||||||
@ -139,18 +151,37 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show the add button if we are allowed to add more items.
|
var toggleDeleteButtonVisibility = function(inlineGroup) {
|
||||||
// Note that max_num = None translates to a blank string.
|
if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) {
|
||||||
var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
|
inlineGroup.find('.inline-deletelink').hide();
|
||||||
|
} else {
|
||||||
|
inlineGroup.find('.inline-deletelink').show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$this.each(function(i) {
|
$this.each(function(i) {
|
||||||
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
|
$(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the add button.
|
// Create the delete buttons for all unsaved inlines:
|
||||||
|
$this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() {
|
||||||
|
addInlineDeleteButton($(this));
|
||||||
|
});
|
||||||
|
toggleDeleteButtonVisibility($this);
|
||||||
|
|
||||||
|
// Create the add button, initially hidden.
|
||||||
addButton = options.addButton;
|
addButton = options.addButton;
|
||||||
|
addInlineAddButton();
|
||||||
|
|
||||||
|
// Show the add button if allowed to add more items.
|
||||||
|
// Note that max_num = None translates to a blank string.
|
||||||
|
var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
|
||||||
if ($this.length && showAddButton) {
|
if ($this.length && showAddButton) {
|
||||||
addInlineAddButton();
|
addButton.parent().show();
|
||||||
|
} else {
|
||||||
|
addButton.parent().hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -314,7 +345,7 @@
|
|||||||
$(selector).stackedFormset(selector, inlineOptions.options);
|
$(selector).stackedFormset(selector, inlineOptions.options);
|
||||||
break;
|
break;
|
||||||
case "tabular":
|
case "tabular":
|
||||||
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr";
|
selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row";
|
||||||
$(selector).tabularFormset(selector, inlineOptions.options);
|
$(selector).tabularFormset(selector, inlineOptions.options);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
(function(b){b.fn.formset=function(c){var a=b.extend({},b.fn.formset.defaults,c),d=b(this),h=d.parent(),l=function(a,e,k){var f=new RegExp("("+e+"-(\\d+|__prefix__))");e=e+"-"+k;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(f,e));a.id&&(a.id=a.id.replace(f,e));a.name&&(a.name=a.name.replace(f,e))},g=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),e=parseInt(g.val(),10),k=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off");c=function(){if(null===m)if("TR"===d.prop("tagName")){var b=
|
(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),k=c.parent(),n=function(a,e,l){var g=new RegExp("("+e+"-(\\d+|__prefix__))");e=e+"-"+l;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(g,e));a.id&&(a.id=a.id.replace(g,e));a.name&&(a.name=a.name.replace(g,e))},h=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),e=parseInt(h.val(),10),l=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),q=b("#id_"+a.prefix+"-MIN_NUM_FORMS").prop("autocomplete",
|
||||||
d.eq(-1).children().length;h.append('<tr class="'+a.addCssClass+'"><td colspan="'+b+'"><a href="#">'+a.addText+"</a></tr>");m=h.find("tr:last a")}else d.filter(":last").after('<div class="'+a.addCssClass+'"><a href="#">'+a.addText+"</a></div>"),m=d.filter(":last").next().find("a");m.on("click",n)};var n=function(f){f.preventDefault();f=b("#"+a.prefix+"-empty");var c=f.clone(!0);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+e);p(c);c.find("*").each(function(){l(this,
|
"off"),t=function(g){g.preventDefault();g=b("#"+a.prefix+"-empty");var f=g.clone(!0);f.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+e);r(f);f.find("*").each(function(){n(this,a.prefix,h.val())});f.insertBefore(b(g));b(h).val(parseInt(h.val(),10)+1);e+=1;""!==l.val()&&0>=l.val()-h.val()&&m.parent().hide();p(f.closest(".inline-group"));a.added&&a.added(f);b(document).trigger("formset:added",[f,a.prefix])},r=function(b){b.is("tr")?b.children(":last").append('<div><a class="'+
|
||||||
a.prefix,g.val())});c.insertBefore(b(f));b(g).val(parseInt(g.val(),10)+1);e+=1;""!==k.val()&&0>=k.val()-g.val()&&m.parent().hide();a.added&&a.added(c);b(document).trigger("formset:added",[c,a.prefix])},p=function(b){b.is("tr")?b.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></div>"):b.is("ul")||b.is("ol")?b.append('<li><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></li>"):b.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="#">'+
|
a.deleteCssClass+'" href="#">'+a.deleteText+"</a></div>"):b.is("ul")||b.is("ol")?b.append('<li><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></li>"):b.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></span>");b.find("a."+a.deleteCssClass).on("click",u.bind(this))},u=function(g){g.preventDefault();var f=b(g.target).closest("."+a.formCssClass);g=f.closest(".inline-group");var d=f.prev();d.length&&d.hasClass("row-form-errors")&&d.remove();
|
||||||
a.deleteText+"</a></span>");b.find("a."+a.deleteCssClass).on("click",q.bind(this))},q=function(c){c.preventDefault();c=b(c.target).closest("."+a.formCssClass);c.remove();--e;a.removed&&a.removed(c);b(document).trigger("formset:removed",[c,a.prefix]);c=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(c.length);(""===k.val()||0<k.val()-c.length)&&m.parent().show();var d,g=function(){l(this,a.prefix,f)};var f=0;for(d=c.length;f<d;f++)l(b(c).get(f),a.prefix,f),b(c.get(f)).find("*").each(g)},
|
f.remove();--e;a.removed&&a.removed(f);b(document).trigger("formset:removed",[f,a.prefix]);f=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(f.length);(""===l.val()||0<l.val()-f.length)&&m.parent().show();p(g);d=function(){n(this,a.prefix,c)};var c=0;for(g=f.length;c<g;c++)n(b(f).get(c),a.prefix,c),b(f.get(c)).find("*").each(d)},p=function(a){""!==q.val()&&0<=q.val()-h.val()?a.find(".inline-deletelink").hide():a.find(".inline-deletelink").show()};c.each(function(e){b(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});
|
||||||
r=""===k.val()||0<k.val()-g.val();d.each(function(e){b(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});var m=a.addButton;d.length&&r&&c();return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null,addButton:null};b.fn.tabularFormset=function(c,a){var d=b(this),h=function(a){b(c).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},
|
c.filter("."+a.formCssClass+":not(.has_original):not(."+a.emptyCssClass+")").each(function(){r(b(this))});p(c);var m=a.addButton;(function(){if(null===m)if("TR"===c.prop("tagName")){var b=c.eq(-1).children().length;k.append('<tr class="'+a.addCssClass+'"><td colspan="'+b+'"><a href="#">'+a.addText+"</a></tr>");m=k.find("tr:last a")}else c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="#">'+a.addText+"</a></div>"),m=c.filter(":last").next().find("a");m.on("click",t)})();d=""===l.val()||
|
||||||
l=function(){"undefined"!==typeof SelectFilter&&(b(".selectfilter").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!0)}))},g=function(a){a.find(".prepopulated_field").each(function(){var c=b(this).find("input, select, textarea"),e=c.data("dependency_list")||[],d=[];b.each(e,function(b,c){d.push("#"+a.find(".field-"+c).find("input, select, textarea").attr("id"))});
|
0<l.val()-h.val();c.length&&d?m.parent().show():m.parent().hide();return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null,addButton:null};b.fn.tabularFormset=function(d,a){var c=b(this),k=function(a){b(d).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},n=function(){"undefined"!==
|
||||||
d.length&&c.prepopulate(d,c.attr("maxlength"))})};d.formset({prefix:a.prefix,addText:a.addText,formCssClass:"dynamic-"+a.prefix,deleteCssClass:"inline-deletelink",deleteText:a.deleteText,emptyCssClass:"empty-form",removed:h,added:function(a){g(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());l();h(a)},addButton:a.addButton});return d};b.fn.stackedFormset=function(c,a){var d=b(this),h=function(a){b(c).find(".inline_label").each(function(a){a+=1;
|
typeof SelectFilter&&(b(".selectfilter").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!0)}))},h=function(a){a.find(".prepopulated_field").each(function(){var e=b(this).find("input, select, textarea"),d=e.data("dependency_list")||[],c=[];b.each(d,function(b,e){c.push("#"+a.find(".field-"+e).find("input, select, textarea").attr("id"))});c.length&&e.prepopulate(c,
|
||||||
b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})},l=function(){"undefined"!==typeof SelectFilter&&(b(".selectfilter").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!0)}))},g=function(a){a.find(".prepopulated_field").each(function(){var c=b(this).find("input, select, textarea"),d=c.data("dependency_list")||[],e=[];b.each(d,function(b,c){e.push("#"+a.find(".form-row .field-"+
|
e.attr("maxlength"))})};c.formset({prefix:a.prefix,addText:a.addText,formCssClass:"dynamic-"+a.prefix,deleteCssClass:"inline-deletelink",deleteText:a.deleteText,emptyCssClass:"empty-form",removed:k,added:function(a){h(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());n();k(a)},addButton:a.addButton});return c};b.fn.stackedFormset=function(d,a){var c=b(this),k=function(a){b(d).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,
|
||||||
c).find("input, select, textarea").attr("id"))});e.length&&c.prepopulate(e,c.attr("maxlength"))})};d.formset({prefix:a.prefix,addText:a.addText,formCssClass:"dynamic-"+a.prefix,deleteCssClass:"inline-deletelink",deleteText:a.deleteText,emptyCssClass:"empty-form",removed:h,added:function(a){g(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());l();h(a)},addButton:a.addButton});return d};b(document).ready(function(){b(".js-inline-admin-formset").each(function(){var c=
|
"#"+a))})},n=function(){"undefined"!==typeof SelectFilter&&(b(".selectfilter").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){a=b.name.split("-");SelectFilter.init(b.id,a[a.length-1],!0)}))},h=function(a){a.find(".prepopulated_field").each(function(){var c=b(this).find("input, select, textarea"),d=c.data("dependency_list")||[],e=[];b.each(d,function(b,c){e.push("#"+a.find(".form-row .field-"+c).find("input, select, textarea").attr("id"))});
|
||||||
b(this).data(),a=c.inlineFormset;switch(c.inlineType){case "stacked":c=a.name+"-group .inline-related";b(c).stackedFormset(c,a.options);break;case "tabular":c=a.name+"-group .tabular.inline-related tbody:first > tr",b(c).tabularFormset(c,a.options)}})})})(django.jQuery);
|
e.length&&c.prepopulate(e,c.attr("maxlength"))})};c.formset({prefix:a.prefix,addText:a.addText,formCssClass:"dynamic-"+a.prefix,deleteCssClass:"inline-deletelink",deleteText:a.deleteText,emptyCssClass:"empty-form",removed:k,added:function(a){h(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());n();k(a)},addButton:a.addButton});return c};b(document).ready(function(){b(".js-inline-admin-formset").each(function(){var d=b(this).data(),a=d.inlineFormset;
|
||||||
|
switch(d.inlineType){case "stacked":d=a.name+"-group .inline-related";b(d).stackedFormset(d,a.options);break;case "tabular":d=a.name+"-group .tabular.inline-related tbody:first > tr.form-row",b(d).tabularFormset(d,a.options)}})})})(django.jQuery);
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for inline_admin_form in inline_admin_formset %}
|
{% for inline_admin_form in inline_admin_formset %}
|
||||||
{% if inline_admin_form.form.non_field_errors %}
|
{% if inline_admin_form.form.non_field_errors %}
|
||||||
<tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
|
<tr class="row-form-errors"><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}"
|
<tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}"
|
||||||
id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
|
id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
|
||||||
|
@ -11,7 +11,7 @@ QUnit.module('admin.inlines: tabular formsets', {
|
|||||||
$('#qunit-fixture').append($('#tabular-formset').text());
|
$('#qunit-fixture').append($('#tabular-formset').text());
|
||||||
this.table = $('table.inline');
|
this.table = $('table.inline');
|
||||||
this.inlineRow = this.table.find('tr');
|
this.inlineRow = this.table.find('tr');
|
||||||
that.inlineRow.tabularFormset('table.inline tr', {
|
this.inlineRow.tabularFormset('table.inline tr.form-row', {
|
||||||
prefix: 'first',
|
prefix: 'first',
|
||||||
addText: that.addText,
|
addText: that.addText,
|
||||||
deleteText: 'Remove'
|
deleteText: 'Remove'
|
||||||
@ -31,6 +31,13 @@ QUnit.test('add form', function(assert) {
|
|||||||
assert.ok(this.table.find('#first-1').hasClass('row2'));
|
assert.ok(this.table.find('#first-1').hasClass('row2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('added form has remove button', function(assert) {
|
||||||
|
var addButton = this.table.find('.add-row a');
|
||||||
|
assert.equal(addButton.text(), this.addText);
|
||||||
|
addButton.click();
|
||||||
|
assert.equal(this.table.find('#first-1.row2 .inline-deletelink').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('add/remove form events', function(assert) {
|
QUnit.test('add/remove form events', function(assert) {
|
||||||
assert.expect(6);
|
assert.expect(6);
|
||||||
var $ = django.jQuery;
|
var $ = django.jQuery;
|
||||||
@ -49,7 +56,7 @@ QUnit.test('add/remove form events', function(assert) {
|
|||||||
assert.equal(true, $row.is(deletedRow));
|
assert.equal(true, $row.is(deletedRow));
|
||||||
assert.equal(formsetName, 'first');
|
assert.equal(formsetName, 'first');
|
||||||
});
|
});
|
||||||
deleteLink.click();
|
deleteLink.trigger($.Event('click', {target: deleteLink}));
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('existing add button', function(assert) {
|
QUnit.test('existing add button', function(assert) {
|
||||||
@ -69,3 +76,120 @@ QUnit.test('existing add button', function(assert) {
|
|||||||
addButton.click();
|
addButton.click();
|
||||||
assert.ok(this.table.find('#first-1').hasClass('row2'));
|
assert.ok(this.table.find('#first-1').hasClass('row2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
QUnit.module('admin.inlines: tabular formsets with validation errors', {
|
||||||
|
beforeEach: function() {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
|
||||||
|
$('#qunit-fixture').append($('#tabular-formset-with-validation-error').text());
|
||||||
|
this.table = $('table.inline');
|
||||||
|
this.inlineRows = this.table.find('tr.form-row');
|
||||||
|
this.inlineRows.tabularFormset('table.inline tr.form-row', {
|
||||||
|
prefix: 'second'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('first form has delete checkbox and no button', function(assert) {
|
||||||
|
var tr = this.inlineRows.slice(0, 1);
|
||||||
|
assert.ok(tr.hasClass('dynamic-second'));
|
||||||
|
assert.ok(tr.hasClass('has_original'));
|
||||||
|
assert.equal(tr.find('td.delete input').length, 1);
|
||||||
|
assert.equal(tr.find('td.delete .inline-deletelink').length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('dynamic form has remove button', function(assert) {
|
||||||
|
var tr = this.inlineRows.slice(1, 2);
|
||||||
|
assert.ok(tr.hasClass('dynamic-second'));
|
||||||
|
assert.notOk(tr.hasClass('has_original'));
|
||||||
|
assert.equal(tr.find('.inline-deletelink').length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('dynamic template has nothing', function(assert) {
|
||||||
|
var tr = this.inlineRows.slice(2, 3);
|
||||||
|
assert.ok(tr.hasClass('empty-form'));
|
||||||
|
assert.notOk(tr.hasClass('dynamic-second'));
|
||||||
|
assert.notOk(tr.hasClass('has_original'));
|
||||||
|
assert.equal(tr.find('td.delete')[0].innerHTML, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('removing a form-row also removed related row with non-field errors', function(assert) {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
assert.ok(this.table.find('.row-form-errors').length);
|
||||||
|
var tr = this.inlineRows.slice(1, 2);
|
||||||
|
var trWithErrors = tr.prev();
|
||||||
|
assert.ok(trWithErrors.hasClass('row-form-errors'));
|
||||||
|
var deleteLink = tr.find('a.inline-deletelink');
|
||||||
|
deleteLink.trigger($.Event('click', {target: deleteLink}));
|
||||||
|
assert.notOk(this.table.find('.row-form-errors').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('removing and adding a row keeps cycling row1 and row2 classes', function(assert) {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
var tr = this.inlineRows.slice(1, 2);
|
||||||
|
var deleteLink = tr.find('a.inline-deletelink');
|
||||||
|
var addLink = this.table.find('.add-row > td > a');
|
||||||
|
assert.ok(this.table.find('tr.form-row:even').hasClass('row1'));
|
||||||
|
assert.ok(this.table.find('tr.form-row:odd').hasClass('row2'));
|
||||||
|
deleteLink.trigger($.Event('click', {target: deleteLink}));
|
||||||
|
assert.ok(this.table.find('tr.form-row:even').hasClass('row1'));
|
||||||
|
assert.ok(this.table.find('tr.form-row:odd').hasClass('row2'));
|
||||||
|
addLink.trigger($.Event('click', {target: addLink}));
|
||||||
|
assert.ok(this.table.find('tr.form-row:even').hasClass('row1'));
|
||||||
|
assert.ok(this.table.find('tr.form-row:odd').hasClass('row2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
QUnit.module('admin.inlines: tabular formsets with max_num', {
|
||||||
|
beforeEach: function() {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
$('#qunit-fixture').append($('#tabular-formset-with-validation-error').text());
|
||||||
|
this.table = $('table.inline');
|
||||||
|
this.maxNum = $('input.id_second-MAX_NUM_FORMS');
|
||||||
|
this.maxNum.val(2);
|
||||||
|
this.inlineRows = this.table.find('tr.form-row');
|
||||||
|
this.inlineRows.tabularFormset('table.inline tr.form-row', {
|
||||||
|
prefix: 'second'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('does not show the add button if already at max_num', function(assert) {
|
||||||
|
var addButton = this.table.find('tr.add_row > td > a');
|
||||||
|
assert.notOk(addButton.is(':visible'));
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('make addButton visible again', function(assert) {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
var addButton = this.table.find('tr.add_row > td > a');
|
||||||
|
var removeButton = this.table.find('tr.form-row:first').find('a.inline-deletelink');
|
||||||
|
removeButton.trigger($.Event( "click", { target: removeButton } ));
|
||||||
|
assert.notOk(addButton.is(':visible'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
QUnit.module('admin.inlines: tabular formsets with min_num', {
|
||||||
|
beforeEach: function() {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
$('#qunit-fixture').append($('#tabular-formset-with-validation-error').text());
|
||||||
|
this.table = $('table.inline');
|
||||||
|
this.minNum = $('input#id_second-MIN_NUM_FORMS');
|
||||||
|
this.minNum.val(2);
|
||||||
|
this.inlineRows = this.table.find('tr.form-row');
|
||||||
|
this.inlineRows.tabularFormset('table.inline tr.form-row', {
|
||||||
|
prefix: 'second'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('does not show the remove buttons if already at min_num', function(assert) {
|
||||||
|
assert.notOk(this.table.find('.inline-deletelink:visible').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('make removeButtons visible again', function(assert) {
|
||||||
|
var $ = django.jQuery;
|
||||||
|
var addButton = this.table.find('tr.add-row > td > a');
|
||||||
|
addButton.trigger($.Event( "click", { target: addButton } ));
|
||||||
|
assert.equal(this.table.find('.inline-deletelink:visible').length, 2);
|
||||||
|
});
|
||||||
|
@ -28,18 +28,56 @@
|
|||||||
<input id="id_first-TOTAL_FORMS" value="1">
|
<input id="id_first-TOTAL_FORMS" value="1">
|
||||||
<input id="id_first-MAX_NUM_FORMS" value="">
|
<input id="id_first-MAX_NUM_FORMS" value="">
|
||||||
<table class="inline">
|
<table class="inline">
|
||||||
<tr id="first-0" class="form-row">
|
<tr id="first-0" class="form-row row1">
|
||||||
<td class="field-test_field">
|
<td class="field-test_field">
|
||||||
<input id="id_first-test_field">
|
<input id="id_first-0-test_field">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="first-empty" class="empty-row">
|
<tr id="first-empty" class="form-row empty-form">
|
||||||
<td class="field-test_field">
|
<td class="field-test_field">
|
||||||
<input id="id_first-test_field">
|
<input id="id_first-__prefix__-test_field">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</script>
|
</script>
|
||||||
|
<script type="text/html" id="tabular-formset-with-validation-error">
|
||||||
|
<div class="inline-group">
|
||||||
|
<div class="tabular inline-related">
|
||||||
|
<input id="id_second-TOTAL_FORMS" value="2">
|
||||||
|
<input id="id_second-MAX_NUM_FORMS" value="">
|
||||||
|
<input id="id_second-MIN_NUM_FORMS" value="">
|
||||||
|
<table class="inline">
|
||||||
|
<tr id="second-0" class="form-row has_original row1">
|
||||||
|
<td class="field-test_field">
|
||||||
|
<input id="id_second-0-test_field">
|
||||||
|
</td>
|
||||||
|
<td class="delete">
|
||||||
|
<input type="checkbox" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="row-form-errors">
|
||||||
|
<td colspan="2">
|
||||||
|
<ul class="errorlist nonfield">
|
||||||
|
<li>This next form has non-field errors.</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="second-1" class="form-row row2">
|
||||||
|
<td class="field-test_field">
|
||||||
|
<input id="id_second-1-test_field">
|
||||||
|
</td>
|
||||||
|
<td class="delete"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="second-empty" class="form-row empty-form">
|
||||||
|
<td class="field-test_field">
|
||||||
|
<input id="id_second-__prefix__-test_field">
|
||||||
|
</td>
|
||||||
|
<td class="delete"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="./qunit/qunit.js"></script>
|
<script src="./qunit/qunit.js"></script>
|
||||||
|
|
||||||
|
@ -110,11 +110,21 @@ class Inner4Stacked(models.Model):
|
|||||||
dummy = models.IntegerField(help_text="Awesome stacked help text is awesome.")
|
dummy = models.IntegerField(help_text="Awesome stacked help text is awesome.")
|
||||||
holder = models.ForeignKey(Holder4, models.CASCADE)
|
holder = models.ForeignKey(Holder4, models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['dummy', 'holder'], name='unique_stacked_dummy_per_holder')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Inner4Tabular(models.Model):
|
class Inner4Tabular(models.Model):
|
||||||
dummy = models.IntegerField(help_text="Awesome tabular help text is awesome.")
|
dummy = models.IntegerField(help_text="Awesome tabular help text is awesome.")
|
||||||
holder = models.ForeignKey(Holder4, models.CASCADE)
|
holder = models.ForeignKey(Holder4, models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['dummy', 'holder'], name='unique_tabular_dummy_per_holder')
|
||||||
|
]
|
||||||
|
|
||||||
# Models for #12749
|
# Models for #12749
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ class TestInline(TestDataMixin, TestCase):
|
|||||||
# Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbox.
|
# Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbox.
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
'<tr><td colspan="4"><ul class="errorlist nonfield">'
|
'<tr class="row-form-errors"><td colspan="4"><ul class="errorlist nonfield">'
|
||||||
'<li>The two titles must be the same</li></ul></td></tr>'
|
'<li>The two titles must be the same</li></ul></td></tr>'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -907,8 +907,100 @@ class SeleniumTests(AdminSeleniumTestCase):
|
|||||||
self.assertEqual(rows_length(), 5, msg="sanity check")
|
self.assertEqual(rows_length(), 5, msg="sanity check")
|
||||||
for delete_link in self.selenium.find_elements_by_css_selector('%s .inline-deletelink' % inline_id):
|
for delete_link in self.selenium.find_elements_by_css_selector('%s .inline-deletelink' % inline_id):
|
||||||
delete_link.click()
|
delete_link.click()
|
||||||
|
with self.disable_implicit_wait():
|
||||||
|
self.assertEqual(rows_length(), 0)
|
||||||
|
|
||||||
|
def test_delete_invalid_stacked_inlines(self):
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
|
self.admin_login(username='super', password='secret')
|
||||||
|
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
|
||||||
|
|
||||||
|
inline_id = '#inner4stacked_set-group'
|
||||||
|
|
||||||
|
def rows_length():
|
||||||
|
return len(self.selenium.find_elements_by_css_selector('%s .dynamic-inner4stacked_set' % inline_id))
|
||||||
self.assertEqual(rows_length(), 3)
|
self.assertEqual(rows_length(), 3)
|
||||||
|
|
||||||
|
add_button = self.selenium.find_element_by_link_text(
|
||||||
|
'Add another Inner4 stacked')
|
||||||
|
add_button.click()
|
||||||
|
add_button.click()
|
||||||
|
self.assertEqual(len(self.selenium.find_elements_by_css_selector('#id_inner4stacked_set-4-dummy')), 1)
|
||||||
|
|
||||||
|
# Enter some data and click 'Save'.
|
||||||
|
self.selenium.find_element_by_name('dummy').send_keys('1')
|
||||||
|
self.selenium.find_element_by_name('inner4stacked_set-0-dummy').send_keys('100')
|
||||||
|
self.selenium.find_element_by_name('inner4stacked_set-1-dummy').send_keys('101')
|
||||||
|
self.selenium.find_element_by_name('inner4stacked_set-2-dummy').send_keys('222')
|
||||||
|
self.selenium.find_element_by_name('inner4stacked_set-3-dummy').send_keys('103')
|
||||||
|
self.selenium.find_element_by_name('inner4stacked_set-4-dummy').send_keys('222')
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.wait_page_loaded()
|
||||||
|
|
||||||
|
self.assertEqual(rows_length(), 5, msg="sanity check")
|
||||||
|
errorlist = self.selenium.find_element_by_css_selector(
|
||||||
|
'%s .dynamic-inner4stacked_set .errorlist li' % inline_id
|
||||||
|
)
|
||||||
|
self.assertEqual('Please correct the duplicate values below.', errorlist.text)
|
||||||
|
delete_link = self.selenium.find_element_by_css_selector('#inner4stacked_set-4 .inline-deletelink')
|
||||||
|
delete_link.click()
|
||||||
|
self.assertEqual(rows_length(), 4)
|
||||||
|
with self.disable_implicit_wait(), self.assertRaises(NoSuchElementException):
|
||||||
|
self.selenium.find_element_by_css_selector('%s .dynamic-inner4stacked_set .errorlist li' % inline_id)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.wait_page_loaded()
|
||||||
|
|
||||||
|
# The objects have been created in the database.
|
||||||
|
self.assertEqual(Inner4Stacked.objects.all().count(), 4)
|
||||||
|
|
||||||
|
def test_delete_invalid_tabular_inlines(self):
|
||||||
|
from selenium.common.exceptions import NoSuchElementException
|
||||||
|
self.admin_login(username='super', password='secret')
|
||||||
|
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
|
||||||
|
|
||||||
|
inline_id = '#inner4tabular_set-group'
|
||||||
|
|
||||||
|
def rows_length():
|
||||||
|
return len(self.selenium.find_elements_by_css_selector('%s .dynamic-inner4tabular_set' % inline_id))
|
||||||
|
self.assertEqual(rows_length(), 3)
|
||||||
|
|
||||||
|
add_button = self.selenium.find_element_by_link_text(
|
||||||
|
'Add another Inner4 tabular')
|
||||||
|
add_button.click()
|
||||||
|
add_button.click()
|
||||||
|
self.assertEqual(len(self.selenium.find_elements_by_css_selector('#id_inner4tabular_set-4-dummy')), 1)
|
||||||
|
|
||||||
|
# Enter some data and click 'Save'.
|
||||||
|
self.selenium.find_element_by_name('dummy').send_keys('1')
|
||||||
|
self.selenium.find_element_by_name('inner4tabular_set-0-dummy').send_keys('100')
|
||||||
|
self.selenium.find_element_by_name('inner4tabular_set-1-dummy').send_keys('101')
|
||||||
|
self.selenium.find_element_by_name('inner4tabular_set-2-dummy').send_keys('222')
|
||||||
|
self.selenium.find_element_by_name('inner4tabular_set-3-dummy').send_keys('103')
|
||||||
|
self.selenium.find_element_by_name('inner4tabular_set-4-dummy').send_keys('222')
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.wait_page_loaded()
|
||||||
|
|
||||||
|
self.assertEqual(rows_length(), 5, msg="sanity check")
|
||||||
|
|
||||||
|
# Non-field errorlist is in its own <tr> just before
|
||||||
|
# tr#inner4tabular_set-3:
|
||||||
|
errorlist = self.selenium.find_element_by_css_selector(
|
||||||
|
'%s #inner4tabular_set-3 + .row-form-errors .errorlist li' % inline_id
|
||||||
|
)
|
||||||
|
self.assertEqual('Please correct the duplicate values below.', errorlist.text)
|
||||||
|
delete_link = self.selenium.find_element_by_css_selector('#inner4tabular_set-4 .inline-deletelink')
|
||||||
|
delete_link.click()
|
||||||
|
self.assertEqual(rows_length(), 4)
|
||||||
|
with self.disable_implicit_wait(), self.assertRaises(NoSuchElementException):
|
||||||
|
self.selenium.find_element_by_css_selector('%s .dynamic-inner4tabular_set .errorlist li' % inline_id)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.wait_page_loaded()
|
||||||
|
|
||||||
|
# The objects have been created in the database.
|
||||||
|
self.assertEqual(Inner4Tabular.objects.all().count(), 4)
|
||||||
|
|
||||||
def test_add_inlines(self):
|
def test_add_inlines(self):
|
||||||
"""
|
"""
|
||||||
The "Add another XXX" link correctly adds items to the inline form.
|
The "Add another XXX" link correctly adds items to the inline form.
|
||||||
|
Loading…
Reference in New Issue
Block a user