From 5dee27e0771311b6cab964a52f079d41d0086b59 Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Wed, 23 Dec 2015 16:39:45 -0500 Subject: [PATCH] Added sorting icons. Refactored determination of sortability of classes referenced as sortable foreign keys in admin in a more reliable way. --- adminsortable/admin.py | 24 ++++++++---- .../adminsortable/css/admin.sortable.css | 9 +++++ .../css/admin.sortable.inline.css | 14 +++++++ .../static/adminsortable/js/admin.sortable.js | 23 +++++++++++- .../js/admin.sortable.stacked.inlines.js | 19 +++++++++- .../js/admin.sortable.tabular.inlines.js | 18 ++++++++- .../templates/adminsortable/change_form.html | 1 + .../templates/adminsortable/change_list.html | 5 ++- .../edit_inline/stacked-1.5.x.html | 2 +- .../adminsortable/edit_inline/stacked.html | 6 +-- .../edit_inline/tabular-1.5.x.html | 1 + .../adminsortable/edit_inline/tabular.html | 5 +++ .../adminsortable/shared/fieldset.html | 35 ++++++++++++++++++ .../adminsortable/shared/list_items.html | 2 +- .../adminsortable/shared/nested_objects.html | 4 +- .../adminsortable/shared/object_rep.html | 2 +- .../templatetags/adminsortable_tags.py | 4 +- adminsortable/utils.py | 5 ++- sample_project/app/admin.py | 2 +- sample_project/database/test_project.sqlite | Bin 86016 -> 89088 bytes 20 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 adminsortable/templates/adminsortable/shared/fieldset.html diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 031ac0b..2def272 100755 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -27,8 +27,9 @@ from django.http import HttpResponse from django.shortcuts import render from django.template.defaultfilters import capfirst -from adminsortable.utils import get_is_sortable, check_model_is_sortable +from adminsortable.fields import SortableForeignKey from adminsortable.models import SortableMixin +from adminsortable.utils import get_is_sortable, check_model_is_sortable STATIC_URL = settings.STATIC_URL @@ -145,9 +146,17 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): # Legacy support for 'sortable_by' defined as a model property sortable_by_property = getattr(self.model, 'sortable_by', None) - # `sortable_by` defined as a SortableForeignKey - sortable_by_fk = self.model.sortable_foreign_key - sortable_by_class_is_sortable = check_model_is_sortable(sortable_by_fk) + # see if our model is sortable by a SortableForeignKey field + # and that the number of objects available is >= 2 + sortable_by_fk = None + sortable_by_field_name = None + sortable_by_class_is_sortable = False + + for field in self.model._meta.fields: + if isinstance(field, SortableForeignKey): + sortable_by_fk = field.rel.to + sortable_by_field_name = field.name.lower() + sortable_by_class_is_sortable = sortable_by_fk.objects.count() >= 2 if sortable_by_property: # backwards compatibility for < 1.1.1, where sortable_by was a @@ -165,10 +174,9 @@ class SortableAdmin(SortableAdminBase, ModelAdmin): elif sortable_by_fk: # get sortable by properties from the SortableForeignKey # field - supported in 1.3+ - sortable_by_class_display_name = sortable_by_fk.rel.to \ - ._meta.verbose_name_plural - sortable_by_class = sortable_by_fk.rel.to - sortable_by_expression = sortable_by_fk.name.lower() + sortable_by_class_display_name = sortable_by_fk._meta.verbose_name_plural + sortable_by_class = sortable_by_fk + sortable_by_expression = sortable_by_field_name else: # model is not sortable by another model diff --git a/adminsortable/static/adminsortable/css/admin.sortable.css b/adminsortable/static/adminsortable/css/admin.sortable.css index 7af1b1f..e03ef81 100644 --- a/adminsortable/static/adminsortable/css/admin.sortable.css +++ b/adminsortable/static/adminsortable/css/admin.sortable.css @@ -42,7 +42,16 @@ margin-bottom: 0; } +.sortable-help-text { + color: #999; + font-size: 13px; +} + .sortable a:hover { color: #003366; } + +.sortable .fa { + margin-right: 7px; +} diff --git a/adminsortable/static/adminsortable/css/admin.sortable.inline.css b/adminsortable/static/adminsortable/css/admin.sortable.inline.css index 44f7e70..cbde4e8 100644 --- a/adminsortable/static/adminsortable/css/admin.sortable.inline.css +++ b/adminsortable/static/adminsortable/css/admin.sortable.inline.css @@ -1,3 +1,17 @@ .sortable.has_original { cursor: move; } + +.sortable .inline-related .module.aligned .fa, +.sortable.inline-group .module .fa { + display: block; + float: left; +} + +.sortable .inline-related .module.aligned .fa { + margin: 9px 10px 0 0; +} + +.sortable.inline-group .module .fa { + margin: 34px -10px 0 10px; +} diff --git a/adminsortable/static/adminsortable/js/admin.sortable.js b/adminsortable/static/adminsortable/js/admin.sortable.js index b854f52..cca28cb 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.js @@ -8,17 +8,36 @@ items : 'li', stop : function(event, ui) { - var indexes = []; - ui.item.parent().children('li').each(function(i) + var indexes = [], + lineItems = ui.item.parent().find('> li'); + + lineItems.each(function(i) { indexes.push($(this).find(':hidden[name="pk"]').val()); }); + $.ajax({ url: ui.item.find('a.admin_sorting_url').attr('href'), type: 'POST', data: { indexes: indexes.join(',') }, success: function() { + // set icons based on position + lineItems.each(function(index, element) { + var icon = $(element).find('> a .fa'); + icon.removeClass('fa-sort-desc fa-sort-asc fa-sort'); + + if (index === 0) { + icon.addClass('fa fa-sort-desc'); + } + else if (index == lineItems.length - 1) { + icon.addClass('fa fa-sort-asc'); + } + else { + icon.addClass('fa fa-sort'); + } + }); + ui.item.effect('highlight', {}, 1000); } }); diff --git a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js b/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js index 3b32b7e..4afbea3 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js @@ -41,7 +41,24 @@ data: { indexes : indexes.join(',') }, success: function() { var fieldsets = ui.item.find('fieldset'), - highlightedSelector = fieldsets.filter('.collapsed').length === fieldsets.length ? 'h3' : '.form-row'; + highlightedSelector = fieldsets.filter('.collapsed').length === fieldsets.length ? 'h3' : '.form-row', + icons = ui.item.parent().find(highlightedSelector).find('.fa'); + + // set icons based on position + icons.removeClass('fa-sort-desc fa-sort-asc fa-sort'); + icons.each(function(index, element) { + var icon = $(element); + if (index === 0) { + icon.addClass('fa fa-sort-desc'); + } + else if (index == icons.length - 1) { + icon.addClass('fa fa-sort-asc'); + } + else { + icon.addClass('fa fa-sort'); + } + }); + ui.item.find(highlightedSelector).effect('highlight', {}, 1000); } }); diff --git a/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js b/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js index 44c9fe8..debd654 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js @@ -38,7 +38,23 @@ type: 'POST', data: { indexes : indexes.join(',') }, success: function() { - //highlight sorted row, then re-stripe table + // set icons based on position + var icons = ui.item.parent().find('.fa'); + icons.removeClass('fa-sort-desc fa-sort-asc fa-sort'); + icons.each(function(index, element) { + var icon = $(element); + if (index === 0) { + icon.addClass('fa fa-sort-desc'); + } + else if (index == icons.length - 1) { + icon.addClass('fa fa-sort-asc'); + } + else { + icon.addClass('fa fa-sort'); + } + }); + + // highlight sorted row, then re-stripe table ui.item.effect('highlight', {}, 1000); tabular_inline_rows.removeClass('row1 row2'); $('.tabular table tbody tr:odd').addClass('row2'); diff --git a/adminsortable/templates/adminsortable/change_form.html b/adminsortable/templates/adminsortable/change_form.html index 3a4e922..0f3fd76 100644 --- a/adminsortable/templates/adminsortable/change_form.html +++ b/adminsortable/templates/adminsortable/change_form.html @@ -24,6 +24,7 @@ {{ block.super }} {% if has_sortable_tabular_inlines or has_sortable_stacked_inlines %} + {% endif %} {% endblock %} diff --git a/adminsortable/templates/adminsortable/change_list.html b/adminsortable/templates/adminsortable/change_list.html index f8dfbc9..7b6e83e 100644 --- a/adminsortable/templates/adminsortable/change_list.html +++ b/adminsortable/templates/adminsortable/change_list.html @@ -4,6 +4,7 @@ {% block extrastyle %} {{ block.super }} + {% endblock %} @@ -40,8 +41,8 @@ {% blocktrans with opts.verbose_name_plural|capfirst as model %}Drag and drop {{ model }} to change their order.{% endblocktrans %} {% endif %} - {% if sortable_by_class.is_sortable %} -

+ {% if sortable_by_class_is_sortable %} +

{% blocktrans %}You may also drag and drop {{ sortable_by_class_display_name }} to change their order.{% endblocktrans %}

{% endif %} diff --git a/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html b/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html index bc7ce69..c133fe6 100644 --- a/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html +++ b/adminsortable/templates/adminsortable/edit_inline/stacked-1.5.x.html @@ -12,7 +12,7 @@ {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} {% for fieldset in inline_admin_form %} - {% include "admin/includes/fieldset.html" %} + {% include "admin/includes/fieldset.html" with inline_admin_form_forloop=forloop.parentloop %} {% endfor %} {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {{ inline_admin_form.fk_field.field }} diff --git a/adminsortable/templates/adminsortable/edit_inline/stacked.html b/adminsortable/templates/adminsortable/edit_inline/stacked.html index 0bf7d7b..ff45712 100644 --- a/adminsortable/templates/adminsortable/edit_inline/stacked.html +++ b/adminsortable/templates/adminsortable/edit_inline/stacked.html @@ -12,13 +12,13 @@ {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} {% for fieldset in inline_admin_form %} - {% include "admin/includes/fieldset.html" %} + {% include "adminsortable/shared/fieldset.html" with inline_admin_form_forloop=forloop.parentloop initial_forms_count=inline_admin_formset.formset.management_form.initial.INITIAL_FORMS %} {% endfor %} - {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} - {{ inline_admin_form.fk_field.field }} {% if inline_admin_form.original %} {% endif %} + {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} + {{ inline_admin_form.fk_field.field }} {% endfor %} diff --git a/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html b/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html index 34555ee..30f1772 100644 --- a/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html +++ b/adminsortable/templates/adminsortable/edit_inline/tabular-1.5.x.html @@ -48,6 +48,7 @@ {% for line in fieldset %} {% for field in line %} + {% if inline_admin_form.original and forloop.parentloop.counter == 1 %}{% endif %} {% if field.is_readonly %}

{{ field.contents }}

{% else %} diff --git a/adminsortable/templates/adminsortable/edit_inline/tabular.html b/adminsortable/templates/adminsortable/edit_inline/tabular.html index 247818e..9554f0d 100644 --- a/adminsortable/templates/adminsortable/edit_inline/tabular.html +++ b/adminsortable/templates/adminsortable/edit_inline/tabular.html @@ -25,6 +25,11 @@ + {% with initial_forms=inline_admin_form.formset.management_form.initial.INITIAL_FORMS %} + {% if forloop.counter <= initial_forms %} + + {% endif %} + {% endwith %} {% if inline_admin_form.original or inline_admin_form.show_url %}

{% if inline_admin_form.original %} {{ inline_admin_form.original }} diff --git a/adminsortable/templates/adminsortable/shared/fieldset.html b/adminsortable/templates/adminsortable/shared/fieldset.html new file mode 100644 index 0000000..89659ae --- /dev/null +++ b/adminsortable/templates/adminsortable/shared/fieldset.html @@ -0,0 +1,35 @@ +{# overrides admin/includes/fieldset.html: https://github.com/django/django/blob/master/django/contrib/admin/templates/admin/includes/fieldset.html #} +

+ {% if fieldset.name %}

{{ fieldset.name }}

{% endif %} + {% if fieldset.description %} +
{{ fieldset.description|safe }}
+ {% endif %} + {% for line in fieldset %} +
+ {% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %} + {% for field in line %} + + {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %} + {% if forloop.first %} + {% if inline_admin_form_forloop.counter <= initial_forms_count %} + + {% endif %} + {% endif %} + {% if field.is_checkbox %} + {{ field.field }}{{ field.label_tag }} + {% else %} + {{ field.label_tag }} + {% if field.is_readonly %} +

{{ field.contents }}

+ {% else %} + {{ field.field }} + {% endif %} + {% endif %} + {% if field.field.help_text %} +

{{ field.field.help_text|safe }}

+ {% endif %} +
+ {% endfor %} + + {% endfor %} +
diff --git a/adminsortable/templates/adminsortable/shared/list_items.html b/adminsortable/templates/adminsortable/shared/list_items.html index bfc21a6..29df030 100644 --- a/adminsortable/templates/adminsortable/shared/list_items.html +++ b/adminsortable/templates/adminsortable/shared/list_items.html @@ -3,7 +3,7 @@ {% for object in list_objects %}
  • {% if list_objects_length > 1 %} - {% render_object_rep object %} + {% render_object_rep object forloop %} {% else %} {{ object }} {% endif %} diff --git a/adminsortable/templates/adminsortable/shared/nested_objects.html b/adminsortable/templates/adminsortable/shared/nested_objects.html index 6774a60..8ef74d9 100644 --- a/adminsortable/templates/adminsortable/shared/nested_objects.html +++ b/adminsortable/templates/adminsortable/shared/nested_objects.html @@ -5,8 +5,8 @@ {% for regrouped_object in regrouped_objects %} {% with object=regrouped_object.grouper %} {% if object %} -
  • {% if sortable_by_class_is_sortable %} - {% render_object_rep object %} +
  • {% if sortable_by_class_is_sortable %} + {% render_object_rep object forloop %} {% else %} {{ object }} {% endif %} diff --git a/adminsortable/templates/adminsortable/shared/object_rep.html b/adminsortable/templates/adminsortable/shared/object_rep.html index 05dfb16..ffe9edf 100644 --- a/adminsortable/templates/adminsortable/shared/object_rep.html +++ b/adminsortable/templates/adminsortable/shared/object_rep.html @@ -3,4 +3,4 @@
    -{{ object }} + {{ object }} diff --git a/adminsortable/templatetags/adminsortable_tags.py b/adminsortable/templatetags/adminsortable_tags.py index aae2b6c..bbed9a0 100644 --- a/adminsortable/templatetags/adminsortable_tags.py +++ b/adminsortable/templatetags/adminsortable_tags.py @@ -28,8 +28,8 @@ def render_list_items(context, list_objects, @register.simple_tag(takes_context=True) -def render_object_rep(context, obj, +def render_object_rep(context, obj, forloop, sortable_object_rep_template='adminsortable/shared/object_rep.html'): - context.update({'object': obj}) + context.update({'object': obj, 'forloop': forloop}) tmpl = template.loader.get_template(sortable_object_rep_template) return tmpl.render(context) diff --git a/adminsortable/utils.py b/adminsortable/utils.py index 9848274..890e504 100644 --- a/adminsortable/utils.py +++ b/adminsortable/utils.py @@ -1,8 +1,9 @@ from .models import SortableMixin, SortableForeignKey -def check_inheritance(obj): - return issubclass(type(obj), SortableMixin) +def check_inheritance(cls): + print 'check_inheritance: {}'.format(issubclass(type(cls), SortableMixin)) + return issubclass(type(cls), SortableMixin) def get_is_sortable(objects): diff --git a/sample_project/app/admin.py b/sample_project/app/admin.py index a029d62..cba42f8 100644 --- a/sample_project/app/admin.py +++ b/sample_project/app/admin.py @@ -59,7 +59,7 @@ class CreditInline(SortableTabularInline): class NoteInline(SortableStackedInline): model = Note - extra = 0 + extra = 2 class GenericNoteInline(SortableGenericStackedInline): diff --git a/sample_project/database/test_project.sqlite b/sample_project/database/test_project.sqlite index 6f5d859175e3df079cfed0d66f6b3c3df7833050..84ca6166ee6bb08e5bfaef66ab4b08462df6ed64 100644 GIT binary patch delta 4161 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