Added SortableForeignKey field to replace sortable_by model property.

Refactored how the sortable_by properties get populated by looping over the model fields until we get to the SortableForeignKey, then grabbing properties from the field and its related data.
This commit is contained in:
Brandon Taylor 2012-02-24 22:35:30 -06:00
parent 63a80f5953
commit 37f91cce97
80 changed files with 135 additions and 239 deletions

0
.gitignore vendored Executable file → Normal file
View file

0
AUTHORS Executable file → Normal file
View file

0
COPYRIGHT Executable file → Normal file
View file

0
MANIFEST.in Executable file → Normal file
View file

0
README Executable file → Normal file
View file

2
adminsortable/__init__.py Executable file → Normal file
View file

@ -1,4 +1,4 @@
VERSION = (1, 2, "f", 0) # following PEP 386 VERSION = (1, 3, "f", 0) # following PEP 386
DEV_N = None DEV_N = None

38
adminsortable/admin.py Executable file → Normal file
View file

@ -9,6 +9,7 @@ from django.shortcuts import render
from django.template.defaultfilters import capfirst from django.template.defaultfilters import capfirst
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from adminsortable.fields import SortableForeignKey
from adminsortable.models import Sortable from adminsortable.models import Sortable
STATIC_URL = settings.STATIC_URL STATIC_URL = settings.STATIC_URL
@ -20,8 +21,17 @@ class SortableAdmin(ModelAdmin):
class Meta: class Meta:
abstract = True abstract = True
def _get_sortable_foreign_key(self):
sortable_foreign_key = None
for field in self.model._meta.fields:
if isinstance(field, SortableForeignKey):
sortable_foreign_key = field
break
return sortable_foreign_key
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SortableAdmin, self).__init__(*args, **kwargs) super(SortableAdmin, self).__init__(*args, **kwargs)
self.has_sortable_tabular_inlines = False self.has_sortable_tabular_inlines = False
self.has_sortable_stacked_inlines = False self.has_sortable_stacked_inlines = False
for klass in self.inlines: for klass in self.inlines:
@ -55,11 +65,16 @@ class SortableAdmin(ModelAdmin):
#Determine if we need to regroup objects relative to a foreign key specified on the #Determine if we need to regroup objects relative to a foreign key specified on the
# model class that is extending Sortable. # model class that is extending Sortable.
sortable_by = getattr(self.model, 'sortable_by', None) #Legacy support for 'sortable_by' defined as a model property
if sortable_by: sortable_by_property = getattr(self.model, 'sortable_by', None)
#`sortable_by` defined as a SortableForeignKey
sortable_by_fk = self._get_sortable_foreign_key()
if sortable_by_property:
#backwards compatibility for < 1.1.1, where sortable_by was a classmethod instead of a property #backwards compatibility for < 1.1.1, where sortable_by was a classmethod instead of a property
try: try:
sortable_by_class, sortable_by_expression = sortable_by() sortable_by_class, sortable_by_expression = sortable_by_property()
except TypeError, ValueError: except TypeError, ValueError:
sortable_by_class = self.model.sortable_by sortable_by_class = self.model.sortable_by
sortable_by_expression = sortable_by_class.__name__.lower() sortable_by_expression = sortable_by_class.__name__.lower()
@ -67,15 +82,24 @@ class SortableAdmin(ModelAdmin):
sortable_by_class_display_name = sortable_by_class._meta.verbose_name_plural sortable_by_class_display_name = sortable_by_class._meta.verbose_name_plural
sortable_by_class_is_sortable = sortable_by_class.is_sortable() sortable_by_class_is_sortable = sortable_by_class.is_sortable()
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_is_sortable = sortable_by_class.is_sortable()
else:
#model is not sortable by another model
sortable_by_class = sortable_by_expression = sortable_by_class_display_name =\
sortable_by_class_is_sortable = None
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 # Order the objects by the property they are sortable by, then by the order, otherwise the regroup
# template tag will not show the objects correctly as # template tag will not show the objects correctly as
# shown in https://docs.djangoproject.com/en/1.3/ref/templates/builtins/#regroup # shown in https://docs.djangoproject.com/en/1.3/ref/templates/builtins/#regroup
objects = objects.order_by(sortable_by_expression, 'order') objects = objects.order_by(sortable_by_expression, 'order')
else:
sortable_by_class = sortable_by_expression = sortable_by_class_display_name =\
sortable_by_class_is_sortable = None
try: try:
verbose_name_plural = opts.verbose_name_plural.__unicode__() verbose_name_plural = opts.verbose_name_plural.__unicode__()
except AttributeError: except AttributeError:

9
adminsortable/fields.py Normal file
View file

@ -0,0 +1,9 @@
from django.db.models.fields.related import ForeignKey
class SortableForeignKey(ForeignKey):
"""
Field simply acts as a flag to determine the class to sort by.
This field replaces previous functionality where `sortable_by` was definied as a model property
that specified another model class.
"""

0
adminsortable/locale/en/LC_MESSAGES/django.po Executable file → Normal file
View file

0
adminsortable/locale/nl/LC_MESSAGES/django.mo Executable file → Normal file
View file

0
adminsortable/locale/nl/LC_MESSAGES/django.po Executable file → Normal file
View file

24
adminsortable/models.py Executable file → Normal file
View file

@ -1,6 +1,16 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from adminsortable.fields import SortableForeignKey
class MultipleSortableForeignKeyException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
class Sortable(models.Model): class Sortable(models.Model):
""" """
@ -18,7 +28,10 @@ class Sortable(models.Model):
Override `sortable_by` method to make your model be sortable by a foreign key field. 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. Set `sortable_by` to the class specified in the foreign key relationship.
""" """
order = models.PositiveIntegerField(editable=False, default=1, db_index=True) order = models.PositiveIntegerField(editable=False, default=1, db_index=True)
#legacy support
sortable_by = None sortable_by = None
class Meta: class Meta:
@ -37,6 +50,17 @@ class Sortable(models.Model):
def model_type_id(cls): def model_type_id(cls):
return ContentType.objects.get_for_model(cls).id return ContentType.objects.get_for_model(cls).id
def __init__(self, *args, **kwargs):
super(Sortable, self).__init__(*args, **kwargs)
#Validate that model only contains at most one SortableForeignKey
sortable_foreign_keys = []
for field in self._meta.fields:
if isinstance(field, SortableForeignKey):
sortable_foreign_keys.append(field)
if len(sortable_foreign_keys) > 1:
raise MultipleSortableForeignKeyException(u'%s may only have one SortableForeignKey' % self)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.id: if not self.id:
try: try:

View file

View file

0
adminsortable/static/adminsortable/js/jquery.effects.core.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.effects.highlight.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.core.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.draggable.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.droppable.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.mouse.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.sortable.js vendored Executable file → Normal file
View file

0
adminsortable/static/adminsortable/js/jquery.ui.widget.js vendored Executable file → Normal file
View file

0
adminsortable/templates/adminsortable/change_form.html Executable file → Normal file
View file

0
adminsortable/templates/adminsortable/change_list.html Executable file → Normal file
View file

View file

View file

View file

View file

View file

View file

View file

View file

0
adminsortable/templatetags/__init__.py Executable file → Normal file
View file

0
adminsortable/templatetags/adminsortable_tags.py Executable file → Normal file
View file

View file

0
sample_project/README Executable file → Normal file
View file

0
sample_project/__init__.py Executable file → Normal file
View file

BIN
sample_project/adminsortable.sqlite Executable file → Normal file

Binary file not shown.

0
sample_project/app/__init__.py Executable file → Normal file
View file

3
sample_project/app/admin.py Executable file → Normal file
View file

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from adminsortable.admin import SortableAdmin, SortableTabularInline, SortableStackedInline from adminsortable.admin import SortableAdmin, SortableTabularInline, SortableStackedInline
from app.models import Category, Project, Credit, Note, Sample from app.models import Category, Project, Credit, Note
admin.site.register(Category, SortableAdmin) admin.site.register(Category, SortableAdmin)
@ -20,4 +20,3 @@ class ProjectAdmin(SortableAdmin):
list_display = ['__unicode__', 'category'] list_display = ['__unicode__', 'category']
admin.site.register(Project, ProjectAdmin) admin.site.register(Project, ProjectAdmin)
admin.site.register(Sample, SortableAdmin)

View file

@ -0,0 +1,56 @@
[
{
"pk": 1,
"model": "app.category",
"fields": {
"order": 1,
"title": "Test 1"
}
},
{
"pk": 2,
"model": "app.category",
"fields": {
"order": 2,
"title": "Test 2"
}
},
{
"pk": 3,
"model": "app.category",
"fields": {
"order": 3,
"title": "Test 3"
}
},
{
"pk": 1,
"model": "app.project",
"fields": {
"category": 1,
"description": "Test",
"order": 1,
"title": "Test Project 1"
}
},
{
"pk": 2,
"model": "app.project",
"fields": {
"category": 1,
"description": "Test",
"order": 2,
"title": "Test Project 2"
}
},
{
"pk": 3,
"model": "app.project",
"fields": {
"category": 2,
"description": "Test",
"order": 3,
"title": "Test Project 3"
}
}
]

0
sample_project/app/fixtures/test_data.json Executable file → Normal file
View file

View file

@ -1,77 +0,0 @@
# encoding: 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('app_category', (
('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('app', ['Category'])
# Adding model 'Project'
db.create_table('app_project', (
('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('django.db.models.fields.related.ForeignKey')(to=orm['app.Category'])),
('description', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('app', ['Project'])
# Adding model 'Credit'
db.create_table('app_credit', (
('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('app', ['Credit'])
def backwards(self, orm):
# Deleting model 'Category'
db.delete_table('app_category')
# Deleting model 'Project'
db.delete_table('app_project')
# Deleting model 'Credit'
db.delete_table('app_credit')
models = {
'app.category': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Category'},
'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'})
},
'app.credit': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Credit'},
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'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': "orm['app.Project']"})
},
'app.project': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Project'},
'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app.Category']"}),
'description': ('django.db.models.fields.TextField', [], {}),
'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'})
}
}
complete_apps = ['app']

View file

@ -1,59 +0,0 @@
# encoding: 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 'Note'
db.create_table('app_note', (
('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('app', ['Note'])
def backwards(self, orm):
# Deleting model 'Note'
db.delete_table('app_note')
models = {
'app.category': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Category'},
'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'})
},
'app.credit': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Credit'},
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'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': "orm['app.Project']"})
},
'app.note': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Note'},
'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': "orm['app.Project']"}),
'text': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'app.project': {
'Meta': {'ordering': "['order', 'id']", 'object_name': 'Project'},
'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app.Category']"}),
'description': ('django.db.models.fields.TextField', [], {}),
'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'})
}
}
complete_apps = ['app']

View file

@ -1,68 +0,0 @@
# encoding: 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 'Sample'
db.create_table('app_sample', (
('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('django.db.models.fields.related.ForeignKey')(to=orm['app.Category'])),
('description', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('app', ['Sample'])
def backwards(self, orm):
# Deleting model 'Sample'
db.delete_table('app_sample')
models = {
'app.category': {
'Meta': {'ordering': "['order']", 'object_name': 'Category'},
'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'})
},
'app.credit': {
'Meta': {'ordering': "['order']", 'object_name': 'Credit'},
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'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': "orm['app.Project']"})
},
'app.note': {
'Meta': {'ordering': "['order']", 'object_name': 'Note'},
'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': "orm['app.Project']"}),
'text': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'app.project': {
'Meta': {'ordering': "['order']", 'object_name': 'Project'},
'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app.Category']"}),
'description': ('django.db.models.fields.TextField', [], {}),
'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'})
},
'app.sample': {
'Meta': {'ordering': "['order']", 'object_name': 'Sample'},
'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['app.Category']"}),
'description': ('django.db.models.fields.TextField', [], {}),
'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'})
}
}
complete_apps = ['app']

27
sample_project/app/models.py Executable file → Normal file
View file

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from adminsortable.fields import SortableForeignKey
from adminsortable.models import Sortable from adminsortable.models import Sortable
@ -25,34 +26,16 @@ class Category(SimpleModel, Sortable):
#a model that is sortable relative to a foreign key that is also sortable #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 Project(SimpleModel, Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
pass pass
#deprecated: shown for backward compatibility only. Reference class "Sample" for proper category = SortableForeignKey(Category)
# designation of `sortable_by` as a property
@classmethod
def sortable_by(cls):
return Category, 'category'
category = models.ForeignKey(Category)
description = models.TextField() description = models.TextField()
#a model that is sortable relative to a foreign key that is also sortable #registered as a tabular inline on `Project`
class Sample(SimpleModel, Sortable):
class Meta(Sortable.Meta):
ordering = Sortable.Meta.ordering + ['category']
category = models.ForeignKey(Category)
description = models.TextField()
#field to define which foreign key the model is sortable by.
#works with versions > 1.1.1
sortable_by = Category
#registered as a tabular inline on project
class Credit(Sortable): class Credit(Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
pass pass
@ -65,7 +48,7 @@ class Credit(Sortable):
return '%s %s' % (self.first_name, self.last_name) return '%s %s' % (self.first_name, self.last_name)
#registered as a stacked inline on project #registered as a stacked inline on `Project`
class Note(Sortable): class Note(Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
pass pass

11
sample_project/app/tests.py Executable file → Normal file
View file

@ -7,8 +7,14 @@ from django.db import models
from django.test import TestCase from django.test import TestCase
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from models import Sortable from adminsortable.fields import SortableForeignKey
from app.models import Category from adminsortable.models import Sortable, MultipleSortableForeignKeyException
from app.models import Category, Credit, Note
class BadSortableModel(models.Model):
note = SortableForeignKey(Note)
credit = SortableForeignKey(Credit)
class TestSortableModel(Sortable): class TestSortableModel(Sortable):
@ -19,7 +25,6 @@ class TestSortableModel(Sortable):
class SortableTestCase(TestCase): class SortableTestCase(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()

0
sample_project/appmedia/BeautifulSoup.py Executable file → Normal file
View file

0
sample_project/appmedia/__init__.py Executable file → Normal file
View file

0
sample_project/appmedia/api.py Executable file → Normal file
View file

0
sample_project/appmedia/management/__init__.py Executable file → Normal file
View file

View file

View file

View file

0
sample_project/appmedia/middleware.py Executable file → Normal file
View file

0
sample_project/appmedia/models.py Executable file → Normal file
View file

0
sample_project/appmedia/urls.py Executable file → Normal file
View file

0
sample_project/appmedia/views.py Executable file → Normal file
View file

0
sample_project/manage.py Executable file → Normal file
View file

0
sample_project/settings.py Executable file → Normal file
View file

View file

View file

0
sample_project/static/adminsortable/js/jquery.effects.core.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.effects.highlight.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.core.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.draggable.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.droppable.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.mouse.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.sortable.js vendored Executable file → Normal file
View file

0
sample_project/static/adminsortable/js/jquery.ui.widget.js vendored Executable file → Normal file
View file

0
sample_project/templates/index.html Executable file → Normal file
View file

0
sample_project/urls.py Executable file → Normal file
View file

0
setup.py Executable file → Normal file
View file