* Ordering now works properly.
authorBradley Ayers <bradley.ayers@gmail.com>
Thu, 31 Mar 2011 23:10:15 +0000 (09:10 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Thu, 31 Mar 2011 23:10:15 +0000 (09:10 +1000)
* Updated documentation with information about OrderByTuple and OrderBy.
* Bumped version to v0.4.0.beta
* Updated paleblue theme with ordering icons.

18 files changed:
django_tables/columns.py
django_tables/static/django_tables/themes/paleblue/css/screen.css
django_tables/static/django_tables/themes/paleblue/img/arrow-active-down.png [new file with mode: 0644]
django_tables/static/django_tables/themes/paleblue/img/arrow-active-up.png [new file with mode: 0644]
django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-down.png [new file with mode: 0644]
django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-up.png [new file with mode: 0644]
django_tables/tables.py
django_tables/templates/django_tables/table.html
django_tables/utils.py
docs/conf.py
docs/index.rst
setup.py
tests/__init__.py
tests/core.py
tests/models.py
tests/testapp/__init__.py [new file with mode: 0644]
tests/testapp/models.py [new file with mode: 0644]
tests/utils.py [new file with mode: 0644]

index d3582621e7a059aa53ac13bd9de1e07c57addb55..5e7f82a6fdf6246144189a1726708e2e62e46e43 100644 (file)
@@ -2,6 +2,7 @@
 from django.utils.encoding import force_unicode, StrAndUnicode
 from django.utils.datastructures import SortedDict
 from django.utils.text import capfirst
+from .utils import OrderBy
 
 
 class Column(object):
@@ -131,7 +132,7 @@ class Column(object):
         """Returns a cell's content.
         This method can be overridden by ``render_FOO`` methods on the table or
         by subclassing :class:`Column`.
-        
+
         """
         return table.data.data_for_cell(bound_column=bound_column,
                                         bound_row=bound_row)
@@ -247,6 +248,17 @@ class BoundColumn(StrAndUnicode):
         """Returns a ``bool`` depending on whether this column is visible."""
         return self.column.visible
 
+    @property
+    def order_by(self):
+        """If this column is sorted, return the associated OrderBy instance.
+        Otherwise return a None.
+
+        """
+        try:
+            return self.table.order_by[self.name]
+        except IndexError:
+            return None
+
 
 class Columns(object):
     """Container for spawning BoundColumns.
index 916a2b9146480703c16ac5cfc5ecab9674baf917..edede86bfa0e280c9f58874e65f12f80b046c448 100644 (file)
@@ -46,6 +46,17 @@ table.paleblue thead th > a:visited {
     display: block;
 }
 
+table.paleblue thead th.sortable > a {
+    padding-right: 20px;
+    background: url(../img/arrow-inactive-up.png) right center no-repeat;
+}
+table.paleblue thead th.sortable.asc > a {
+    background-image: url(../img/arrow-active-up.png);
+}
+table.paleblue thead th.sortable.desc > a {
+    background-image: url(../img/arrow-active-down.png);
+}
+
 table.paleblue tr.odd {
     background-color: #EDF3FE;
 }
diff --git a/django_tables/static/django_tables/themes/paleblue/img/arrow-active-down.png b/django_tables/static/django_tables/themes/paleblue/img/arrow-active-down.png
new file mode 100644 (file)
index 0000000..fbf6073
Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/arrow-active-down.png differ
diff --git a/django_tables/static/django_tables/themes/paleblue/img/arrow-active-up.png b/django_tables/static/django_tables/themes/paleblue/img/arrow-active-up.png
new file mode 100644 (file)
index 0000000..e9b0e58
Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/arrow-active-up.png differ
diff --git a/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-down.png b/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-down.png
new file mode 100644 (file)
index 0000000..0c16ab3
Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-down.png differ
diff --git a/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-up.png b/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-up.png
new file mode 100644 (file)
index 0000000..d3f3245
Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/arrow-inactive-up.png differ
index 2a3cebb07217f5b8dd531a5f467ad17cb1d001a4..a65d7f791550669b70cb95ac19d28e0db6372bc0 100644 (file)
@@ -6,8 +6,7 @@ from django.http import Http404
 from django.template.loader import get_template
 from django.template import Context
 from django.utils.encoding import StrAndUnicode
-from .utils import (rmprefix, toggleprefix, OrderByTuple, Accessor,
-                    AttributeDict)
+from .utils import OrderBy, OrderByTuple, Accessor, AttributeDict
 from .columns import Column
 from .rows import Rows, BoundRow
 from .columns import Columns
@@ -181,7 +180,10 @@ class TableOptions(object):
     def __init__(self, options=None):
         super(TableOptions, self).__init__()
         self.sortable = getattr(options, 'sortable', None)
-        self.order_by = getattr(options, 'order_by', ())
+        order_by = getattr(options, 'order_by', ())
+        if isinstance(order_by, basestring):
+            order_by = (order_by, )
+        self.order_by = OrderByTuple(order_by)
         self.attrs = AttributeDict(getattr(options, 'attrs', {}))
 
 
@@ -251,21 +253,15 @@ class Table(StrAndUnicode):
         # accept both string and tuple instructions
         order_by = value.split(',') if isinstance(value, basestring) else value
         order_by = () if order_by is None else order_by
+        new = []
         # validate, raise exception on failure
         for o in order_by:
-            name = rmprefix(o)
-            if name not in self.columns:
-                raise ValueError('Column "%s" can not be used for ordering as '
-                                 'it does not exist in the table' % name)
-            if not self.columns[name].sortable:
-                raise ValueError('Column "%s" can not be used for ordering as '
-                                 'the column has explicitly forbidden it.' %
-                                 name)
-
-        new = OrderByTuple(order_by)
-        if not (hasattr(self, '_order_by') and self._order_by == new):
-            self._order_by = new
-            self._data.order_by(new)
+            name = OrderBy(o).bare
+            if name in self.columns and self.columns[name].sortable:
+                new.append(o)
+        order_by = OrderByTuple(new)
+        self._order_by = order_by
+        self._data.order_by(order_by)
 
     @property
     def rows(self):
index c375ce634276717520bc5bf4342abb57977f2a42..2681a2a3c7837a615e0422af7e3ca64c715b62e9 100644 (file)
@@ -4,7 +4,13 @@
     <thead>
         <tr class="{% cycle "odd" "even" %}">
         {% for column in table.columns %}
-            <th><a href="{% set_url_param sort=column.name_toggled %}">{{ column }}</a></th>
+        {% if column.sortable %}
+            {% with column.order_by as ob %}
+            <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column }}</a></th>
+            {% endwith %}
+        {% else %}
+            <th>{{ column }}</th>
+        {% endif %}
         {% endfor %}
         </tr>
     </thead>
index 5f190627c901d458a3fab813c3f879eea5736b1e..2a6bd834c8cee31c0c806733bfdb66b021712c14 100644 (file)
@@ -9,82 +9,127 @@ from django.template.defaultfilters import escape
 __all__ = ('BaseTable', 'options')
 
 
-def rmprefix(s):
-    """Normalize a column name by removing a potential sort prefix"""
-    return s[1:] if s[:1] == '-' else s
+class OrderBy(str):
+    """A single element in an :class:`OrderByTuple`. This class is essentially
+    just a :class:`str` with some extra properties.
 
+    """
+    @property
+    def bare(self):
+        """Return the bare or naked version. That is, remove a ``-`` prefix if
+        it exists and return the result.
+
+        """
+        return OrderBy(self[1:]) if self[:1] == '-' else self
+
+    @property
+    def opposite(self):
+        """Return the an :class:`OrderBy` object with the opposite sort
+        influence. e.g.
+
+        .. code-block:: python
 
-def toggleprefix(s):
-    """Remove - prefix is existing, or add if missing."""
-    return s[1:] if s[:1] == '-' else '-' + s
+            >>> order_by = OrderBy('name')
+            >>> order_by.opposite
+            '-name'
+
+        """
+        return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self)
+
+    @property
+    def is_descending(self):
+        """Return :const:`True` if this object induces *descending* ordering."""
+        return self.startswith('-')
+
+    @property
+    def is_ascending(self):
+        """Return :const:`True` if this object induces *ascending* ordering."""
+        return not self.is_descending
 
 
 class OrderByTuple(tuple, StrAndUnicode):
-    """Stores 'order by' instructions; Used to render output in a format we
-    understand as input (see __unicode__) - especially useful in templates.
+    """Stores ordering instructions (as :class:`OrderBy` objects). The
+    :attr:`Table.order_by` property is always converted into an
+    :class:`OrderByTuplw` objectUsed to render output in a format we understand
+    as input (see :meth:`~OrderByTuple.__unicode__`) - especially useful in
+    templates.
+
+    It's quite easy to create one of these. Pass in an iterable, and it will
+    automatically convert each element into an :class:`OrderBy` object. e.g.
+
+    .. code-block:: python
+
+        >>> ordering = ('name', '-age')
+        >>> order_by_tuple = OrderByTuple(ordering)
+        >>> age = order_by_tuple['age']
+        >>> age
+        '-age'
+        >>> age.is_descending
+        True
+        >>> age.opposite
+        'age'
 
-    Also supports some functionality to interact with and modify the order.
     """
+    def __new__(cls, iterable):
+        transformed = []
+        for item in iterable:
+            if not isinstance(item, OrderBy):
+                item = OrderBy(item)
+            transformed.append(item)
+        return tuple.__new__(cls, transformed)
+
     def __unicode__(self):
-        """Output in our input format."""
+        """Output in human readable format."""
         return ','.join(self)
 
     def __contains__(self, name):
-        """Determine whether a column is part of this order."""
-        for o in self:
-            if rmprefix(o) == name:
-                return True
-        return False
+        """Determine whether a column is part of this order (i.e. descending
+        prefix agnostic). e.g.
+
+        .. code-block:: python
+
+            >>> ordering = ('name', '-age')
+            >>> order_by_tuple = OrderByTuple(ordering)
+            >>> 'age' in  order_by_tuple
+            True
+            >>> '-age' in order_by_tuple
+            True
 
-    def is_reversed(self, name):
-        """Returns a bool indicating whether the column is ordered reversed,
-        None if it is missing.
         """
         for o in self:
-            if o == '-' + name:
+            if o == name or o.bare == name:
                 return True
         return False
 
-    def is_straight(self, name):
-        """The opposite of is_reversed."""
-        for o in self:
-            if o == name:
-                return True
-        return False
+    def __getitem__(self, index):
+        """Allows an :class:`OrderBy` object to be extracted using
+        :class:`dict`-style indexing in addition to standard 0-based integer
+        indexing. The :class:`dict`-style is prefix agnostic in the same way as
+        :meth:`~OrderByTuple.__contains__`.
 
-    def polarize(self, reverse, names=()):
-        """Return a new tuple with the columns from ``names`` set to "reversed"
-        (e.g. prefixed with a '-'). Note that the name is ambiguous - do not
-        confuse this with ``toggle()``.
+        .. code-block:: python
+
+            >>> ordering = ('name', '-age')
+            >>> order_by_tuple = OrderByTuple(ordering)
+            >>> order_by_tuple['age']
+            '-age'
+            >>> order_by_tuple['-age']
+            '-age'
 
-        If names is not specified, all columns are reversed. If a column name
-        is given that is currently not part of the order, it is added.
-        """
-        prefix = '-' if reverse else ''
-        return OrderByTuple(
-            [o if (names and rmprefix(o) not in names)
-               else prefix + rmprefix(o) for o in self] +
-               [prefix + name for name in names if not name in self]
-        )
-
-    def toggle(self, names=()):
-        """Return a new tuple with the columns from ``names`` toggled with
-        respect to their "reversed" state. E.g. a '-' prefix will be removed is
-        existing, or added if lacking. Do not confuse with ``reverse()``.
-
-        If names is not specified, all columns are toggled. If a column name is
-        given that is currently not part of the order, it is added in
-        non-reverse form.
         """
-        return OrderByTuple(
-            [o if (names and rmprefix(o) not in names)
-               else (o[1:] if o[:1] == '-' else '-' + o) for o in self] +
-               [name for name in names if not name in self]
-        )
+        if isinstance(index, basestring):
+            for ob in self:
+                if ob == index or ob.bare == index:
+                    return ob
+            raise IndexError
+        return tuple.__getitem__(self, index)
 
     @property
     def cmp(self):
-        """Return a function suitable for sorting a list"""
+        """Return a function suitable for sorting a list. This is used for
+        non-:class:`QuerySet` data sources.
+
+        """
         def _cmp(a, b):
             for accessor, reverse in instructions:
                 res = cmp(accessor.resolve(a), accessor.resolve(b))
index ad216cb863ed7aaef730be97518469e26cc7a01b..37726b76b6fee57e3e69ad4d29490c986eb2892a 100644 (file)
@@ -50,9 +50,9 @@ project = u'django-tables'
 # built documents.
 #
 # The short X.Y version.
-version = '0.4.0.alpha4'
+version = '0.4.0.beta'
 # The full version, including alpha/beta/rc tags.
-release = '0.4.0.alpha4'
+release = '0.4.0.beta'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 96ec14863764b1fa51b662fde05f2c706ad1f9d2..25a4401dcd8fb11e390486c1f0fd179d976025bf 100644 (file)
@@ -490,6 +490,20 @@ API Reference
     :members:
 
 
+:class:`OrderBy` Objects
+------------------------
+
+.. autoclass:: django_tables.utils.OrderBy
+    :members:
+
+
+:class:`OrderByTuple` Objects
+-----------------------------
+
+.. autoclass:: django_tables.utils.OrderByTuple
+    :members: __contains__, __getitem__, __unicode__
+
+
 Glossary
 ========
 
index a32ebe10929966a55ea7a498e88ca19fed36078a..f69c18bd1c0748bc0f7c8536c3f38be36c0c63f3 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='django-tables',
-    version='0.4.0.alpha4',
+    version='0.4.0.beta',
     description='Table framework for Django',
 
     author='Bradley Ayers',
index 602f2c4a46d330a8a1d7d6163ccd0a34db5b5b0d..fdb9e0edf38c5b70791635a8c46f5e1a4b786756 100644 (file)
@@ -12,10 +12,14 @@ settings.configure(
     DATABASES = {
         'default': {
             'ENGINE': 'django.db.backends.sqlite3',
+            'NAME': ':memory:',
         }
     },
     INSTALLED_APPS = [
-        'django_tables'
+        #'django.contrib.contenttypes',
+        #'django.contrib.auth',
+        'tests.testapp',
+        'django_tables',
     ]
 )
 
@@ -23,6 +27,7 @@ settings.configure(
 from .core import core
 from .templates import templates
 from .models import models
+from .utils import utils
 
 
-everything = Tests([core, templates, models])
+everything = Tests([core, templates, models, utils])
index 93fbd2da9a360154403dbe0b71065b575764a2aa..e37dee8ab3e176689a4b6a829a8f0ab2a10a2e02 100644 (file)
@@ -94,7 +94,7 @@ def sorting(context):
     # a rewritten order_by is also wrapped
     table = MyUnsortedTable([])
     table.order_by = 'alpha'
-    assert ('alpha',) == table.order_by
+    assert ('alpha', ) == table.order_by
 
     # default sort order can be specified in table options
     class MySortedTable(MyUnsortedTable):
@@ -193,13 +193,5 @@ def pagination():
         books.paginate(Paginator, page='abc', per_page=10)
 
 
-@core.test
-def utilities():
-    assert utils.rmprefix('thing') == 'thing'
-    assert utils.rmprefix('-thing') == 'thing'
-    assert utils.toggleprefix('thing') == '-thing'
-    assert utils.toggleprefix('-thing') == 'thing'
-
-
 if __name__ == '__main__':
     core.main()
index 7a4c9d4e7380b384494d1eb34b4e2601dfae85b4..cfeb176419e898ab88566692ff306d0c33498b5b 100644 (file)
@@ -1,9 +1,9 @@
-from django.contrib.auth.models import User
 from django.conf import settings
 from django.core.paginator import *
 import django_tables as tables
 from django_attest import TestContext
 from attest import Tests
+from .testapp.models import Person
 
 
 models = Tests()
@@ -12,34 +12,23 @@ models.context(TestContext())
 
 @models.context
 def samples():
-    class Context(object):
-        class UserTable(tables.Table):
-            username = tables.Column()
-            first_name = tables.Column()
-            last_name = tables.Column()
-            email = tables.Column()
-            password = tables.Column()
-            is_staff = tables.Column()
-            is_active = tables.Column()
-            is_superuser = tables.Column()
-            last_login = tables.Column()
-            date_joined = tables.Column()
+    class PersonTable(tables.Table):
+        first_name = tables.Column()
+        last_name = tables.Column()
 
     # we're going to test against User, so let's create a few
-    User.objects.create_user('fake-user-1', 'fake-1@example.com', 'password')
-    User.objects.create_user('fake-user-2', 'fake-2@example.com', 'password')
-    User.objects.create_user('fake-user-3', 'fake-3@example.com', 'password')
-    User.objects.create_user('fake-user-4', 'fake-4@example.com', 'password')
+    Person.objects.create(first_name='Bradley', last_name='Ayers')
+    Person.objects.create(first_name='Chris',   last_name='Doble')
 
-    yield Context
+    yield PersonTable
 
 
 @models.test
-def simple(dj, samples):
-    users = User.objects.all()
-    table = samples.UserTable(users)
+def simple(client, UserTable):
+    queryset = Person.objects.all()
+    table = PersonTable(queryset)
 
     for index, row in enumerate(table.rows):
-        user = users[index]
-        Assert(user.username) == row['username']
-        Assert(user.email) == row['email']
+        person = queryset[index]
+        Assert(person.username) == row['first_name']
+        Assert(person.email) == row['last_name']
diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/testapp/models.py b/tests/testapp/models.py
new file mode 100644 (file)
index 0000000..48d828e
--- /dev/null
@@ -0,0 +1,6 @@
+from django.db import models
+
+
+class Person(models.Model):
+    first_name = models.CharField(max_length=200)
+    last_name = models.CharField(max_length=200)
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644 (file)
index 0000000..f2d7f3b
--- /dev/null
@@ -0,0 +1,35 @@
+# -*- coding: utf8 -*-
+from django_tables.utils import OrderByTuple, OrderBy
+from attest import Tests, Assert
+
+
+utils = Tests()
+
+
+@utils.test
+def orderbytuple():
+    obt = OrderByTuple('abc')
+    Assert(obt) == (OrderBy('a'), OrderBy('b'), OrderBy('c'))
+    Assert(obt[0]) == OrderBy('a')
+    Assert(obt['b']) == OrderBy('b')
+    with Assert.raises(IndexError) as error:
+        obt['d']
+    with Assert.raises(TypeError) as error:
+        obt[('tuple', )]
+
+
+@utils.test
+def orderby():
+    a = OrderBy('a')
+    Assert('a') == a
+    Assert('a') == a.bare
+    Assert('-a') == a.opposite
+    Assert(True) == a.is_ascending
+    Assert(False) == a.is_descending
+
+    b = OrderBy('-b')
+    Assert('-b') == b
+    Assert('b') == b.bare
+    Assert('b') == b.opposite
+    Assert(True) == b.is_descending
+    Assert(False) == b.is_ascending