* Updated documentation with information about OrderByTuple and OrderBy.
* Bumped version to v0.4.0.beta
* Updated paleblue theme with ordering icons.
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):
"""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)
"""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.
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;
}
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
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', {}))
# 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):
<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>
__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))
# 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.
:members:
+:class:`OrderBy` Objects
+------------------------
+
+.. autoclass:: django_tables.utils.OrderBy
+ :members:
+
+
+:class:`OrderByTuple` Objects
+-----------------------------
+
+.. autoclass:: django_tables.utils.OrderByTuple
+ :members: __contains__, __getitem__, __unicode__
+
+
Glossary
========
setup(
name='django-tables',
- version='0.4.0.alpha4',
+ version='0.4.0.beta',
description='Table framework for Django',
author='Bradley Ayers',
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:',
}
},
INSTALLED_APPS = [
- 'django_tables'
+ #'django.contrib.contenttypes',
+ #'django.contrib.auth',
+ 'tests.testapp',
+ 'django_tables',
]
)
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])
# 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):
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()
-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()
@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']
--- /dev/null
+from django.db import models
+
+
+class Person(models.Model):
+ first_name = models.CharField(max_length=200)
+ last_name = models.CharField(max_length=200)
--- /dev/null
+# -*- 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