Skip to content

Instantly share code, notes, and snippets.

@hoheinzollern
Created October 4, 2011 07:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hoheinzollern/1261073 to your computer and use it in GitHub Desktop.
Save hoheinzollern/1261073 to your computer and use it in GitHub Desktop.
Reordering of tabular inlines in Django administration website.
from django.contrib import admin
class SlaveInline(admin.TabularInline):
model = Slave
extra = 0
class MasterAdmin(admin.ModelAdmin):
inlines = (SlaveInline,)
class Media:
js = ('https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js',
'https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js',
'/media/script/dynamic_inlines_with_sort.js',)
css = { 'all' : ['css/dynamic_inlines_with_sort.css'], }
admin.site.register(Master, MasterAdmin)
/* To make row height of saved items same as others */
.inline-group .tabular tr.has_original td { padding-top:0.5em; }
.inline-group .tabular tr.has_original td.original p { display:none; }
/* dynamic_inlines_with_sort.js */
/* Created in May 2009 by Hannes Rydén */
/* Modified in September 2011 by Alessandro Bruni */
/* Use, distribute and modify freely */
// "Add"-link html code. Defaults to Django's "+" image icon, but could use text instead.
add_link_html = '<img src="/media/img/admin/icon_addlink.gif" ' +
'width="10" height="10" alt="Add new row" style="margin:0.5em 1em;" />';
// "Delete"-link html code. Defaults to Django's "x" image icon, but could use text instead.
delete_link_html = '<img src="/media/img/admin/icon_deletelink.gif" ' +
'width="10" height="10" alt="Delete row" style="margin-top:0.5em" />';
position_field = 'order'; // Name of inline model field (integer) used for ordering. Defaults to "position".
jQuery(function($) {
// This script is applied to all TABULAR inlines
$('div.inline-group div.tabular').each(function() {
table = $(this).find('table');
// Hide initial extra row and prepare it to be used as a template for new rows
add_template = table.find('tr.empty-form');
add_template.addClass('add_template').hide();
table.prepend(add_template);
// Remove original add button
table.find('tr.add-row').remove();
// Hide initial deleted rows
table.find('td.delete input:checkbox:checked').parent('td').parent('tr').addClass('deleted_row').hide();
// "Add"-button in bottom of inline for adding new rows
$(this).find('fieldset').after('<a class="add" href="#">' + add_link_html + '</a>');
$(this).find('a.add').click(function(){
new_item = add_template.clone(true);
create_delete_button(new_item.find('td.delete'));
new_item.removeClass('add_template').removeClass('empty-form').show();
$(this).parent().find('table').append(new_item);
update_positions($(this).parent().find('table'), true);
// Place for special code to re-enable javascript widgets after clone (e.g. an ajax-autocomplete field)
// Fictive example: new_item.find('.autocomplete').each(function() { $(this).triggerHandler('autocomplete'); });
}).removeAttr('href').css('cursor', 'pointer');
// "Delete"-buttons for each row that replaces the default checkbox
table.find('tr:not(.add_template) td.delete').each(function() {
create_delete_button($(this));
});
// Drag and drop functionality - only used if a position field exists
if (position_field != '' && table.find('td').is('.' + position_field))
{
// Hide "position"-field (both td:s and th:s)
$(this).find('td.' + position_field).hide();
td_pos_field_index = table.find('tbody tr td').index($(this).find('td.' + position_field));
$(this).find('th:eq(' + (td_pos_field_index-1) + ')').hide();
// Hide "original"-field and set any colspan to 1 (why show in the first case?)
$(this).find('td.original').hide();
$(this).find('th[colspan]').removeAttr('colspan');
// Make table sortable using jQuery UI Sortable
table.sortable({
items: 'tr:has(td)',
tolerance: 'pointer',
axis: 'y',
cancel: 'input,button,select,a',
helper: 'clone',
update: function() {
update_positions($(this));
}
});
// Re-order <tr>:s based on the "position"-field values.
// This is a very simple ordering which only works with correct position number sequences,
// which the rest of this script (hopefully) guarantees.
rows = [];
table.find('tbody tr').each(function() {
position = $(this).find('td.' + position_field + ' input').val();
rows[position] = $(this);
// Add move cursor to table row.
// Also remove row coloring, as it confuses when using drag-and-drop for ordering
table.find('tr:has(td)').css('cursor', 'move').removeClass('row1').removeClass('row2');
});
for (var i in rows) { table.append(rows[i]); } // Move <tr> to its correct position
update_positions($(this), true);
}
else
position_field = '';
// Detach the template row
add_template.detach();
});
});
// Function for creating fancy delete buttons
function create_delete_button(td)
{
// Replace checkbox with image
td.find('input:checkbox').hide();
td.append('<a class="delete" href="#">' + delete_link_html + '</a>');
td.find('a.delete').click(function(){
current_row = $(this).parent('td').parent('tr');
table = current_row.parent().parent();
if (current_row.is('.has_original')) // This row has already been saved once, so we must keep checkbox
{
$(this).prev('input').attr('checked', true);
current_row.addClass('deleted_row').hide();
}
else // This row has never been saved so we can just remove the element completely
{
current_row.remove();
}
update_positions(table, true);
}).removeAttr('href').css('cursor', 'pointer');
}
// Updates "position"-field values based on row order in table
function update_positions(table, update_ids)
{
even = true;
num_rows = 0
position = 0;
// Set correct position: Filter through all trs, excluding first th tr and last hidden template tr
table.find('tbody tr:not(.add_template):not(.deleted_row)').each(function() {
if (position_field != '')
{
// Update position field
$(this).find('td.' + position_field + ' input').val(position + 1);
position++;
}
else
{
// Update row coloring
$(this).removeClass('row1 row2');
if (even)
{
$(this).addClass('row1');
even = false;
}
else
{
$(this).addClass('row2');
even = true;
}
}
});
table.find('tbody tr.has_original').each(function() {
num_rows++;
});
table.find('tbody tr:not(.has_original):not(.add_template)').each(function() {
if (update_ids) update_id_fields($(this), num_rows);
num_rows++;
});
table.find('tbody tr.add_template').each(function() {
if (update_ids) update_id_fields($(this), num_rows);
num_rows++;
});
table.parent().parent('div.tabular').find("input[id$='TOTAL_FORMS']").val(num_rows);
}
// Updates actual id and name attributes of inputs, selects and so on.
// Required for Django validation to keep row order.
function update_id_fields(row, new_position)
{
// Fix IDs, names etc.
// <select ...>
row.find('select').each(function() {
// id=...
old_id = $(this).attr('id').toString();
new_id = old_id.replace(/([^ ]+\-)(?:\d+|__\w+__)(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('id', new_id);
// name=...
old_id = $(this).attr('name').toString();
new_id = old_id.replace(/([^ ]+\-)(?:\d+|__\w+__)(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('name', new_id);
});
// <input ...>
row.find('input').each(function() {
// id=...
old_id = $(this).attr('id').toString();
new_id = old_id.replace(/([^ ]+\-)(?:\d+|__\w+__)(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('id', new_id);
// name=...
old_id = $(this).attr('name').toString();
new_id = old_id.replace(/([^ ]+\-)(?:\d+|__\w+__)(\-[^ ]+)/i, "$1" + new_position + "$2");
$(this).attr('name', new_id);
});
// Are there other element types...? Add here.
}
from django.db import models
class Master(models.Model):
pass
class Slave(models.Model):
master = models.ForeignKey(Master)
order = models.PositiveIntegerField()
@ritiksoni00
Copy link

unfortunately, this is not working

i tried to open a ticket for it and they totally refuse it. https://code.djangoproject.com/ticket/33776

@hoheinzollern
Copy link
Author

I don't even remember what I did with this code 11 years ago, please don't use it, or use it at your own risk.

@ritiksoni00
Copy link

        const addInlineAddButton = function () {
            if (addButton === null) {
                if ($this.prop("tagName") === "TR") {
                    // If forms are laid out as table rows, insert the
                    // "add" button in a new table row:
                    // not using this numcol becuase im replacing tabular add-row tr to a div
                    // const numCols = $this.eq(-1).children().length;
                    ($parent).parent().parent().find('h2').after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");  <--------------from this line
                    addButton = ($parent).parent().parent().find("div.add-row a");
                } else {
                    // Otherwise, insert it immediately after the last form:
                    // console.log($this.parent().find("h2"));
                    $this.parent().find("h2").after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");
                    addButton = $this.parent().find("h2").next();
                }
            }
            addButton.on('click', addInlineClickHandler);
        };

        const addInlineClickHandler = function (e) {
            e.preventDefault();
            const template = $("#" + options.prefix + "-empty");
            const row = template.clone(true);
            row.removeClass(options.emptyCssClass)
                .addClass(options.formCssClass)
                .attr("id", options.prefix + "-" + nextIndex);
            addInlineDeleteButton(row);
            row.find("*").each(function () {
                updateElementIndex(this, options.prefix, totalForms.val());
            });
            // if tabular else stacked 
            if (String($(template).parent()[0].tagName) === 'TBODY') { <-----------this if block
                row.prependTo($(template).parent());
            } else {
                $(template).parent().find('> input:last').after(row);
            }
            // Update number of total forms.
            $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
            nextIndex += 1;
            // Hide the add button if there's a limit and it's been reached.
            if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
                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.
            if (options.added) {
                options.added(row);
            }
            $(document).trigger('formset:added', [row, options.prefix]);
        };

Hy @hoheinzollern,

I customized inlines.js for moving the button at the top of inline fieldsets
and now a new inline form will come at starting not ending.

is it a good solution for https://stackoverflow.com/questions/66409702/how-to-move-add-another-link-button-inline-to-the-top-django-admin

is there any error in this code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment