Added south field triple to SortableForeignKey field

master
Brandon Taylor 2012-03-07 21:52:57 -06:00
commit 49b7d7d0e3
19 changed files with 154 additions and 256 deletions

25
README 100755 → 100644
View File

@ -45,27 +45,24 @@ have an inner Meta class that inherits from ``Sortable.Meta``
return self.title
For models that you want sortable relative to a ``ForeignKey`` field, you need to
specify a property: ``sortable_by`` that is equal to the class defined as your ForeignKey field.
If you're upgrading from a version < 1.2, you do not need to redefine sortable_by.
1.2 is backwards compatible to 1.0.
It is also possible to order objects relative to another object that is a ForeignKey:
#admin.py
from adminsortable.fields import SortableForeignKey
#models.py
class Category(models.Model):
title = models.CharField(max_length=50)
...
class MySortableClass(Sortable):
class Project(Sortable):
class Meta(Sortable.Meta)
category = models.ForeignKey(Category)
category = SortableForeignKey(Category)
title = models.CharField(max_length=50)
def __unicode__(self):
return self.title
sortable_by = Category
Sortable has one field: `order` and adds a default ordering value set to `order`.
@ -132,13 +129,10 @@ Status
admin-sortable is currently used in production.
What's new in 1.2
What's new in 1.3
=============
- Refactored ``sortable_by`` to be a property rather than a classmethod, which is much less work to implement.
- Fixed an issue with ordering which could result in sortable change list view objects not being grouped properly.
- Refactored the ORM calls to determine if an object is sortable, and what the next order should be, to return
scalar values and to not hydrate any objects whatsoever. This potentially decreases memory usage by exponential
factors.
- Refactored ``sortable_by`` to subclass ForeignKey rather than a property or classmethod.
No more extra property hackishness.
Features
=============
@ -156,7 +150,6 @@ to the foreign key object if it also inherits from Sortable.
Future
------
- Support for foreign keys that are self referential
- Support for ForeignKeys that have not been previously defined
- More unit tests
Requirements

View File

@ -1,4 +1,4 @@
VERSION = (1, 2, 0, "f", 0) # following PEP 386
VERSION = (1, 3, "f", 0) # following PEP 386
DEV_N = None
@ -7,7 +7,7 @@ def get_version():
if VERSION[2]:
version = "%s.%s" % (version, VERSION[2])
if VERSION[3] != "f":
version = "%s%s%s" % (version, VERSION[3], VERSION[4])
version = "%s%s" % (version, VERSION[3])
if DEV_N:
version = "%s.dev%s" % (version, DEV_N)
return version

38
adminsortable/admin.py 100755 → 100644
View File

@ -9,6 +9,7 @@ from django.shortcuts import render
from django.template.defaultfilters import capfirst
from django.views.decorators.csrf import csrf_exempt
from adminsortable.fields import SortableForeignKey
from adminsortable.models import Sortable
STATIC_URL = settings.STATIC_URL
@ -20,8 +21,17 @@ class SortableAdmin(ModelAdmin):
class Meta:
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):
super(SortableAdmin, self).__init__(*args, **kwargs)
self.has_sortable_tabular_inlines = False
self.has_sortable_stacked_inlines = False
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
# model class that is extending Sortable.
sortable_by = getattr(self.model, 'sortable_by', None)
if sortable_by:
#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._get_sortable_foreign_key()
if sortable_by_property:
#backwards compatibility for < 1.1.1, where sortable_by was a classmethod instead of a property
try:
sortable_by_class, sortable_by_expression = sortable_by()
sortable_by_class, sortable_by_expression = sortable_by_property()
except TypeError, ValueError:
sortable_by_class = self.model.sortable_by
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_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
# template tag will not show the objects correctly as
# shown in https://docs.djangoproject.com/en/1.3/ref/templates/builtins/#regroup
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:
verbose_name_plural = opts.verbose_name_plural.__unicode__()
except AttributeError:

View File

@ -0,0 +1,18 @@
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.
"""
def south_field_triple(self):
try:
from south.modelsinspector import introspector
cls_name = '%s.%s' % (self.__class__.__module__ , self.__class__.__name__)
args, kwargs = introspector(self)
return cls_name, args, kwargs
except ImportError:
pass

24
adminsortable/models.py 100755 → 100644
View File

@ -1,6 +1,16 @@
from django.contrib.contenttypes.models import ContentType
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):
"""
@ -18,7 +28,10 @@ class Sortable(models.Model):
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)
#legacy support
sortable_by = None
class Meta:
@ -37,6 +50,17 @@ class Sortable(models.Model):
def model_type_id(cls):
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):
if not self.id:
try:

BIN
sample_project/adminsortable.sqlite 100755 → 100644

Binary file not shown.

3
sample_project/app/admin.py 100755 → 100644
View File

@ -1,7 +1,7 @@
from django.contrib import admin
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)
@ -20,4 +20,3 @@ class ProjectAdmin(SortableAdmin):
list_display = ['__unicode__', 'category']
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"
}
}
]

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 100755 → 100644
View File

@ -1,5 +1,6 @@
from django.db import models
from adminsortable.fields import SortableForeignKey
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
#uses SortableForeignKey field. Works with versions 1.3+
class Project(SimpleModel, Sortable):
class Meta(Sortable.Meta):
pass
#deprecated: shown for backward compatibility only. Reference class "Sample" for proper
# designation of `sortable_by` as a property
@classmethod
def sortable_by(cls):
return Category, 'category'
category = models.ForeignKey(Category)
category = SortableForeignKey(Category)
description = models.TextField()
#a model that is sortable relative to a foreign key that is also sortable
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
#registered as a tabular inline on `Project`
class Credit(Sortable):
class Meta(Sortable.Meta):
pass
@ -65,7 +48,7 @@ class Credit(Sortable):
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 Meta(Sortable.Meta):
pass

11
sample_project/app/tests.py 100755 → 100644
View File

@ -7,8 +7,14 @@ from django.db import models
from django.test import TestCase
from django.test.client import Client, RequestFactory
from models import Sortable
from app.models import Category
from adminsortable.fields import SortableForeignKey
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):
@ -19,7 +25,6 @@ class TestSortableModel(Sortable):
class SortableTestCase(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()

0
sample_project/settings.py 100755 → 100644
View File