Ticket #9025: nested_inlines_finished.diff
File nested_inlines_finished.diff, 86.2 KB (added by , 12 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index f4205f2..352cd8b 100644
a b class ModelAdmin(BaseModelAdmin): 715 715 """ 716 716 obj.delete() 717 717 718 def save_formset(self, request, form , formset, change):718 def save_formset(self, request, formset, change): 719 719 """ 720 720 Given an inline formset save it to the database. 721 721 """ 722 722 formset.save() 723 for form in formset.forms: 724 if hasattr(form, 'nested_formsets'): 725 for nested_formset in form.nested_formsets: 726 self.save_formset(request, nested_formset, change) 727 723 728 724 729 def save_related(self, request, form, formsets, change): 725 730 """ … … class ModelAdmin(BaseModelAdmin): 731 736 """ 732 737 form.save_m2m() 733 738 for formset in formsets: 734 self.save_formset(request, form , formset, change=change)739 self.save_formset(request, formset, change=change) 735 740 736 741 def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): 737 742 opts = self.model._meta … … class ModelAdmin(BaseModelAdmin): 920 925 self.message_user(request, msg) 921 926 return None 922 927 928 929 930 def add_nested_inline_formsets(self, request, inline, formset, depth=0): 931 if depth > 5: 932 raise Exception("Maximum nesting depth reached (5)") 933 for form in formset.forms: 934 nested_formsets = [] 935 for nested_inline in inline.get_inline_instances(request): 936 InlineFormSet = nested_inline.get_formset(request, form.instance) 937 prefix = "%s-%s" % (form.prefix, InlineFormSet.get_default_prefix()) 938 if request.method == 'POST': 939 nested_formset = InlineFormSet(request.POST, request.FILES, 940 instance=form.instance, 941 prefix=prefix, queryset=nested_inline.queryset(request)) 942 else: 943 nested_formset = InlineFormSet(instance=form.instance, 944 prefix=prefix, queryset=nested_inline.queryset(request)) 945 nested_formsets.append(nested_formset) 946 if nested_inline.inlines: 947 self.add_nested_inline_formsets(request, nested_inline, nested_formset, depth=depth+1) 948 form.nested_formsets = nested_formsets 949 950 def wrap_nested_inline_formsets(self, request, inline, formset): 951 media = None 952 def get_media(extra_media): 953 if media: 954 return media + extra_media 955 else: 956 return extra_media 957 958 for form in formset.forms: 959 wrapped_nested_formsets = [] 960 for nested_inline, nested_formset in zip(inline.get_inline_instances(request), form.nested_formsets): 961 if form.instance.pk: 962 instance = form.instance 963 else: 964 instance = None 965 fieldsets = list(nested_inline.get_fieldsets(request)) 966 readonly = list(nested_inline.get_readonly_fields(request)) 967 prepopulated = dict(nested_inline.get_prepopulated_fields(request)) 968 wrapped_nested_formset = helpers.InlineAdminFormSet(nested_inline, nested_formset, 969 fieldsets, prepopulated, readonly, model_admin=self) 970 wrapped_nested_formsets.append(wrapped_nested_formset) 971 media = get_media(wrapped_nested_formset.media) 972 if nested_inline.inlines: 973 media = get_media(self.wrap_nested_inline_formsets(request, nested_inline, nested_formset)) 974 form.nested_formsets = wrapped_nested_formsets 975 return media 976 977 def all_valid_with_nesting(self, formsets): 978 "Recursively validate all nested formsets" 979 if not all_valid(formsets): 980 return False 981 for formset in formsets: 982 if not formset.is_bound: 983 pass 984 for form in formset: 985 if hasattr(form, 'nested_formsets'): 986 if not self.all_valid_with_nesting(form.nested_formsets): 987 return False 988 # Here be dragons :( 989 if not form.cleaned_data: 990 form._errors["__all__"] = form.error_class([u"Parent object must be created when creating nested inlines."]) 991 return False 992 return True 993 994 923 995 @csrf_protect_m 924 996 @transaction.commit_on_success 925 997 def add_view(self, request, form_url='', extra_context=None): … … class ModelAdmin(BaseModelAdmin): 952 1024 save_as_new="_saveasnew" in request.POST, 953 1025 prefix=prefix, queryset=inline.queryset(request)) 954 1026 formsets.append(formset) 955 if all_valid(formsets) and form_validated: 1027 if inline.inlines: 1028 self.add_nested_inline_formsets(request, inline, formset) 1029 if self.all_valid_with_nesting(formsets) and form_validated: 956 1030 self.save_model(request, new_object, form, False) 957 1031 self.save_related(request, form, formsets, False) 958 1032 self.log_addition(request, new_object) … … class ModelAdmin(BaseModelAdmin): 978 1052 formset = FormSet(instance=self.model(), prefix=prefix, 979 1053 queryset=inline.queryset(request)) 980 1054 formsets.append(formset) 1055 if inline.inlines: 1056 self.add_nested_inline_formsets(request, inline, formset) 981 1057 982 1058 adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), 983 1059 self.get_prepopulated_fields(request), … … class ModelAdmin(BaseModelAdmin): 994 1070 fieldsets, prepopulated, readonly, model_admin=self) 995 1071 inline_admin_formsets.append(inline_admin_formset) 996 1072 media = media + inline_admin_formset.media 1073 if inline.inlines: 1074 media = media + self.wrap_nested_inline_formsets(request, inline, formset) 997 1075 998 1076 context = { 999 1077 'title': _('Add %s') % force_text(opts.verbose_name), … … class ModelAdmin(BaseModelAdmin): 1047 1125 formset = FormSet(request.POST, request.FILES, 1048 1126 instance=new_object, prefix=prefix, 1049 1127 queryset=inline.queryset(request)) 1050 1051 1128 formsets.append(formset) 1129 if inline.inlines: 1130 self.add_nested_inline_formsets(request, inline, formset) 1052 1131 1053 if all_valid(formsets) and form_validated:1132 if self.all_valid_with_nesting(formsets) and form_validated: 1054 1133 self.save_model(request, new_object, form, True) 1055 1134 self.save_related(request, form, formsets, True) 1056 1135 change_message = self.construct_change_message(request, form, formsets) … … class ModelAdmin(BaseModelAdmin): 1068 1147 formset = FormSet(instance=obj, prefix=prefix, 1069 1148 queryset=inline.queryset(request)) 1070 1149 formsets.append(formset) 1150 if inline.inlines: 1151 self.add_nested_inline_formsets(request, inline, formset) 1071 1152 1072 1153 adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), 1073 1154 self.get_prepopulated_fields(request, obj), … … class ModelAdmin(BaseModelAdmin): 1084 1165 fieldsets, prepopulated, readonly, model_admin=self) 1085 1166 inline_admin_formsets.append(inline_admin_formset) 1086 1167 media = media + inline_admin_formset.media 1168 if inline.inlines: 1169 media = media + self.wrap_nested_inline_formsets(request, inline, formset) 1087 1170 1088 1171 context = { 1089 1172 'title': _('Change %s') % force_text(opts.verbose_name), … … class InlineModelAdmin(BaseModelAdmin): 1358 1441 verbose_name = None 1359 1442 verbose_name_plural = None 1360 1443 can_delete = True 1444 inlines = [] 1361 1445 1362 1446 def __init__(self, parent_model, admin_site): 1363 1447 self.admin_site = admin_site … … class InlineModelAdmin(BaseModelAdmin): 1369 1453 if self.verbose_name_plural is None: 1370 1454 self.verbose_name_plural = self.model._meta.verbose_name_plural 1371 1455 1456 def get_inline_instances(self, request): 1457 inline_instances = [] 1458 for inline_class in self.inlines: 1459 inline = inline_class(self.model, self.admin_site) 1460 if request: 1461 if not (inline.has_add_permission(request) or 1462 inline.has_change_permission(request) or 1463 inline.has_delete_permission(request)): 1464 continue 1465 if not inline.has_add_permission(request): 1466 inline.max_num = 0 1467 inline_instances.append(inline) 1468 return inline_instances 1469 1372 1470 @property 1373 1471 def media(self): 1374 1472 extra = '' if settings.DEBUG else '.min' -
django/contrib/admin/static/admin/css/forms.css
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index efec04b..35224aa 100644
a b body.popup .submit-row { 285 285 color: #fff; 286 286 } 287 287 288 .inline-group .tabular fieldset.module {288 .inline-group .tabular > fieldset.module { 289 289 border: none; 290 290 border-bottom: 1px solid #ddd; 291 291 } … … body.popup .submit-row { 358 358 outline: 0; /* Remove dotted border around link */ 359 359 } 360 360 361 .nested-inline { 362 margin: 10px; 363 } 364 365 td > .nested-inline { 366 margin: 0px; 367 } 368 369 .nested-inline-bottom-border { 370 border-bottom: 1px solid #DDDDDD; 371 } 372 373 .no-bottom-border.row1 > td { 374 border-bottom: solid #EDF3FE 1px; 375 } 376 377 .no-bottom-border.row2 > td { 378 border-bottom: solid white 1px; 379 } 380 361 381 .empty-form { 362 382 display: none; 363 383 } -
django/contrib/admin/static/admin/js/inlines.js
diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index 4dc9459..4d1943d 100644
a b 15 15 * See: http://www.opensource.org/licenses/bsd-license.php 16 16 */ 17 17 (function($) { 18 $.fn.formset = function(opts) { 19 var options = $.extend({}, $.fn.formset.defaults, opts); 20 var $this = $(this); 21 var $parent = $this.parent(); 22 var updateElementIndex = function(el, prefix, ndx) { 23 var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); 24 var replacement = prefix + "-" + ndx; 25 if ($(el).attr("for")) { 26 $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); 27 } 28 if (el.id) { 29 el.id = el.id.replace(id_regex, replacement); 30 } 31 if (el.name) { 32 el.name = el.name.replace(id_regex, replacement); 33 } 34 }; 35 var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); 36 var nextIndex = parseInt(totalForms.val(), 10); 37 var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); 38 // only show the add button if we are allowed to add more items, 39 // note that max_num = None translates to a blank string. 40 var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; 41 $this.each(function(i) { 42 $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); 43 }); 44 if ($this.length && showAddButton) { 45 var addButton; 46 if ($this.attr("tagName") == "TR") { 47 // If forms are laid out as table rows, insert the 48 // "add" button in a new table row: 49 var numCols = this.eq(-1).children().length; 50 $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>"); 51 addButton = $parent.find("tr:last a"); 52 } else { 53 // Otherwise, insert it immediately after the last form: 54 $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>"); 55 addButton = $this.filter(":last").next().find("a"); 56 } 57 addButton.click(function(e) { 58 e.preventDefault(); 59 var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); 60 var template = $("#" + options.prefix + "-empty"); 61 var row = template.clone(true); 62 row.removeClass(options.emptyCssClass) 63 .addClass(options.formCssClass) 64 .attr("id", options.prefix + "-" + nextIndex); 65 if (row.is("tr")) { 66 // If the forms are laid out in table rows, insert 67 // the remove button into the last table cell: 68 row.children(":last").append('<div><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></div>"); 69 } else if (row.is("ul") || row.is("ol")) { 70 // If they're laid out as an ordered/unordered list, 71 // insert an <li> after the last list item: 72 row.append('<li><a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + "</a></li>"); 73 } else { 74 // Otherwise, just insert the remove button as the 75 // last child element of the form's container: 76 row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>"); 77 } 78 row.find("*").each(function() { 79 updateElementIndex(this, options.prefix, totalForms.val()); 80 }); 81 // Insert the new form when it has been fully edited 82 row.insertBefore($(template)); 83 // Update number of total forms 84 $(totalForms).val(parseInt(totalForms.val(), 10) + 1); 85 nextIndex += 1; 86 // Hide add button in case we've hit the max, except we want to add infinitely 87 if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { 88 addButton.parent().hide(); 89 } 90 // The delete button of each row triggers a bunch of other things 91 row.find("a." + options.deleteCssClass).click(function(e) { 92 e.preventDefault(); 93 // Remove the parent form containing this button: 94 var row = $(this).parents("." + options.formCssClass); 95 row.remove(); 96 nextIndex -= 1; 97 // If a post-delete callback was provided, call it with the deleted form: 98 if (options.removed) { 99 options.removed(row); 100 } 101 // Update the TOTAL_FORMS form count. 102 var forms = $("." + options.formCssClass); 103 $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); 104 // Show add button again once we drop below max 105 if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { 106 addButton.parent().show(); 107 } 108 // Also, update names and ids for all remaining form controls 109 // so they remain in sequence: 110 for (var i=0, formCount=forms.length; i<formCount; i++) 111 { 112 updateElementIndex($(forms).get(i), options.prefix, i); 113 $(forms.get(i)).find("*").each(function() { 114 updateElementIndex(this, options.prefix, i); 115 }); 116 } 117 }); 118 // If a post-add callback was supplied, call it with the added form: 119 if (options.added) { 120 options.added(row); 121 } 122 }); 123 } 124 return this; 125 }; 18 $.fn.formset = function(opts) { 19 var options = $.extend({}, $.fn.formset.defaults, opts); 20 var $this = $(this); 21 var $parent = $this.parent(); 22 var updateElementIndex = function(el, prefix, ndx) { 23 var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); 24 var replacement = prefix + "-" + ndx; 25 if ($(el).attr("for")) { 26 $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); 27 } 28 if (el.id) { 29 el.id = el.id.replace(id_regex, replacement); 30 } 31 if (el.name) { 32 el.name = el.name.replace(id_regex, replacement); 33 } 34 }; 35 var nextIndex = get_no_forms(options.prefix); 126 36 127 /* Setup plugin defaults */ 128 $.fn.formset.defaults = { 129 prefix: "form", // The form prefix for your django formset 130 addText: "add another", // Text for the add link 131 deleteText: "remove", // Text for the delete link 132 addCssClass: "add-row", // CSS class applied to the add link 133 deleteCssClass: "delete-row", // CSS class applied to the delete link 134 emptyCssClass: "empty-row", // CSS class applied to the empty row 135 formCssClass: "dynamic-form", // CSS class applied to each form in a formset 136 added: null, // Function called each time a new form is added 137 removed: null // Function called each time a form is deleted 138 }; 37 // Add form classes for dynamic behaviour 38 $this.each(function(i) { 39 $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); 40 }); 41 // Only show the add button if we are allowed to add more items, 42 // note that max_num = None translates to a blank string. 43 var showAddButton = get_max_forms(options.prefix) === '' || (get_max_forms(options.prefix) - get_no_forms(options.prefix)) > 0; 44 if ($this.length && showAddButton) { 45 var addButton; 46 if ($this.attr("tagName") == "TR") { 47 // If forms are laid out as table rows, insert the 48 // "add" button in a new table row: 49 var numCols = this.eq(-1).children().length; 50 $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>"); 51 addButton = $parent.find("tr:last a"); 52 } else { 53 // Otherwise, insert it immediately after the last form: 54 $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>"); 55 addButton = $this.filter(":last").next().find("a"); 56 } 57 addButton.click(function(e) { 58 e.preventDefault(); 59 var nextIndex = get_no_forms(options.prefix); 60 var template = $("#" + options.prefix + "-empty"); 61 var row = template.clone(true); 62 row.removeClass(options.emptyCssClass).addClass(options.formCssClass).attr("id", options.prefix + "-" + nextIndex); 63 if (row.is("tr")) { 64 // If the forms are laid out in table rows, insert 65 // the remove button into the last table cell: 66 row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></div>"); 67 } else if (row.is("ul") || row.is("ol")) { 68 // If they're laid out as an ordered/unordered list, 69 // insert an <li> after the last list item: 70 row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></li>"); 71 } else { 72 // Otherwise, just insert the remove button as the 73 // last child element of the form's container: 74 row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>"); 75 } 76 row.find("*").each(function() { 77 updateElementIndex(this, options.prefix, nextIndex); 78 }); 79 // when adding something from a cloned formset the id is the same 139 80 81 // Insert the new form when it has been fully edited 82 row.insertBefore($(template)); 140 83 141 // Tabular inlines --------------------------------------------------------- 142 $.fn.tabularFormset = function(options) { 143 var $rows = $(this); 144 var alternatingRows = function(row) { 145 $($rows.selector).not(".add-row").removeClass("row1 row2") 146 .filter(":even").addClass("row1").end() 147 .filter(":odd").addClass("row2"); 148 }; 84 // Update number of total forms 85 change_no_forms(options.prefix, true); 149 86 150 var reinitDateTimeShortCuts = function() { 151 // Reinitialize the calendar and clock widgets by force 152 if (typeof DateTimeShortcuts != "undefined") { 153 $(".datetimeshortcuts").remove(); 154 DateTimeShortcuts.init(); 155 } 156 }; 87 // Hide add button in case we've hit the max, except we want to add infinitely 88 if ((get_max_forms(options.prefix) !== '') && (get_max_forms(options.prefix) - get_no_forms(options.prefix)) <= 0) { 89 addButton.parent().hide(); 90 } 157 91 158 var updateSelectFilter = function() { 159 // If any SelectFilter widgets are a part of the new form, 160 // instantiate a new SelectFilter instance for it. 161 if (typeof SelectFilter != 'undefined'){ 162 $('.selectfilter').each(function(index, value){ 163 var namearr = value.name.split('-'); 164 SelectFilter.init(value.id, namearr[namearr.length-1], false, options.adminStaticPrefix ); 165 }); 166 $('.selectfilterstacked').each(function(index, value){ 167 var namearr = value.name.split('-'); 168 SelectFilter.init(value.id, namearr[namearr.length-1], true, options.adminStaticPrefix ); 169 }); 170 } 171 }; 92 // The delete button of each row triggers a bunch of other things 93 row.find("a." + options.deleteCssClass).click(function(e) { 94 e.preventDefault(); 95 // Find the row that will be deleted by this button 96 var row = $(this).parents("." + options.formCssClass); 97 // Remove the parent form containing this button: 98 var formset_to_update = row.parent(); 99 while (row.next().hasClass('nested-inline-row')) { 100 row.next().remove(); 101 } 102 row.remove(); 103 change_no_forms(options.prefix, false); 104 // If a post-delete callback was provided, call it with the deleted form: 105 if (options.removed) { 106 options.removed(formset_to_update); 107 } 172 108 173 var initPrepopulatedFields = function(row) { 174 row.find('.prepopulated_field').each(function() { 175 var field = $(this), 176 input = field.find('input, select, textarea'), 177 dependency_list = input.data('dependency_list') || [], 178 dependencies = []; 179 $.each(dependency_list, function(i, field_name) { 180 dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); 181 }); 182 if (dependencies.length) { 183 input.prepopulate(dependencies, input.attr('maxlength')); 184 } 185 }); 186 }; 109 }); 187 110 188 $rows.formset({ 189 prefix: options.prefix, 190 addText: options.addText, 191 formCssClass: "dynamic-" + options.prefix, 192 deleteCssClass: "inline-deletelink", 193 deleteText: options.deleteText, 194 emptyCssClass: "empty-form", 195 removed: alternatingRows, 196 added: function(row) { 197 initPrepopulatedFields(row); 198 reinitDateTimeShortCuts(); 199 updateSelectFilter(); 200 alternatingRows(row); 201 } 202 }); 111 if (row.is("tr")) { 112 // If the forms are laid out in table rows, insert 113 // the remove button into the last table cell: 114 // Insert the nested formsets into the new form 115 nested_formsets = create_nested_formset(options.prefix, nextIndex, options, false); 116 if (nested_formsets.length) { 117 row.addClass("no-bottom-border"); 118 } 119 nested_formsets.each(function() { 120 if (!$(this).next()) { 121 border_class = ""; 122 } else { 123 border_class = " no-bottom-border"; 124 } 125 ($('<tr class="nested-inline-row' + border_class + '">').html(($('<td>', { 126 colspan : '100%' 127 }).html($(this))))).insertBefore($(template)); 128 }); 129 } else { 130 // stacked 131 // Insert the nested formsets into the new form 132 nested_formsets = create_nested_formset(options.prefix, nextIndex, options, true); 133 nested_formsets.each(function() { 134 row.append($(this)); 135 }); 136 } 203 137 204 return $rows; 205 }; 138 // If a post-add callback was supplied, call it with the added form: 139 if (options.added) { 140 options.added(row); 141 } 206 142 207 // Stacked inlines --------------------------------------------------------- 208 $.fn.stackedFormset = function(options) { 209 var $rows = $(this); 210 var updateInlineLabel = function(row) { 211 $($rows.selector).find(".inline_label").each(function(i) { 212 var count = i + 1; 213 $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); 214 }); 215 }; 143 nextIndex = nextIndex + 1; 144 }); 145 } 146 return this; 147 }; 216 148 217 var reinitDateTimeShortCuts = function() { 218 // Reinitialize the calendar and clock widgets by force, yuck. 219 if (typeof DateTimeShortcuts != "undefined") { 220 $(".datetimeshortcuts").remove(); 221 DateTimeShortcuts.init(); 222 } 223 }; 149 /* Setup plugin defaults */ 150 $.fn.formset.defaults = { 151 prefix : "form", // The form prefix for your django formset 152 addText : "add another", // Text for the add link 153 deleteText : "remove", // Text for the delete link 154 addCssClass : "add-row", // CSS class applied to the add link 155 deleteCssClass : "delete-row", // CSS class applied to the delete link 156 emptyCssClass : "empty-row", // CSS class applied to the empty row 157 formCssClass : "dynamic-form", // CSS class applied to each form in a formset 158 added : null, // Function called each time a new form is added 159 removed : null // Function called each time a form is deleted 160 }; 224 161 225 var updateSelectFilter = function() { 226 // If any SelectFilter widgets were added, instantiate a new instance. 227 if (typeof SelectFilter != "undefined"){ 228 $(".selectfilter").each(function(index, value){ 229 var namearr = value.name.split('-'); 230 SelectFilter.init(value.id, namearr[namearr.length-1], false, options.adminStaticPrefix); 231 }); 232 $(".selectfilterstacked").each(function(index, value){ 233 var namearr = value.name.split('-'); 234 SelectFilter.init(value.id, namearr[namearr.length-1], true, options.adminStaticPrefix); 235 }); 236 } 237 }; 162 // Tabular inlines --------------------------------------------------------- 163 $.fn.tabularFormset = function(options) { 164 var $rows = $(this); 165 var alternatingRows = function(row) { 166 row_number = 0; 167 $($rows.selector).not(".add-row").removeClass("row1 row2").each(function() { 168 $(this).addClass('row' + ((row_number%2)+1)); 169 next = $(this).next(); 170 while (next.hasClass('nested-inline-row')) { 171 next.addClass('row' + ((row_number%2)+1)); 172 next = next.next(); 173 } 174 row_number = row_number + 1; 175 }); 176 }; 238 177 239 var initPrepopulatedFields = function(row) { 240 row.find('.prepopulated_field').each(function() { 241 var field = $(this), 242 input = field.find('input, select, textarea'), 243 dependency_list = input.data('dependency_list') || [], 244 dependencies = []; 245 $.each(dependency_list, function(i, field_name) { 246 dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); 247 }); 248 if (dependencies.length) { 249 input.prepopulate(dependencies, input.attr('maxlength')); 250 } 251 }); 252 }; 178 var reinitDateTimeShortCuts = function() { 179 // Reinitialize the calendar and clock widgets by force 180 if ( typeof DateTimeShortcuts != "undefined") { 181 $(".datetimeshortcuts").remove(); 182 DateTimeShortcuts.init(); 183 } 184 }; 253 185 254 $rows.formset({ 255 prefix: options.prefix, 256 addText: options.addText, 257 formCssClass: "dynamic-" + options.prefix, 258 deleteCssClass: "inline-deletelink", 259 deleteText: options.deleteText, 260 emptyCssClass: "empty-form", 261 removed: updateInlineLabel, 262 added: (function(row) { 263 initPrepopulatedFields(row); 264 reinitDateTimeShortCuts(); 265 updateSelectFilter(); 266 updateInlineLabel(row); 267 }) 268 }); 186 var updateSelectFilter = function() { 187 // If any SelectFilter widgets are a part of the new form, 188 // instantiate a new SelectFilter instance for it. 189 if ( typeof SelectFilter != 'undefined') { 190 $('.selectfilter').each(function(index, value) { 191 var namearr = value.name.split('-'); 192 SelectFilter.init(value.id, namearr[namearr.length - 1], false, options.adminStaticPrefix); 193 }); 194 $('.selectfilterstacked').each(function(index, value) { 195 var namearr = value.name.split('-'); 196 SelectFilter.init(value.id, namearr[namearr.length - 1], true, options.adminStaticPrefix); 197 }); 198 } 199 }; 269 200 270 return $rows; 271 }; 201 var initPrepopulatedFields = function(row) { 202 row.find('.prepopulated_field').each(function() { 203 var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = []; 204 $.each(dependency_list, function(i, field_name) { 205 dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); 206 }); 207 if (dependencies.length) { 208 input.prepopulate(dependencies, input.attr('maxlength')); 209 } 210 }); 211 }; 212 213 $rows.formset({ 214 prefix : options.prefix, 215 addText : options.addText, 216 formCssClass : "dynamic-" + options.prefix, 217 deleteCssClass : "inline-deletelink", 218 deleteText : options.deleteText, 219 emptyCssClass : "empty-form", 220 removed : alternatingRows, 221 added : function(row) { 222 initPrepopulatedFields(row); 223 reinitDateTimeShortCuts(); 224 updateSelectFilter(); 225 alternatingRows(row); 226 } 227 }); 228 229 return $rows; 230 }; 231 232 // Stacked inlines --------------------------------------------------------- 233 $.fn.stackedFormset = function(options) { 234 var $rows = $(this); 235 236 var update_inline_labels = function(formset_to_update) { 237 formset_to_update.children('.inline-related').not('.empty-form').children('h3').find('.inline_label').each(function(i) { 238 var count = i + 1; 239 $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); 240 }); 241 }; 242 243 var reinitDateTimeShortCuts = function() { 244 // Reinitialize the calendar and clock widgets by force, yuck. 245 if ( typeof DateTimeShortcuts != "undefined") { 246 $(".datetimeshortcuts").remove(); 247 DateTimeShortcuts.init(); 248 } 249 }; 250 251 var updateSelectFilter = function() { 252 // If any SelectFilter widgets were added, instantiate a new instance. 253 if ( typeof SelectFilter != "undefined") { 254 $(".selectfilter").each(function(index, value) { 255 var namearr = value.name.split('-'); 256 SelectFilter.init(value.id, namearr[namearr.length - 1], false, options.adminStaticPrefix); 257 }); 258 $(".selectfilterstacked").each(function(index, value) { 259 var namearr = value.name.split('-'); 260 SelectFilter.init(value.id, namearr[namearr.length - 1], true, options.adminStaticPrefix); 261 }); 262 } 263 }; 264 265 var initPrepopulatedFields = function(row) { 266 row.find('.prepopulated_field').each(function() { 267 var field = $(this), input = field.find('input, select, textarea'), dependency_list = input.data('dependency_list') || [], dependencies = []; 268 $.each(dependency_list, function(i, field_name) { 269 dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); 270 }); 271 if (dependencies.length) { 272 input.prepopulate(dependencies, input.attr('maxlength')); 273 } 274 }); 275 }; 276 277 $rows.formset({ 278 prefix : options.prefix, 279 addText : options.addText, 280 formCssClass : "dynamic-" + options.prefix, 281 deleteCssClass : "inline-deletelink", 282 deleteText : options.deleteText, 283 emptyCssClass : "empty-form", 284 removed : update_inline_labels, 285 added : (function(row) { 286 initPrepopulatedFields(row); 287 reinitDateTimeShortCuts(); 288 updateSelectFilter(); 289 update_inline_labels(row.parent()); 290 }) 291 }); 292 293 return $rows; 294 }; 295 296 function create_nested_formset(parent_formset_prefix, next_form_id, options, add_bottom_border) { 297 var formsets = $(false); 298 // update options 299 // Normalize prefix to something we can rely on 300 var normalized_parent_formset_prefix = parent_formset_prefix.replace(/[-][0-9][-]/g, "-0-"); 301 // Check if the form should have nested formsets 302 var nested_inlines = $('#' + normalized_parent_formset_prefix + "-group ." + normalized_parent_formset_prefix + "-nested-inline").not('.cloned'); 303 nested_inlines.each(function() { 304 // prefixes for the nested formset 305 var normalized_formset_prefix = $(this).attr('id').split('-group')[0]; 306 // = "parent_formset_prefix"-0-"nested_inline_name"_set 307 var formset_prefix = normalized_formset_prefix.replace(normalized_parent_formset_prefix + "-0", parent_formset_prefix + "-" + next_form_id); 308 // = "parent_formset_prefix"-"next_form_id"-"nested_inline_name"_set 309 // Find the normalized formset and clone it 310 var template = $("#" + normalized_formset_prefix + "-group").clone(); 311 template.addClass('cloned'); 312 if (template.children().first().hasClass('tabular')) { 313 // Template is tabular 314 template.find(".form-row").not(".empty-form").remove(); 315 template.find(".nested-inline-row").remove(); 316 // Make a new form 317 template_form = template.find("#" + normalized_formset_prefix + "-empty") 318 new_form = template_form.clone().removeClass(options.emptyCssClass).addClass("dynamic-" + formset_prefix); 319 new_form.insertBefore(template_form); 320 // Update Form Properties 321 template.find('#id_' + formset_prefix + '-TOTAL_FORMS').val(1); 322 update_props(template, normalized_formset_prefix, formset_prefix); 323 var add_text = template.find('.add-row').text(); 324 template.find('.add-row').remove(); 325 template.find('.tabular.inline-related tbody tr.' + formset_prefix + '-not-nested').tabularFormset({ 326 prefix : formset_prefix, 327 adminStaticPrefix : options.adminStaticPrefix, 328 addText : add_text, 329 deleteText : options.deleteText 330 }); 331 // Create the nested formset 332 var nested_formsets = create_nested_formset(formset_prefix, 0, options, false); 333 if (nested_formsets.length) { 334 template.find(".form-row").addClass('no-bottom-border'); 335 } 336 // Insert nested formsets 337 nested_formsets.each(function() { 338 if (!$(this).next()) { 339 border_class = ""; 340 } else { 341 border_class = " no-bottom-border"; 342 } 343 template.find("#" + formset_prefix + "-empty").before(($('<tr class="nested-inline-row' + border_class + '">').html(($('<td>', { 344 colspan : '100%' 345 }).html($(this)))))); 346 }); 347 } else { 348 // Template is stacked 349 // Create the nested formset 350 var nested_formsets = create_nested_formset(formset_prefix, 0, options, true); 351 template.find(".inline-related").not(".empty-form").remove(); 352 // Make a new form 353 template_form = template.find("#" + normalized_formset_prefix + "-empty") 354 new_form = template_form.clone().removeClass(options.emptyCssClass).addClass("dynamic-" + formset_prefix); 355 new_form.insertBefore(template_form); 356 // Update Form Properties 357 template.find('#id_' + normalized_formset_prefix + '-TOTAL_FORMS').val(1); 358 new_form.find('.inline_label').text('#1'); 359 update_props(template, normalized_formset_prefix, formset_prefix); 360 var add_text = template.find('.add-row').text(); 361 template.find('.add-row').remove(); 362 template.find(".inline-related").stackedFormset({ 363 prefix : formset_prefix, 364 adminStaticPrefix : options.adminStaticPrefix, 365 addText : add_text, 366 deleteText : options.deleteText 367 }); 368 nested_formsets.each(function() { 369 new_form.append($(this)); 370 }); 371 } 372 if (add_bottom_border) { 373 template = template.add($('<div class="nested-inline-bottom-border">')); 374 } 375 if (formsets.length) { 376 formsets = formsets.add(template); 377 } else { 378 formsets = template; 379 } 380 }); 381 return formsets; 382 }; 383 384 function update_props(template, normalized_formset_prefix, formset_prefix) { 385 // Fix template id 386 template.attr('id', template.attr('id').replace(normalized_formset_prefix, formset_prefix)); 387 template.find('*').each(function() { 388 if ($(this).attr("for")) { 389 $(this).attr("for", $(this).attr("for").replace(normalized_formset_prefix, formset_prefix)); 390 } 391 if ($(this).attr("class")) { 392 $(this).attr("class", $(this).attr("class").replace(normalized_formset_prefix, formset_prefix)); 393 } 394 if (this.id) { 395 this.id = this.id.replace(normalized_formset_prefix, formset_prefix); 396 } 397 if (this.name) { 398 this.name = this.name.replace(normalized_formset_prefix, formset_prefix); 399 } 400 }); 401 // fix __prefix__ where needed 402 prefix_fix = template.find(".inline-related").first(); 403 nextIndex = get_no_forms(formset_prefix); 404 if (prefix_fix.hasClass('tabular')) { 405 // tabular 406 prefix_fix = prefix_fix.find('.form-row').first(); 407 prefix_fix.attr('id', prefix_fix.attr('id').replace('-empty', '-' + nextIndex)); 408 } else { 409 // stacked 410 prefix_fix.attr('id', prefix_fix.attr('id').replace('-empty', '-' + nextIndex)); 411 } 412 prefix_fix.find('*').each(function() { 413 if ($(this).attr("for")) { 414 $(this).attr("for", $(this).attr("for").replace('__prefix__', '0')); 415 } 416 if ($(this).attr("class")) { 417 $(this).attr("class", $(this).attr("class").replace('__prefix__', '0')); 418 } 419 if (this.id) { 420 this.id = this.id.replace('__prefix__', '0'); 421 } 422 if (this.name) { 423 this.name = this.name.replace('__prefix__', '0'); 424 } 425 }); 426 }; 427 428 // This returns the amount of forms in the given formset 429 function get_no_forms(formset_prefix) { 430 formset_prop = $("#id_" + formset_prefix + "-TOTAL_FORMS") 431 if (!formset_prop.length) { 432 return 0; 433 } 434 return parseInt(formset_prop.attr("autocomplete", "off").val()); 435 } 436 437 function change_no_forms(formset_prefix, increase) { 438 var no_forms = get_no_forms(formset_prefix); 439 if (increase) { 440 $("#id_" + formset_prefix + "-TOTAL_FORMS").attr("autocomplete", "off").val(parseInt(no_forms) + 1); 441 } else { 442 $("#id_" + formset_prefix + "-TOTAL_FORMS").attr("autocomplete", "off").val(parseInt(no_forms) - 1); 443 } 444 }; 445 446 // This return the maximum amount of forms in the given formset 447 function get_max_forms(formset_prefix) { 448 var max_forms = $("#id_" + formset_prefix + "-MAX_FORMS").attr("autocomplete", "off").val(); 449 if ( typeof max_forms == 'undefined') { 450 return ''; 451 } 452 return parseInt(max_forms); 453 }; 272 454 })(django.jQuery); 455 456 // TODO: 457 // Remove border between tabular fieldset and nested inline 458 // Fix alternating rows -
django/contrib/admin/static/admin/js/inlines.min.js
diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js index d48ee0a..c2fec35 100644
a b 1 (function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).attr("for")&&b(a).attr("for",b(a).attr("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off"),f=""===e.val()||0<e.val()-f.val();c.each(function(){b(this).not("."+ 2 a.emptyCssClass).addClass(a.formCssClass)});if(c.length&&f){var h;"TR"==c.attr("tagName")?(c=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+ 3 "-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){i(this, 4 a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c<f;c++){i(b(d).get(c),a.prefix,c);b(d.get(c)).find("*").each(function(){i(this, 5 a.prefix,c)})}});a.added&&a.added(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};b.fn.tabularFormset=function(d){var a=b(this),c=function(){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+ 6 d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!= 7 typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),c=function(){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix, 8 addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".form-row .field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(), 9 DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a}})(django.jQuery); 1 (function(b){function j(d,a,e,m){var h=b(!1),n=d.replace(/[-][0-9][-]/g,"-0-");b("#"+n+"-group ."+n+"-nested-inline").not(".cloned").each(function(){var f=b(this).attr("id").split("-group")[0],g=f.replace(n+"-0",d+"-"+a),c=b("#"+f+"-group").clone();c.addClass("cloned");if(c.children().first().hasClass("tabular")){c.find(".form-row").not(".empty-form").remove();c.find(".nested-inline-row").remove();template_form=c.find("#"+f+"-empty");new_form=template_form.clone().removeClass(e.emptyCssClass).addClass("dynamic-"+ 2 g);new_form.insertBefore(template_form);c.find("#id_"+g+"-TOTAL_FORMS").val(1);p(c,f,g);f=c.find(".add-row").text();c.find(".add-row").remove();c.find(".tabular.inline-related tbody tr."+g+"-not-nested").tabularFormset({prefix:g,adminStaticPrefix:e.adminStaticPrefix,addText:f,deleteText:e.deleteText});var k=j(g,0,e,!1);k.length&&c.find(".form-row").addClass("no-bottom-border");k.each(function(){border_class=b(this).next()?" no-bottom-border":"";c.find("#"+g+"-empty").before(b('<tr class="nested-inline-row'+ 3 border_class+'">').html(b("<td>",{colspan:"100%"}).html(b(this))))})}else k=j(g,0,e,!0),c.find(".inline-related").not(".empty-form").remove(),template_form=c.find("#"+f+"-empty"),new_form=template_form.clone().removeClass(e.emptyCssClass).addClass("dynamic-"+g),new_form.insertBefore(template_form),c.find("#id_"+f+"-TOTAL_FORMS").val(1),new_form.find(".inline_label").text("#1"),p(c,f,g),f=c.find(".add-row").text(),c.find(".add-row").remove(),c.find(".inline-related").stackedFormset({prefix:g,adminStaticPrefix:e.adminStaticPrefix, 4 addText:f,deleteText:e.deleteText}),k.each(function(){new_form.append(b(this))});m&&(c=c.add(b('<div class="nested-inline-bottom-border">')));h=h.length?h.add(c):c});return h}function p(d,a,e){d.attr("id",d.attr("id").replace(a,e));d.find("*").each(function(){b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace(a,e));b(this).attr("class")&&b(this).attr("class",b(this).attr("class").replace(a,e));this.id&&(this.id=this.id.replace(a,e));this.name&&(this.name=this.name.replace(a,e))}); 5 prefix_fix=d.find(".inline-related").first();nextIndex=i(e);prefix_fix.hasClass("tabular")&&(prefix_fix=prefix_fix.find(".form-row").first());prefix_fix.attr("id",prefix_fix.attr("id").replace("-empty","-"+nextIndex));prefix_fix.find("*").each(function(){b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace("__prefix__","0"));b(this).attr("class")&&b(this).attr("class",b(this).attr("class").replace("__prefix__","0"));this.id&&(this.id=this.id.replace("__prefix__","0"));this.name&&(this.name= 6 this.name.replace("__prefix__","0"))})}function i(d){formset_prop=b("#id_"+d+"-TOTAL_FORMS");return!formset_prop.length?0:parseInt(formset_prop.attr("autocomplete","off").val())}function q(d,a){var e=i(d);a?b("#id_"+d+"-TOTAL_FORMS").attr("autocomplete","off").val(parseInt(e)+1):b("#id_"+d+"-TOTAL_FORMS").attr("autocomplete","off").val(parseInt(e)-1)}function l(d){d=b("#id_"+d+"-MAX_FORMS").attr("autocomplete","off").val();return"undefined"==typeof d?"":parseInt(d)}b.fn.formset=function(d){var a= 7 b.extend({},b.fn.formset.defaults,d),e=b(this),d=e.parent();i(a.prefix);e.each(function(){b(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});var m=""===l(a.prefix)||0<l(a.prefix)-i(a.prefix);if(e.length&&m){var h;"TR"==e.attr("tagName")?(e=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+e+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(e.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+ 8 a.addText+"</a></div>"),h=e.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=i(a.prefix),e=b("#"+a.prefix+"-empty"),c=e.clone(!0);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+f);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+ 9 a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){var c=a.prefix,d=RegExp("("+c+"-(\\d+|__prefix__))"),c=c+"-"+f;b(this).attr("for")&&b(this).attr("for",b(this).attr("for").replace(d,c));this.id&&(this.id=this.id.replace(d,c));this.name&&(this.name=this.name.replace(d,c))});c.insertBefore(b(e));q(a.prefix,!0);""!==l(a.prefix)&&0>=l(a.prefix)-i(a.prefix)&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(c){c.preventDefault();for(var c= 10 b(this).parents("."+a.formCssClass),d=c.parent();c.next().hasClass("nested-inline-row");)c.next().remove();c.remove();q(a.prefix,!1);a.removed&&a.removed(d)});c.is("tr")?(nested_formsets=j(a.prefix,f,a,!1),nested_formsets.length&&c.addClass("no-bottom-border"),nested_formsets.each(function(){border_class=b(this).next()?" no-bottom-border":"";b('<tr class="nested-inline-row'+border_class+'">').html(b("<td>",{colspan:"100%"}).html(b(this))).insertBefore(b(e))})):(nested_formsets=j(a.prefix,f,a,!0), 11 nested_formsets.each(function(){c.append(b(this))}));a.added&&a.added(c);f+=1})}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};b.fn.tabularFormset=function(d){var a=b(this),e=function(){row_number=0;b(a.selector).not(".add-row").removeClass("row1 row2").each(function(){b(this).addClass("row"+(row_number%2+1));for(next=b(this).next();next.hasClass("nested-inline-row");)next.addClass("row"+ 12 (row_number%2+1)),next=next.next();row_number+=1})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),e=d.data("dependency_list")||[],f=[];b.each(e,function(b,c){f.push("#"+a.find(".field-"+c).find("input, select, textarea").attr("id"))});f.length&&d.prepopulate(f,d.attr("maxlength"))}); 13 "undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(b,a){var e=a.name.split("-");SelectFilter.init(a.id,e[e.length-1],!1,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(b,a){var e=a.name.split("-");SelectFilter.init(a.id,e[e.length-1],!0,d.adminStaticPrefix)}));e(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),e=function(a){a.children(".inline-related").not(".empty-form").children("h3").find(".inline_label").each(function(a){a+= 14 1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),e=d.data("dependency_list")||[],f=[];b.each(e,function(b,c){f.push("#"+a.find(".form-row .field-"+c).find("input, select, textarea").attr("id"))});f.length&&d.prepopulate(f, 15 d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var e=b.name.split("-");SelectFilter.init(b.id,e[e.length-1],!1,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var e=b.name.split("-");SelectFilter.init(b.id,e[e.length-1],!0,d.adminStaticPrefix)}));e(a.parent())}});return a}})(django.jQuery); -
django/contrib/admin/templates/admin/edit_inline/stacked.html
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html index 2025dd8..c0c5389 100644
a b 1 1 {% load i18n admin_static %} 2 <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"> 3 <h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2> 4 {{ inline_admin_formset.formset.management_form }} 5 {{ inline_admin_formset.formset.non_form_errors }} 2 <div class="inline-group{% if recursive_formset %} {{ recursive_formset.formset.prefix|default:"Root" }}-nested-inline nested-inline{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group"> 3 {% with recursive_formset=inline_admin_formset stacked_template='admin/edit_inline/stacked.html' tabular_template='admin/edit_inline/tabular.html'%} 4 <h2>{{ recursive_formset.opts.verbose_name_plural|title }}</h2> 5 {{ recursive_formset.formset.management_form }} 6 {{ recursive_formset.formset.non_form_errors }} 6 7 7 {% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">8 <h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b> <span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>8 {% for inline_admin_form in recursive_formset %}<div class="inline-related{% if forloop.last %} empty-form last-related{% endif %}" id="{{ recursive_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> 9 <h3><b>{{ recursive_formset.opts.verbose_name|title }}:</b> <span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span> 9 10 {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %} 10 {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}11 {% if recursive_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %} 11 12 </h3> 12 13 {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} 13 14 {% for fieldset in inline_admin_form %} … … 15 16 {% endfor %} 16 17 {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} 17 18 {{ inline_admin_form.fk_field.field }} 19 {% if inline_admin_form.form.nested_formsets %} 20 {% for inline_admin_formset in inline_admin_form.form.nested_formsets %} 21 {% if inline_admin_formset.opts.template == stacked_template %} 22 {% include stacked_template %} 23 {% else %} 24 {% include tabular_template %} 25 {% endif %} 26 <div class="nested-inline-bottom-border"></div> 27 {% endfor %} 28 {% endif %} 18 29 </div>{% endfor %} 19 30 </div> 20 31 21 32 <script type="text/javascript"> 22 33 (function($) { 23 $("#{{ inline_admin_formset.formset.prefix }}-group.inline-related").stackedFormset({24 prefix: '{{ inline_admin_formset.formset.prefix }}',34 $("#{{ recursive_formset.formset.prefix }}-group > .inline-related").stackedFormset({ 35 prefix: '{{ recursive_formset.formset.prefix }}', 25 36 adminStaticPrefix: '{% static "admin/" %}', 26 deleteText: "{% trans "Remove"%}",27 addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans%}"37 addText: "{% blocktrans with verbose_name=recursive_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}", 38 deleteText: "{% trans "Remove" %}" 28 39 }); 29 40 })(django.jQuery); 30 41 </script> 42 {% endwith %} -
django/contrib/admin/templates/admin/edit_inline/tabular.html
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index f2757ed..1b0de0e 100644
a b 1 1 {% load i18n admin_static admin_modify %} 2 <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"> 2 <div class="inline-group{% if recursive_formset %} {{ recursive_formset.formset.prefix|default:"Root" }}-nested-inline nested-inline{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-group"> 3 {% with recursive_formset=inline_admin_formset stacked_template='admin/edit_inline/stacked.html' tabular_template='admin/edit_inline/tabular.html'%} 3 4 <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> 4 {{ inline_admin_formset.formset.management_form }}5 {{ recursive_formset.formset.management_form }} 5 6 <fieldset class="module"> 6 <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>7 {{ inline_admin_formset.formset.non_form_errors }}7 <h2>{{ recursive_formset.opts.verbose_name_plural|capfirst }}</h2> 8 {{ recursive_formset.formset.non_form_errors }} 8 9 <table> 9 10 <thead><tr> 10 {% for field in inline_admin_formset.fields %}11 {% for field in recursive_formset.fields %} 11 12 {% if not field.widget.is_hidden %} 12 13 <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }} 13 14 {% if field.help_text %} <img src="{% static "admin/img/icon-unknown.gif" %}" class="help help-tooltip" width="10" height="10" alt="({{ field.help_text|striptags }})" title="{{ field.help_text|striptags }}" />{% endif %} 14 15 </th> 15 16 {% endif %} 16 17 {% endfor %} 17 {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}18 {% if recursive_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %} 18 19 </tr></thead> 19 20 20 21 <tbody> 21 {% for inline_admin_form in inline_admin_formset %}22 {% for inline_admin_form in recursive_formset %} 22 23 {% if inline_admin_form.form.non_field_errors %} 23 24 <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr> 24 25 {% endif %} 25 <tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"26 id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">26 <tr class="form-row {% cycle "row1" "row2" as row_number_class %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %} {{ recursive_formset.formset.prefix }}-not-nested {% if inline_admin_form.form.nested_formsets %} no-bottom-border {% endif %}" 27 id="{{ recursive_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> 27 28 <td class="original"> 28 29 {% if inline_admin_form.original or inline_admin_form.show_url %}<p> 29 30 {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %} … … 55 56 {% endfor %} 56 57 {% endfor %} 57 58 {% endfor %} 58 {% if inline_admin_formset.formset.can_delete %}59 {% if recursive_formset.formset.can_delete %} 59 60 <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td> 60 61 {% endif %} 61 62 </tr> 63 {% if inline_admin_form.form.nested_formsets %} 64 {% for inline_admin_formset in inline_admin_form.form.nested_formsets %} 65 <tr class="nested-inline-row {{ row_number_class }}{% if not forloop.last %} no-bottom-border{% endif %}"> 66 <td colspan="0"> 67 {% if inline_admin_formset.opts.template == stacked_template %} 68 {% include stacked_template with indent=0 prev_prefix=recursive_formset.formset.prefix %} 69 {% else %} 70 {% include tabular_template with indent=0 prev_prefix=recursive_formset.formset.prefix %} 71 {% endif %} 72 </td> 73 </tr> 74 {% endfor %} 75 {% endif %} 62 76 {% endfor %} 63 77 </tbody> 64 78 </table> … … 67 81 </div> 68 82 69 83 <script type="text/javascript"> 70 71 84 (function($) { 72 $("#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr").tabularFormset({73 prefix: "{{ inline_admin_formset.formset.prefix }}",85 $("#{{ recursive_formset.formset.prefix }}-group .tabular.inline-related tbody tr.{{ recursive_formset.formset.prefix }}-not-nested").tabularFormset({ 86 prefix: "{{ recursive_formset.formset.prefix }}", 74 87 adminStaticPrefix: '{% static "admin/" %}', 75 addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",88 addText: "{% blocktrans with recursive_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}", 76 89 deleteText: "{% trans 'Remove' %}" 77 90 }); 78 91 })(django.jQuery); 79 92 </script> 93 {% endwith %} -
django/contrib/admin/tests.py
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index 7c62c1a..6fe6216 100644
a b from django.test import LiveServerTestCase 2 2 from django.utils.importlib import import_module 3 3 from django.utils.unittest import SkipTest 4 4 from django.utils.translation import ugettext as _ 5 from selenium import webdriver 5 6 6 7 class AdminSeleniumWebDriverTestCase(LiveServerTestCase): 7 8 webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver' … … class AdminSeleniumWebDriverTestCase(LiveServerTestCase): 13 14 module, attr = cls.webdriver_class.rsplit('.', 1) 14 15 mod = import_module(module) 15 16 WebDriver = getattr(mod, attr) 17 #Avoid startup screen 16 18 cls.selenium = WebDriver() 17 19 except Exception as e: 18 20 raise SkipTest('Selenium webdriver "%s" not installed or not ' -
docs/ref/contrib/admin/index.txt
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 06751df..b610616 100644
a b information. 1369 1369 1370 1370 The difference between these two is merely the template used to render 1371 1371 them. 1372 1373 .. versionadded:: 1.5 1374 1375 You can also display inlines inside an inline. Suppose you have a third model:: 1376 1377 class Chapter(models.Model): 1378 name = models.CharField(max_length=100) 1379 book = models.ForeignKey(Book) 1380 1381 These inlines work exactly the same as normal inlines, but they are specified 1382 inside a ``InlineModelAdmin.inlines``:: 1372 1383 1384 class BookInline(admin.TabularInline): 1385 model = Book 1386 inlines = [ 1387 ChapterInline, 1388 ] 1389 1390 class ChapterInline(admin.StackedInline): 1391 model = Chapter 1392 1373 1393 ``InlineModelAdmin`` options 1374 1394 ----------------------------- 1375 1395 … … adds some of its own (the shared features are actually defined in the 1399 1419 - :meth:`~ModelAdmin.has_change_permission` 1400 1420 - :meth:`~ModelAdmin.has_delete_permission` 1401 1421 1422 .. versionadded:: 1.5 1423 1424 - :meth:`~ModelAdmin.inlines` 1425 1402 1426 The ``InlineModelAdmin`` class adds: 1403 1427 1404 1428 .. attribute:: InlineModelAdmin.model -
tests/regressiontests/admin_inlines/admin.py
diff --git a/tests/regressiontests/admin_inlines/admin.py b/tests/regressiontests/admin_inlines/admin.py index cf51fa4..3f2d067 100644
a b class ChildModel1Inline(admin.TabularInline): 123 123 124 124 class ChildModel2Inline(admin.StackedInline): 125 125 model = ChildModel2 126 127 class FurnitureInline(admin.StackedInline): 128 model = Furniture 129 extra = 1 130 131 class InhabitantInline(admin.StackedInline): 132 model = Inhabitant 133 extra = 1 134 inlines = [ FurnitureInline, ] 135 136 class AppartementInline(admin.TabularInline): 137 model = Appartement 138 extra = 1 139 inlines = [ InhabitantInline, ] 140 141 class MonumentInline(admin.StackedInline): 142 model = Monument 143 extra = 1 144 145 class BuildingInline(admin.TabularInline): 146 model = Building 147 extra = 1 148 inlines = [ AppartementInline, ] 149 150 class CityInline(admin.StackedInline): 151 model = City 152 extra = 1 153 inlines = [BuildingInline, MonumentInline, ] 126 154 127 155 128 156 site.register(TitleCollection, inlines=[TitleInline]) … … site.register(Holder4, Holder4Admin) 141 169 site.register(Author, AuthorAdmin) 142 170 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline]) 143 171 site.register(ProfileCollection, inlines=[ProfileInline]) 144 site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) 145 No newline at end of file 172 site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) 173 site.register(Country, inlines=[CityInline]) 174 site.register(City) 175 site.register(Building) 176 site.register(Monument) 177 site.register(Appartement) 178 site.register(Inhabitant) 179 site.register(Furniture) 180 No newline at end of file -
tests/regressiontests/admin_inlines/models.py
diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py index b004d5f..c9c522c 100644
a b class Profile(models.Model): 180 180 collection = models.ForeignKey(ProfileCollection, blank=True, null=True) 181 181 first_name = models.CharField(max_length=100) 182 182 last_name = models.CharField(max_length=100) 183 184 class Country(models.Model): 185 name = models.CharField(max_length=100) 186 187 def __unicode__(self): 188 return self.name 189 190 class City(models.Model): 191 name = models.CharField(max_length=100) 192 population = models.IntegerField() 193 country = models.ForeignKey(Country) 194 195 def __unicode__(self): 196 return self.name 197 198 class Building(models.Model): 199 name = models.CharField(max_length=100) 200 city = models.ForeignKey(City) 201 202 def __unicode__(self): 203 return self.name 204 205 class Appartement(models.Model): 206 name = models.CharField(max_length=100) 207 building = models.ForeignKey(Building) 208 209 def __unicode__(self): 210 return self.name 211 212 class Inhabitant(models.Model): 213 name = models.CharField(max_length=100) 214 appartement = models.ForeignKey(Appartement) 215 216 def __unicode__(self): 217 return self.name 218 219 class Furniture(models.Model): 220 name = models.CharField(max_length=100) 221 inhabitant = models.ForeignKey(Inhabitant) 222 223 def __unicode__(self): 224 return self.name 225 226 class Monument(models.Model): 227 name = models.CharField(max_length=100) 228 city = models.ForeignKey(City) 229 230 def __unicode__(self): 231 return self.name 232 No newline at end of file -
tests/regressiontests/admin_inlines/tests.py
diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index 5bb6077..bc9c5d3 100644
a b from django.test import TestCase 8 8 from django.test.utils import override_settings 9 9 10 10 # local test models 11 from .admin import InnerInline , TitleInline, site11 from .admin import InnerInline 12 12 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, 13 13 OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, 14 ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, 15 Title)14 ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, 15 Country, City, Building, Appartement, Inhabitant, Furniture, Monument) 16 16 17 17 18 18 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) … … class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): 560 560 'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click() 561 561 self.selenium.find_element_by_css_selector( 562 562 'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click() 563 # Verify that they're gone and that the IDs have been re-sequenced563 # Verify that they're gone 564 564 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 565 565 '#profile_set-group table tr.dynamic-profile_set')), 3) 566 566 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 567 567 'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1) 568 568 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 569 'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)569 'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 0) 570 570 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 571 'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1) 571 'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 0) 572 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 573 'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1) 574 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 575 'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1) 572 576 573 577 def test_alternating_rows(self): 574 578 self.admin_login(username='super', password='secret') … … class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): 583 587 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 584 588 "%s.row1" % row_selector)), 2, msg="Expect two row1 styled rows") 585 589 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 586 "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row") 590 "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row") 587 591 592 def test_add_nested_inlines(self): 593 self.admin_login(username='super', password='secret') 594 self.selenium.get('%s%s' % (self.live_server_url, 595 '/admin/admin_inlines/country/add/')) 596 597 # Add some cities 598 self.selenium.find_element_by_link_text('Add another City').click() 599 self.selenium.find_element_by_link_text('Add another City').click() 600 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 601 "#city_set-1 .nested-inline-row .nested-inline-row #city_set-1-building_set-0-appartement_set-0-inhabitant_set-0 " + 602 "#city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0")), 1, "Expected furniture set in second city"); 603 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 604 "#city_set-2 .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 " + 605 "#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0")), 1, "Expected furniture set in third city"); 606 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 607 "#city_set-1 #city_set-1-monument_set-0")), 1, "Expected monument set in second city"); 608 # Add monument in first city 609 self.selenium.find_elements_by_css_selector('#city_set-0-monument_set-group > .add-row a')[0].click() 610 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 611 "#city_set-0 #city_set-0-monument_set-1")), 1, "Expected second monument in first city") 612 # Add building in second city 613 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-0 ~ .add-row a')[0].click() 614 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 615 "#city_set-1 #city_set-1-building_set-1")), 1, "Expected second building in second city"); 616 # Add apartement in second building of second city 617 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0 ~ .add-row a')[0].click() 618 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 619 "#city_set-1 .nested-inline-row #city_set-1-building_set-1-appartement_set-1")), 1, "Expected second appartement in second building of second city"); 620 # Add inhabitants in third city 621 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click() 622 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click() 623 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 624 "#city_set-2 .nested-inline-row .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-1")), 1, "Expected second inhabitant in third city"); 625 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 626 "#city_set-2 .nested-inline-row .nested-inline-row #city_set-2-building_set-0-appartement_set-0-inhabitant_set-2")), 1, "Expected third inhabitant in third city"); 627 # Add furniture in first city 628 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 629 self.assertEqual(len(self.selenium.find_elements_by_css_selector( 630 "#city_set-0 .nested-inline-row .nested-inline-row #city_set-0-building_set-0-appartement_set-0-inhabitant_set-0 "+ 631 "#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-1")), 1, "Expected second furniture in first city"); 632 633 def test_delete_nested_inlines(self): 634 self.admin_login(username='super', password='secret') 635 self.selenium.get('%s%s' % (self.live_server_url, 636 '/admin/admin_inlines/country/add/')) 637 638 # Add 2 cities 639 self.selenium.find_element_by_link_text('Add another City').click() 640 self.selenium.find_element_by_link_text('Add another City').click() 641 # Delete second city 642 self.selenium.find_elements_by_css_selector('#city_set-1 > h3 a.inline-deletelink')[0].click() 643 # Check if only two cities 644 self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set")), 2, "Expected 2 cities") 645 # Add 2 appartements in first city 646 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click() 647 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click() 648 # Delete second appartement 649 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1 > td.delete a')[0].click() 650 # Check if only two appartements in first city 651 self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-0-building_set-0-appartement_set")), 2, "Expected 2 Inhabitants") 652 # Check that nested inlines have also been deleted 653 self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-0-building_set-0-appartement_set")), 2, "Expected 2 Inhabitants") 654 # Add 4 furniture in second city 655 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 656 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 657 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 658 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 659 # Delete second and fourth 660 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-1 > h3 a.inline-deletelink')[0].click() 661 self.selenium.find_elements_by_css_selector('#city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-3 > h3 a.inline-deletelink')[0].click() 662 # Check if only 3 furniture 663 self.assertEqual(len(self.selenium.find_elements_by_css_selector(".dynamic-city_set-2-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set")), 3, "Expected 3 furniture") 664 665 def test_save_nested_inlines(self): 666 self.admin_login(username='super', password='secret') 667 self.selenium.get('%s%s' % (self.live_server_url, 668 '/admin/admin_inlines/country/add/')) 669 670 # Add City 671 self.selenium.find_element_by_link_text('Add another City').click() 672 # Add Buildings 673 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0 ~ .add-row a')[0].click() 674 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-0 ~ .add-row a')[0].click() 675 # Add Appartements 676 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0 ~ .add-row a')[0].click() 677 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0 ~ .add-row a')[0].click() 678 # Add Inhabitant 679 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click() 680 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0-inhabitant_set-0 ~ .add-row a')[0].click() 681 # Add Furniture 682 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 683 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-0 ~ .add-row a')[0].click() 684 # Add Monument 685 self.selenium.find_elements_by_css_selector('#city_set-0-monument_set-0 ~ .add-row a')[0].click() 686 self.selenium.find_elements_by_css_selector('#city_set-1-monument_set-0 ~ .add-row a')[0].click() 687 # Input Data 688 self.selenium.find_element_by_css_selector('#id_name').send_keys('Belgium') 689 self.selenium.find_element_by_css_selector('#id_city_set-0-name').send_keys('C 1') 690 self.selenium.find_element_by_css_selector('#id_city_set-0-population').send_keys('10') 691 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-name').send_keys('B 1.1') 692 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-name').send_keys('A 1.1.1') 693 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-name').send_keys('I 1.1.1.1') 694 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.1.1.1.1') 695 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-1-name').send_keys('I 1.1.1.2') 696 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-0-inhabitant_set-1-furniture_set-0-name').send_keys('F 1.1.1.2.1') 697 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-name').send_keys('A 1.1.2') 698 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-name').send_keys('I 1.1.2.1') 699 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.1.2.1.1') 700 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-1-name').send_keys('F 1.1.2.1.2') 701 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-name').send_keys('B 1.2') 702 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-name').send_keys('A 1.2.1') 703 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-inhabitant_set-0-name').send_keys('I 1.2.1.1') 704 self.selenium.find_element_by_css_selector('#id_city_set-0-building_set-1-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 1.2.1.1.1') 705 self.selenium.find_element_by_css_selector('#id_city_set-0-monument_set-0-name').send_keys('M 1.1') 706 self.selenium.find_element_by_css_selector('#id_city_set-0-monument_set-1-name').send_keys('M 1.2') 707 self.selenium.find_element_by_css_selector('#id_city_set-1-name').send_keys('C 2') 708 self.selenium.find_element_by_css_selector('#id_city_set-1-population').send_keys('10') 709 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-name').send_keys('B 2.1') 710 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-name').send_keys('A 2.1.1') 711 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-name').send_keys('I 2.1.1.1') 712 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-0-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.1.1.1.1') 713 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-name').send_keys('B 2.2') 714 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-name').send_keys('A 2.2.1') 715 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-0-name').send_keys('I 2.2.1.1') 716 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.2.1.1.1') 717 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-1-name').send_keys('I 2.2.1.2') 718 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-0-inhabitant_set-1-furniture_set-0-name').send_keys('F 2.2.1.2.1') 719 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-name').send_keys('A 2.2.2') 720 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-name').send_keys('I 2.2.2.1') 721 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-0-name').send_keys('F 2.2.2.1.1') 722 self.selenium.find_element_by_css_selector('#id_city_set-1-building_set-1-appartement_set-1-inhabitant_set-0-furniture_set-1-name').send_keys('F 2.2.2.1.2') 723 self.selenium.find_element_by_css_selector('#id_city_set-1-monument_set-0-name').send_keys('M 2.1') 724 self.selenium.find_element_by_css_selector('#id_city_set-1-monument_set-1-name').send_keys('M 2.2') 725 # Delete inhabitant 2.2.1.2 726 self.selenium.find_elements_by_css_selector('#city_set-1-building_set-1-appartement_set-0-inhabitant_set-1 > h3 a.inline-deletelink')[0].click() 727 # Delete furniture 1.1.2.1.2 728 self.selenium.find_elements_by_css_selector('#city_set-0-building_set-0-appartement_set-1-inhabitant_set-0-furniture_set-1 > h3 a.inline-deletelink')[0].click() 729 # Save 730 self.selenium.find_element_by_xpath('//input[@value="Save"]').click() 731 732 try: 733 # Wait for the next page to be loaded. 734 self.wait_loaded_tag('body') 735 except TimeoutException: 736 # IE7 occasionnally returns an error "Internet Explorer cannot 737 # display the webpage" and doesn't load the next page. We just 738 # ignore it. 739 pass 588 740 741 # Check if saved correctly 742 self.assertEqual(Country.objects.all().count(), 1) 743 self.assertEqual(City.objects.get(name="C 1").country, Country.objects.get(name="Belgium")) 744 self.assertEqual(Building.objects.get(name="B 1.1").city, City.objects.get(name="C 1")) 745 self.assertEqual(Building.objects.get(name="B 1.2").city, City.objects.get(name="C 1")) 746 self.assertEqual(Building.objects.get(name="B 2.1").city, City.objects.get(name="C 2")) 747 self.assertEqual(Building.objects.get(name="B 2.2").city, City.objects.get(name="C 2")) 748 self.assertEqual(Appartement.objects.get(name="A 1.1.1").building, Building.objects.get(name="B 1.1")) 749 self.assertEqual(Appartement.objects.get(name="A 1.1.2").building, Building.objects.get(name="B 1.1")) 750 self.assertEqual(Appartement.objects.get(name="A 1.2.1").building, Building.objects.get(name="B 1.2")) 751 self.assertEqual(Appartement.objects.get(name="A 2.1.1").building, Building.objects.get(name="B 2.1")) 752 self.assertEqual(Appartement.objects.get(name="A 2.2.1").building, Building.objects.get(name="B 2.2")) 753 self.assertEqual(Appartement.objects.get(name="A 2.2.2").building, Building.objects.get(name="B 2.2")) 754 self.assertEqual(Inhabitant.objects.get(name="I 1.1.1.1").appartement, Appartement.objects.get(name="A 1.1.1")) 755 self.assertEqual(Inhabitant.objects.get(name="I 1.1.1.2").appartement, Appartement.objects.get(name="A 1.1.1")) 756 self.assertEqual(Inhabitant.objects.get(name="I 1.1.2.1").appartement, Appartement.objects.get(name="A 1.1.2")) 757 self.assertEqual(Inhabitant.objects.get(name="I 1.2.1.1").appartement, Appartement.objects.get(name="A 1.2.1")) 758 self.assertEqual(Inhabitant.objects.get(name="I 2.1.1.1").appartement, Appartement.objects.get(name="A 2.1.1")) 759 self.assertEqual(Inhabitant.objects.get(name="I 2.2.1.1").appartement, Appartement.objects.get(name="A 2.2.1")) 760 self.assertEqual(len(Inhabitant.objects.filter(name="I 2.2.1.2")), 0) 761 self.assertEqual(Inhabitant.objects.get(name="I 2.2.2.1").appartement, Appartement.objects.get(name="A 2.2.2")) 762 self.assertEqual(Furniture.objects.get(name="F 1.1.1.1.1").inhabitant, Inhabitant.objects.get(name="I 1.1.1.1")) 763 self.assertEqual(Furniture.objects.get(name="F 1.1.1.2.1").inhabitant, Inhabitant.objects.get(name="I 1.1.1.2")) 764 self.assertEqual(Furniture.objects.get(name="F 1.1.2.1.1").inhabitant, Inhabitant.objects.get(name="I 1.1.2.1")) 765 self.assertEqual(len(Furniture.objects.filter(name="F 1.1.2.1.2")), 0) 766 self.assertEqual(Furniture.objects.get(name="F 1.2.1.1.1").inhabitant, Inhabitant.objects.get(name="I 1.2.1.1")) 767 self.assertEqual(Furniture.objects.get(name="F 2.1.1.1.1").inhabitant, Inhabitant.objects.get(name="I 2.1.1.1")) 768 self.assertEqual(Furniture.objects.get(name="F 2.2.1.1.1").inhabitant, Inhabitant.objects.get(name="I 2.2.1.1")) 769 self.assertEqual(len(Furniture.objects.filter(name="F 2.2.1.2.1")), 0) 770 self.assertEqual(Furniture.objects.get(name="F 2.2.2.1.1").inhabitant, Inhabitant.objects.get(name="I 2.2.2.1")) 771 self.assertEqual(Furniture.objects.get(name="F 2.2.2.1.2").inhabitant, Inhabitant.objects.get(name="I 2.2.2.1")) 772 self.assertEqual(Monument.objects.get(name="M 1.1").city, City.objects.get(name="C 1")) 773 self.assertEqual(Monument.objects.get(name="M 1.2").city, City.objects.get(name="C 1")) 774 self.assertEqual(Monument.objects.get(name="M 2.1").city, City.objects.get(name="C 2")) 775 self.assertEqual(Monument.objects.get(name="M 2.2").city, City.objects.get(name="C 2")) 776 589 777 class SeleniumChromeTests(SeleniumFirefoxTests): 590 778 webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' 591 779