Incremented version to 1.2.

Refactored ORM calls to properly order objects by the sortable_by property to ensure objects are grouped correctly in the sortable change list template after being passed through dynamic_regroup.
Fixed missing import for jquery.effects.core, again.
Refactored sortable_by classmethod into a property.
master
Brandon Taylor 2011-11-22 22:19:39 -06:00
parent 1b9c5c0a64
commit 63a80f5953
10 changed files with 132 additions and 127 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

@ -60,16 +60,21 @@ class SortableAdmin(ModelAdmin):
#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()
except 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()
print sortable_by_expression
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__()
@ -115,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(','))
@ -136,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):
@ -144,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

@ -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]
print expression #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)

Binary file not shown.

View File

View File

@ -29,20 +29,20 @@ class Project(SimpleModel, Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
pass pass
# @classmethod #deprecated: shown for backward compatibility only. Reference class "Sample" for proper
# def sortable_by(cls): # designation of `sortable_by` as a property
# return Category, 'category' @classmethod
def sortable_by(cls):
return Category, 'category'
category = models.ForeignKey(Category) category = models.ForeignKey(Category)
description = models.TextField() description = models.TextField()
sortable_by = Category
#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
class Sample(SimpleModel, Sortable): class Sample(SimpleModel, Sortable):
class Meta(Sortable.Meta): class Meta(Sortable.Meta):
pass ordering = Sortable.Meta.ordering + ['category']
category = models.ForeignKey(Category) category = models.ForeignKey(Category)
description = models.TextField() description = models.TextField()

View File

@ -80,9 +80,6 @@ class SortableTestCase(TestCase):
def get_category_indexes(self, *categories): def get_category_indexes(self, *categories):
return {'indexes' : ','.join([str(c.id) for c in categories])} return {'indexes' : ','.join([str(c.id) for c in categories])}
def test_sortable_by_backwards_compatibility(self):
pass
def test_adminsortable_changelist_templates(self): def test_adminsortable_changelist_templates(self):
logged_in = self.client.login(username=self.user.username, password=self.user_raw_password) logged_in = self.client.login(username=self.user.username, password=self.user_raw_password)
self.assertTrue(logged_in, 'User is not logged in') self.assertTrue(logged_in, 'User is not logged in')