Merge branch 'sortable-by-refactor'

master
Brandon Taylor 2011-11-22 22:25:45 -06:00
commit 789f22b69b
13 changed files with 156 additions and 59 deletions

20
README
View File

@ -46,8 +46,9 @@ have an inner Meta class that inherits from ``Sortable.Meta``
For models that you want sortable relative to a ``ForeignKey`` field, you need to For models that you want sortable relative to a ``ForeignKey`` field, you need to
specify an ``@classmethod`` that returns a double: the foreign key class, and the specify a property: ``sortable_by`` that is equal to the class defined as your ForeignKey field.
name of the foreign key property as defined on your model, as a string. 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.
#admin.py #admin.py
class Category(models.Model): class Category(models.Model):
@ -63,9 +64,7 @@ name of the foreign key property as defined on your model, as a string.
def __unicode__(self): def __unicode__(self):
return self.title return self.title
@classmethod sortable_by = Category
def sortable_by(cls):
return Category, 'category'
Sortable has one field: `order` and adds a default ordering value set to `order`. Sortable has one field: `order` and adds a default ordering value set to `order`.
@ -132,7 +131,16 @@ Status
============= =============
admin-sortable is currently used in production. admin-sortable is currently used in production.
Feautures
What's new in 1.2
=============
- 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.
Features
============= =============
Current Current
--------- ---------

View File

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

View File

@ -53,18 +53,28 @@ class SortableAdmin(ModelAdmin):
has_perm = request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) has_perm = request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
objects = self.model.objects.all() objects = self.model.objects.all()
""" #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) sortable_by = getattr(self.model, 'sortable_by', None)
if sortable_by: if sortable_by:
sortable_by_class, sortable_by_expression = sortable_by() #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()
except TypeError, ValueError:
sortable_by_class = self.model.sortable_by
sortable_by_expression = sortable_by_class.__name__.lower()
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()
# 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: else:
sortable_by_class = sortable_by_expression = sortable_by_class_display_name = \ sortable_by_class = sortable_by_expression = sortable_by_class_display_name =\
sortable_by_class_is_sortable = None sortable_by_class_is_sortable = None
try: try:
verbose_name_plural = opts.verbose_name_plural.__unicode__() verbose_name_plural = opts.verbose_name_plural.__unicode__()
@ -110,7 +120,6 @@ class SortableAdmin(ModelAdmin):
This view sets the ordering of the objects for the model type and primary keys This view sets the ordering of the objects for the model type and primary keys
passed in. It must be an Ajax POST. passed in. It must be an Ajax POST.
""" """
if request.is_ajax() and request.method == 'POST': if request.is_ajax() and request.method == 'POST':
try: try:
indexes = map(str, request.POST.get('indexes', []).split(',')) indexes = map(str, request.POST.get('indexes', []).split(','))
@ -131,7 +140,7 @@ class SortableAdmin(ModelAdmin):
else: else:
response = {'objects_sorted' : False} response = {'objects_sorted' : False}
return HttpResponse(json.dumps(response, ensure_ascii=False), return HttpResponse(json.dumps(response, ensure_ascii=False),
mimetype='application/json') mimetype='application/json')
class SortableInlineBase(InlineModelAdmin): class SortableInlineBase(InlineModelAdmin):
@ -139,7 +148,8 @@ class SortableInlineBase(InlineModelAdmin):
super(SortableInlineBase, self).__init__(*args, **kwargs) super(SortableInlineBase, self).__init__(*args, **kwargs)
if not issubclass(self.model, Sortable): if not issubclass(self.model, Sortable):
raise Warning(u'Models that are specified in SortableTabluarInline and SortableStackedInline must inherit from Sortable') raise Warning(u'Models that are specified in SortableTabluarInline and SortableStackedInline '
'must inherit from Sortable')
self.is_sortable = self.model.is_sortable() self.is_sortable = self.model.is_sortable()

View File

@ -14,8 +14,12 @@ class Sortable(models.Model):
`model_type_id` returns the ContentType.id for the Model that inherits Sortable `model_type_id` returns the ContentType.id for the Model that inherits Sortable
`save` the override of save increments the last/highest value of order by 1 `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) order = models.PositiveIntegerField(editable=False, default=1, db_index=True)
sortable_by = None
class Meta: class Meta:
abstract = True abstract = True

View File

@ -55,7 +55,7 @@
{% if objects %} {% if objects %}
<div id="sortable"> <div id="sortable">
{% if group_expression %} {% if group_expression %}
{% render_nested_sortable_objects objects group_expression %} {% render_nested_sortable_objects objects group_expression %}
{% else %} {% else %}
{% render_sortable_objects objects %} {% render_sortable_objects objects %}
{% endif %} {% endif %}

View File

@ -5,6 +5,7 @@
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.draggable.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.draggable.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.droppable.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.droppable.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.sortable.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.ui.sortable.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.effects.core.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.effects.highlight.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/jquery.effects.highlight.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/admin.sortable.stacked.inlines.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/admin.sortable.stacked.inlines.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/admin.sortable.js"></script> <script type="text/javascript" src="{{ STATIC_URL }}adminsortable/js/admin.sortable.js"></script>

View File

@ -1,19 +1,18 @@
{% load django_template_additions adminsortable_tags %} {% load django_template_additions adminsortable_tags %}
{% dynamic_regroup objects by group_expression as regrouped_objects %} {% dynamic_regroup objects by group_expression as regrouped_objects %}
{% if regrouped_objects %} {% if regrouped_objects %}
<ul {% if sortable_by_class_is_sortable %}class="sortable"{% endif %}>
<ul {% if sortable_by_class_is_sortable %}class="sortable"{% endif %}> {% for regrouped_object in regrouped_objects %}
{% for regrouped_object in regrouped_objects %} <li>
<li> {% with object=regrouped_object.grouper %}
{% with object=regrouped_object.grouper %} {% render_object_rep object %}
{% render_object_rep object %} {% endwith %}
{% endwith %} {% if regrouped_object.list %}
{% if regrouped_object.list %} <ul {% if regrouped_object.grouper.is_sortable %}class="sortable"{% endif %}>
<ul {% if regrouped_object.grouper.is_sortable %}class="sortable"{% endif %}> {% render_list_items regrouped_object.list %}
{% render_list_items regrouped_object.list %} </ul>
</ul> {% endif %}
{% endif %} </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul>
{% endif %} {% endif %}

View File

@ -25,11 +25,9 @@ class DynamicRegroupNode(template.Node):
# List of dictionaries in the format: # List of dictionaries in the format:
# {'grouper': 'key', 'list': [list of contents]}. # {'grouper': 'key', 'list': [list of contents]}.
""" #Try to resolve the filter expression from the template context.
Try to resolve the filter expression from the template context. #If the variable doesn't exist, accept the value that passed to the
If the variable doesn't exist, accept the value that passed to the #template tag and convert it to a string
template tag and convert it to a string
"""
try: try:
exp = self.expression.resolve(context) exp = self.expression.resolve(context)
except template.VariableDoesNotExist: except template.VariableDoesNotExist:
@ -47,6 +45,15 @@ class DynamicRegroupNode(template.Node):
@register.tag @register.tag
def dynamic_regroup(parser, token): def dynamic_regroup(parser, token):
"""
Django expects the value of `expression` to be an attribute available on
your objects. The value you pass to the template tag gets converted into a
FilterExpression object from the literal.
Sometimes we need the attribute to group on to be dynamic. So, instead
of converting the value to a FilterExpression here, we're going to pass the
value as-is and convert it in the Node.
"""
firstbits = token.contents.split(None, 3) firstbits = token.contents.split(None, 3)
if len(firstbits) != 4: if len(firstbits) != 4:
raise TemplateSyntaxError("'regroup' tag takes five arguments") raise TemplateSyntaxError("'regroup' tag takes five arguments")
@ -58,20 +65,8 @@ def dynamic_regroup(parser, token):
raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must" raise TemplateSyntaxError("next-to-last argument to 'regroup' tag must"
" be 'as'") " be 'as'")
"""
Django expects the value of `expression` to be an attribute available on
your objects. The value you pass to the template tag gets converted into a
FilterExpression object from the literal.
Sometimes we need the attribute to group on to be dynamic. So, instead
of converting the value to a FilterExpression here, we're going to pass the
value as-is and convert it in the Node.
"""
expression = lastbits_reversed[2][::-1] expression = lastbits_reversed[2][::-1]
var_name = lastbits_reversed[0][::-1] var_name = lastbits_reversed[0][::-1]
#We also need to hand the parser to the node in order to convert the value
""" #for `expression` to a FilterExpression.
We also need to hand the parser to the node in order to convert the value
for `expression` to a FilterExpression.
"""
return DynamicRegroupNode(target, parser, expression, var_name) return DynamicRegroupNode(target, parser, expression, var_name)

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 from app.models import Category, Project, Credit, Note, Sample
admin.site.register(Category, SortableAdmin) admin.site.register(Category, SortableAdmin)
@ -20,3 +20,4 @@ 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,68 @@
# 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']

View File

@ -29,6 +29,8 @@ 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
# designation of `sortable_by` as a property
@classmethod @classmethod
def sortable_by(cls): def sortable_by(cls):
return Category, 'category' return Category, 'category'
@ -37,6 +39,19 @@ class Project(SimpleModel, Sortable):
description = models.TextField() 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 Credit(Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):

View File

@ -1,12 +1,9 @@
import httplib import httplib
import json import json
from django.contrib.auth.forms import authenticate, UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.middleware import csrf
from django.db import models from django.db import models
from django.template.defaultfilters import urlencode
from django.test import TestCase from django.test import TestCase
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory

View File

@ -102,7 +102,6 @@ MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
) )
TEMPLATE_CONTEXT_PROCESSORS = ( TEMPLATE_CONTEXT_PROCESSORS = (