From 014f6d16603433ecb53a92d81aad2034b6c3eaa7 Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Sat, 27 Apr 2013 22:58:02 -0400 Subject: [PATCH] Added support for queryset() overrides on admin classes and inline admin classes. Updated version to 1.4.5. Updated README with explanation of requirements for overriding queryset() on inline models. Added extra models to sample project to demonstrate sortable models with custom querysets. Improved JavaScript of sortables to be more efficient with better comparison checking. Fixed highlighting of stacked inlines on sort finish. --- README.md | 48 ++++++- adminsortable/__init__.py | 2 +- adminsortable/admin.py | 86 +++++++----- adminsortable/models.py | 25 ++-- .../static/adminsortable/js/admin.sortable.js | 4 +- .../js/admin.sortable.stacked.inlines.js | 12 +- .../js/admin.sortable.tabular.inlines.js | 10 +- adminsortable/utils.py | 4 + sample_project/app/admin.py | 32 ++++- sample_project/app/migrations/0001_initial.py | 123 ++++++++++++++++++ .../app/migrations/0002_add_widget.py | 78 +++++++++++ .../app/migrations/0003_add_component.py | 86 ++++++++++++ sample_project/app/migrations/__init__.py | 0 sample_project/app/models.py | 32 ++++- sample_project/database/test_project.sqlite | Bin 56320 -> 67584 bytes sample_project/sample_project/settings.py | 3 +- 16 files changed, 477 insertions(+), 68 deletions(-) create mode 100644 adminsortable/utils.py create mode 100644 sample_project/app/migrations/0001_initial.py create mode 100644 sample_project/app/migrations/0002_add_widget.py create mode 100644 sample_project/app/migrations/0003_add_component.py create mode 100644 sample_project/app/migrations/__init__.py diff --git a/README.md b/README.md index fb59ca8..1f526cb 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,47 @@ There are also generic equivalents that you can inherit from: """Your generic inline options go here""" +### Overriding `queryset()` +django-admin-sortable now supports custom queryset overrides on admin models +and inline models in Django admin! + +If you're providing an override of a SortableAdmin or Sortable inline model, +you don't need to do anything extra. django-admin-sortable will automatically +honor your queryset. + +Have a look at the WidgetAdmin class in the sample project for an example of +an admin class with a custom `queryset()` override. + +#### Overriding `queryset()` for an inline model +This is a special case, which requires a few lines of extra code to properly +determine the sortability of your model. Example: + + # add this import to your admin.py + from adminsortable.utils import get_is_sortable + + + class ComponentInline(SortableStackedInline): + model = Component + + def queryset(self, request): + qs = super(ComponentInline, self).queryset(request).filter( + title__icontains='foo') + + # You'll need to add these lines to determine if your model + # is sortable once we hit the change_form() for the parent model. + + if get_is_sortable(qs): + self.model.is_sortable = True + else: + self.model.is_sortable = False + return qs + +If you override the queryset of an inline, the number of objects present +may change, and adminsortable won't be able to automatically determine +if the inline model is sortable from here, which is why we have to set the +`is_sortable` property of the model in this method. + + *** IMPORTANT *** With stacked inline models, their height can dynamically increase, which can cause sortable stacked inlines to not behave as expected. @@ -162,9 +203,10 @@ ordering on top of that just seemed a little much in my opinion. django-admin-sortable is currently used in production. -### What's new in 1.4.4? -- Decided to go with the simplest approach to add the sorting urls to inlines -for Django <= 1.4 and Django 1.5.x support +### What's new in 1.4.5? +- Support for queryset overrides! +- More efficient JavaScript in sortables +- Fixed highlight effect for stacked inlines on sort finish ### Future diff --git a/adminsortable/__init__.py b/adminsortable/__init__.py index 07cf343..a5df678 100755 --- a/adminsortable/__init__.py +++ b/adminsortable/__init__.py @@ -1,4 +1,4 @@ -VERSION = (1, 4, 4) # following PEP 386 +VERSION = (1, 4, 5) # following PEP 386 DEV_N = None diff --git a/adminsortable/admin.py b/adminsortable/admin.py index 6913d90..032cfb0 100644 --- a/adminsortable/admin.py +++ b/adminsortable/admin.py @@ -21,13 +21,30 @@ from django.shortcuts import render from django.template.defaultfilters import capfirst from django.views.decorators.csrf import csrf_exempt +from adminsortable.utils import get_is_sortable from adminsortable.fields import SortableForeignKey from adminsortable.models import Sortable STATIC_URL = settings.STATIC_URL -class SortableAdmin(ModelAdmin): +class SortableAdminBase(object): + def changelist_view(self, request, extra_context=None): + """ + If the model that inherits Sortable has more than one object, + its sort order can be changed. This view adds a link to the + object_tools block to take people to the view to change the sorting. + """ + + if get_is_sortable(self.queryset(request)): + self.change_list_template = \ + self.sortable_change_list_with_sort_link_template + self.is_sortable = True + return super(SortableAdminBase, self).changelist_view(request, + extra_context=extra_context) + + +class SortableAdmin(SortableAdminBase, ModelAdmin): """ Admin class to add template overrides and context objects to enable drag-and-drop ordering. @@ -52,18 +69,20 @@ class SortableAdmin(ModelAdmin): break return sortable_foreign_key - def __init__(self, *args, **kwargs): - super(SortableAdmin, self).__init__(*args, **kwargs) + # def __init__(self, *args, **kwargs): + # super(SortableAdmin, self).__init__(*args, **kwargs) - self.has_sortable_tabular_inlines = False - self.has_sortable_stacked_inlines = False - for klass in self.inlines: - if issubclass(klass, SortableTabularInline): - if klass.model.is_sortable(): - self.has_sortable_tabular_inlines = True - if issubclass(klass, SortableStackedInline): - if klass.model.is_sortable(): - self.has_sortable_stacked_inlines = True + # self.has_sortable_tabular_inlines = False + # self.has_sortable_stacked_inlines = False + # for klass in self.inlines: + # print type(klass) + # is_sortable = get_is_sortable( + # klass.model._default_manager.get_query_set()) + # print is_sortable + # if issubclass(klass, SortableTabularInline) and is_sortable: + # self.has_sortable_tabular_inlines = True + # if issubclass(klass, SortableStackedInline) and is_sortable: + # self.has_sortable_stacked_inlines = True def get_urls(self): urls = super(SortableAdmin, self).get_urls() @@ -88,7 +107,8 @@ class SortableAdmin(ModelAdmin): opts = self.model._meta has_perm = request.user.has_perm('{0}.{1}'.format(opts.app_label, opts.get_change_permission())) - objects = self.model.objects.all() + + objects = self.queryset(request) # Determine if we need to regroup objects relative to a # foreign key specified on the model class that is extending Sortable. @@ -97,6 +117,7 @@ class SortableAdmin(ModelAdmin): # `sortable_by` defined as a SortableForeignKey sortable_by_fk = self._get_sortable_foreign_key() + sortable_by_class_is_sortable = get_is_sortable(objects) if sortable_by_property: # backwards compatibility for < 1.1.1, where sortable_by was a @@ -110,7 +131,6 @@ class SortableAdmin(ModelAdmin): sortable_by_class_display_name = sortable_by_class._meta \ .verbose_name_plural - sortable_by_class_is_sortable = sortable_by_class.is_sortable() elif sortable_by_fk: # get sortable by properties from the SortableForeignKey @@ -119,10 +139,6 @@ class SortableAdmin(ModelAdmin): ._meta.verbose_name_plural sortable_by_class = sortable_by_fk.rel.to sortable_by_expression = sortable_by_fk.name.lower() - try: - sortable_by_class_is_sortable = sortable_by_class.is_sortable() - except AttributeError: - sortable_by_class_is_sortable = False else: # model is not sortable by another model @@ -132,7 +148,7 @@ class SortableAdmin(ModelAdmin): if sortable_by_property or sortable_by_fk: # Order the objects by the property they are sortable by, - #then by the order, otherwise the regroup + # then by the order, otherwise the regroup # template tag will not show the objects correctly objects = objects.order_by(sortable_by_expression, 'order') @@ -157,19 +173,17 @@ class SortableAdmin(ModelAdmin): } return render(request, self.sortable_change_list_template, context) - def changelist_view(self, request, extra_context=None): - """ - If the model that inherits Sortable has more than one object, - its sort order can be changed. This view adds a link to the - object_tools block to take people to the view to change the sorting. - """ - if self.model.is_sortable(): - self.change_list_template = \ - self.sortable_change_list_with_sort_link_template - return super(SortableAdmin, self).changelist_view(request, - extra_context=extra_context) - def change_view(self, request, object_id, extra_context=None): + self.has_sortable_tabular_inlines = False + self.has_sortable_stacked_inlines = False + + for klass in self.inlines: + is_sortable = klass.model.is_sortable + if issubclass(klass, SortableTabularInline) and is_sortable: + self.has_sortable_tabular_inlines = True + if issubclass(klass, SortableStackedInline) and is_sortable: + self.has_sortable_stacked_inlines = True + if self.has_sortable_tabular_inlines or \ self.has_sortable_stacked_inlines: self.change_form_template = self.sortable_change_form_template @@ -222,7 +236,7 @@ class SortableAdmin(ModelAdmin): mimetype='application/json') -class SortableInlineBase(InlineModelAdmin): +class SortableInlineBase(SortableAdminBase, InlineModelAdmin): def __init__(self, *args, **kwargs): super(SortableInlineBase, self).__init__(*args, **kwargs) @@ -230,7 +244,13 @@ class SortableInlineBase(InlineModelAdmin): raise Warning(u'Models that are specified in SortableTabluarInline' ' and SortableStackedInline must inherit from Sortable') - self.is_sortable = self.model.is_sortable() + def queryset(self, request): + qs = super(SortableInlineBase, self).queryset(request) + if get_is_sortable(qs): + self.model.is_sortable = True + else: + self.model.is_sortable = False + return qs class SortableTabularInline(SortableInlineBase, TabularInline): diff --git a/adminsortable/models.py b/adminsortable/models.py index 26cb682..f7640be 100644 --- a/adminsortable/models.py +++ b/adminsortable/models.py @@ -26,14 +26,11 @@ class Sortable(models.Model): `save` the override of save increments the last/highest value of order by 1 - - Override `sortable_by` method to make your model be sortable by a - foreign key field. Set `sortable_by` to the class specified in the - foreign key relationship. """ order = models.PositiveIntegerField(editable=False, default=1, db_index=True) + is_sortable = False # legacy support sortable_by = None @@ -42,14 +39,18 @@ class Sortable(models.Model): abstract = True ordering = ['order'] - @classmethod - def is_sortable(cls): - try: - max_order = cls.objects.aggregate( - models.Max('order'))['order__max'] - except (TypeError, IndexError): - max_order = 0 - return True if max_order > 1 else False + # @classmethod + # def determine_if_sortable(cls): + # try: + # max_order = cls.objects.aggregate( + # models.Max('order'))['order__max'] + # except (TypeError, IndexError): + # max_order = 0 + + # if max_order > 1: + # cls.is_sortable = True + # else: + # cls.is_sortable = False @classmethod def model_type_id(cls): diff --git a/adminsortable/static/adminsortable/js/admin.sortable.js b/adminsortable/static/adminsortable/js/admin.sortable.js index 0374ded..3f1a449 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.js @@ -6,7 +6,7 @@ jQuery(function($){ items : 'li', stop : function(event, ui) { - var indexes = Array(); + var indexes = []; ui.item.parent().children('li').each(function(i) { indexes.push($(this).find(':hidden[name="pk"]').val()); @@ -17,5 +17,7 @@ jQuery(function($){ data: { indexes: indexes.join(',') } }); } + }).click(function(e){ + e.preventDefault(); }); }); diff --git a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js b/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js index 08971b9..539100e 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.stacked.inlines.js @@ -10,21 +10,23 @@ jQuery(function($){ items : '.inline-related', stop : function(event, ui) { - var indexes = Array(); + var indexes = []; ui.item.parent().children('.inline-related').each(function(i) { - index_value = $(this).find(':hidden[name$="-id"]').val(); - if (index_value != "" && index_value != undefined) + var index_value = $(this).find(':hidden[name$="-id"]').val(); + if (index_value !== "" && index_value !== undefined) + { indexes.push(index_value); + } }); - + $.ajax({ url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(), type: 'POST', data: { indexes : indexes.join(',') }, success: function() { - ui.item.effect('highlight', {}, 1000); + ui.item.find('.form-row').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 f5cfa28..ae032df 100644 --- a/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js +++ b/adminsortable/static/adminsortable/js/admin.sortable.tabular.inlines.js @@ -10,14 +10,16 @@ jQuery(function($){ items : 'tr:not(.add-row)', stop : function(event, ui) { - var indexes = Array(); + var indexes = []; ui.item.parent().children('tr').each(function(i) { - index_value = $(this).find('.original :hidden:first').val(); - if (index_value != "" && index_value != undefined) + var index_value = $(this).find('.original :hidden:first').val(); + if (index_value !== '' && index_value !== undefined) + { indexes.push(index_value); + } }); - + $.ajax({ url: ui.item.parent().find(':hidden[name="admin_sorting_url"]').val(), type: 'POST', diff --git a/adminsortable/utils.py b/adminsortable/utils.py new file mode 100644 index 0000000..5cc41b1 --- /dev/null +++ b/adminsortable/utils.py @@ -0,0 +1,4 @@ +def get_is_sortable(objects): + if len(objects) > 1: + return True + return False diff --git a/sample_project/app/admin.py b/sample_project/app/admin.py index 4975ae0..c382be1 100644 --- a/sample_project/app/admin.py +++ b/sample_project/app/admin.py @@ -2,12 +2,42 @@ from django.contrib import admin from adminsortable.admin import (SortableAdmin, SortableTabularInline, SortableStackedInline, SortableGenericStackedInline) -from app.models import Category, Project, Credit, Note, GenericNote +from adminsortable.utils import get_is_sortable +from app.models import (Category, Widget, Project, Credit, Note, GenericNote, + Component) admin.site.register(Category, SortableAdmin) +class ComponentInline(SortableStackedInline): + model = Component + + def queryset(self, request): + qs = super(ComponentInline, self).queryset(request).exclude(title__icontains='2') + if get_is_sortable(qs): + self.model.is_sortable = True + else: + self.model.is_sortable = False + return qs + + +class WidgetAdmin(SortableAdmin): + def queryset(self, request): + """ + A simple example demonstrating that adminsortable works even in + situations where you need to filter the queryset in admin. Here, + we are just filtering out `widget` instances with an pk higher + than 3 + """ + qs = super(WidgetAdmin, self).queryset(request) + return qs.filter(id__lte=3) + + inlines = [ComponentInline] + +admin.site.register(Widget, WidgetAdmin) + + class CreditInline(SortableTabularInline): model = Credit diff --git a/sample_project/app/migrations/0001_initial.py b/sample_project/app/migrations/0001_initial.py new file mode 100644 index 0000000..4633a2e --- /dev/null +++ b/sample_project/app/migrations/0001_initial.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Category' + db.create_table(u'app_category', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), + )) + db.send_create_signal(u'app', ['Category']) + + # Adding model 'Project' + db.create_table(u'app_project', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('category', self.gf('adminsortable.fields.SortableForeignKey')(to=orm['app.Category'])), + ('description', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal(u'app', ['Project']) + + # Adding model 'Credit' + db.create_table(u'app_credit', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app.Project'])), + ('first_name', self.gf('django.db.models.fields.CharField')(max_length=30)), + ('last_name', self.gf('django.db.models.fields.CharField')(max_length=30)), + )) + db.send_create_signal(u'app', ['Credit']) + + # Adding model 'Note' + db.create_table(u'app_note', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['app.Project'])), + ('text', self.gf('django.db.models.fields.CharField')(max_length=100)), + )) + db.send_create_signal(u'app', ['Note']) + + # Adding model 'GenericNote' + db.create_table(u'app_genericnote', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='generic_notes', to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + )) + db.send_create_signal(u'app', ['GenericNote']) + + + def backwards(self, orm): + # Deleting model 'Category' + db.delete_table(u'app_category') + + # Deleting model 'Project' + db.delete_table(u'app_project') + + # Deleting model 'Credit' + db.delete_table(u'app_credit') + + # Deleting model 'Note' + db.delete_table(u'app_note') + + # Deleting model 'GenericNote' + db.delete_table(u'app_genericnote') + + + models = { + u'app.category': { + 'Meta': {'ordering': "['order']", 'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.credit': { + 'Meta': {'ordering': "['order']", 'object_name': 'Credit'}, + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}) + }, + u'app.genericnote': { + 'Meta': {'ordering': "['order']", 'object_name': 'GenericNote'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'generic_notes'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.note': { + 'Meta': {'ordering': "['order']", 'object_name': 'Note'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'app.project': { + 'Meta': {'ordering': "['order']", 'object_name': 'Project'}, + 'category': ('adminsortable.fields.SortableForeignKey', [], {'to': u"orm['app.Category']"}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['app'] \ No newline at end of file diff --git a/sample_project/app/migrations/0002_add_widget.py b/sample_project/app/migrations/0002_add_widget.py new file mode 100644 index 0000000..b7f66b8 --- /dev/null +++ b/sample_project/app/migrations/0002_add_widget.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Widget' + db.create_table(u'app_widget', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), + )) + db.send_create_signal(u'app', ['Widget']) + + + def backwards(self, orm): + # Deleting model 'Widget' + db.delete_table(u'app_widget') + + + models = { + u'app.category': { + 'Meta': {'ordering': "['order']", 'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.credit': { + 'Meta': {'ordering': "['order']", 'object_name': 'Credit'}, + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}) + }, + u'app.genericnote': { + 'Meta': {'ordering': "['order']", 'object_name': 'GenericNote'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'generic_notes'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.note': { + 'Meta': {'ordering': "['order']", 'object_name': 'Note'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'app.project': { + 'Meta': {'ordering': "['order']", 'object_name': 'Project'}, + 'category': ('adminsortable.fields.SortableForeignKey', [], {'to': u"orm['app.Category']"}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.widget': { + 'Meta': {'ordering': "['order']", 'object_name': 'Widget'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['app'] \ No newline at end of file diff --git a/sample_project/app/migrations/0003_add_component.py b/sample_project/app/migrations/0003_add_component.py new file mode 100644 index 0000000..6442d69 --- /dev/null +++ b/sample_project/app/migrations/0003_add_component.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Component' + db.create_table(u'app_component', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('order', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('widget', self.gf('adminsortable.fields.SortableForeignKey')(to=orm['app.Widget'])), + )) + db.send_create_signal(u'app', ['Component']) + + + def backwards(self, orm): + # Deleting model 'Component' + db.delete_table(u'app_component') + + + models = { + u'app.category': { + 'Meta': {'ordering': "['order']", 'object_name': 'Category'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.component': { + 'Meta': {'ordering': "['order']", 'object_name': 'Component'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'widget': ('adminsortable.fields.SortableForeignKey', [], {'to': u"orm['app.Widget']"}) + }, + u'app.credit': { + 'Meta': {'ordering': "['order']", 'object_name': 'Credit'}, + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}) + }, + u'app.genericnote': { + 'Meta': {'ordering': "['order']", 'object_name': 'GenericNote'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'generic_notes'", 'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.note': { + 'Meta': {'ordering': "['order']", 'object_name': 'Note'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['app.Project']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'app.project': { + 'Meta': {'ordering': "['order']", 'object_name': 'Project'}, + 'category': ('adminsortable.fields.SortableForeignKey', [], {'to': u"orm['app.Category']"}), + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'app.widget': { + 'Meta': {'ordering': "['order']", 'object_name': 'Widget'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['app'] \ No newline at end of file diff --git a/sample_project/app/migrations/__init__.py b/sample_project/app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample_project/app/models.py b/sample_project/app/models.py index 0f972b8..b3622b1 100644 --- a/sample_project/app/models.py +++ b/sample_project/app/models.py @@ -16,7 +16,7 @@ class SimpleModel(models.Model): return self.title -#a model that is sortable +# A model that is sortable class Category(SimpleModel, Sortable): class Meta(Sortable.Meta): """ @@ -27,8 +27,17 @@ class Category(SimpleModel, Sortable): verbose_name_plural = 'Categories' -#a model that is sortable relative to a foreign key that is also sortable -#uses SortableForeignKey field. Works with versions 1.3+ +# A model with an override of its queryset for admin +class Widget(SimpleModel, Sortable): + class Meta(Sortable.Meta): + pass + + def __unicode__(self): + return self.title + + +# A model that is sortable relative to a foreign key that is also sortable +# uses SortableForeignKey field. Works with versions 1.3+ class Project(SimpleModel, Sortable): class Meta(Sortable.Meta): pass @@ -37,7 +46,7 @@ class Project(SimpleModel, Sortable): description = models.TextField() -#registered as a tabular inline on `Project` +# Registered as a tabular inline on `Project` class Credit(Sortable): class Meta(Sortable.Meta): pass @@ -50,7 +59,7 @@ class Credit(Sortable): return '{0} {1}'.format(self.first_name, self.last_name) -#registered as a stacked inline on `Project` +# Registered as a stacked inline on `Project` class Note(Sortable): class Meta(Sortable.Meta): pass @@ -62,7 +71,7 @@ class Note(Sortable): return self.text -#a generic bound model +# A generic bound model class GenericNote(SimpleModel, Sortable): content_type = models.ForeignKey(ContentType, verbose_name=u"Content type", related_name="generic_notes") @@ -75,3 +84,14 @@ class GenericNote(SimpleModel, Sortable): def __unicode__(self): return u'{0} : {1}'.format(self.title, self.content_object) + + +# An model registered as an inline that has a custom queryset +class Component(SimpleModel, Sortable): + class Meta(Sortable.Meta): + pass + + widget = SortableForeignKey(Widget) + + def __unicode__(self): + return self.title diff --git a/sample_project/database/test_project.sqlite b/sample_project/database/test_project.sqlite index fd926983c8009cb5585508e1e809f829342e4ff4..6629776a5cadda4b28afec0b1e19471b2fde4238 100644 GIT binary patch delta 5074 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 delta 1713 zcmY*ZYitx%7`@+j?(BBjrR;3Ch1M3fJW}bRvQ?-MBi08fB0(V%A`zE%(k_(UvR#Zd zMlcdlF#=A^V&q{$fJ9BS9TUZv@DzXG4SQ4s;A|jw-#0I?cSVAYe$;|oAx#!$_ zzk642kgLDIhN_6TDuifM|H3n78?Z(|7=NDCI#u$4B`9ck?!Wi=XG`_z7<0S{_c< z=q&ZpUiz4}&^qd(W|~C_%aCU(M@mXisKsnIbLQe!Gi_#5&FM_e9G4#qQ01y}NpT7# zTUu_djWHyRr4nD{uxmY}xhpUY)|twg5fAaD4q~|L z5O8G9XE;9>R9@%KNh{FI&dyXOUFvEzm+2wH@?lV=w?(p$K9kO=Jh`q8vm}3psUqdd z;D=b*X=Zf~@dXZ2lLj8c6?@^ z3~PK{@O3qQ#YM}Iz4pZm5d__X zg^5;h(PK*a1KSf9pQsLNFa`FjklV2aCT6J8WLWj9CRhtk8uqKJUXc|#dIbjY53Z!ub1djyWDvwYq*HgCa< zW1_;j1%^CPmV)9u-|WoA9`gG%)=)-L=<%UoA_|8J`|^52Mo+3cWeL=8tD5>ZD&L!U zK{?NcwIqMvln4g|=_4xnaWk7u%};i7=zcUt5D& z$z`Tjbrx3>yM)+9Uvm$};BCv0@7mKhEkvnP1NI9w=ijhjIe&mha9os=UdQv2%RAc5 z*c|7cD1)Yw48`t_kfvPgZsjG)*d@aLbDJTnN_c_534DqTSc->nI|BR@@8;KefwG#) z_JW-sk{98UP*>_DTvPaIl;Nt-G!QWZy6(KezVT70NV}%C=r23`SwAAsBy^HaQNktL zH$K;7Bq-{I=%z|~jn48ctP%Aao1#|Np0(Dd_uK4uzB