From: Bradley Ayers Date: Mon, 4 Apr 2011 05:12:09 +0000 (+1000) Subject: * Added pagination X-Git-Tag: v0.4.0.beta3 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=2ea21b445ec12cdbfbe31baa8227a7930bbb1f94;p=django-tables2.git * Added pagination * TemplateColumn now renders with a RequestContext if using {% render_table %} * Column accessors no longer need to successfully resolve. If the resolve fails, the default value will be used. --- diff --git a/django_tables/columns.py b/django_tables/columns.py index 24001b5..9bb29e9 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -4,7 +4,7 @@ from django.utils.encoding import force_unicode, StrAndUnicode from django.utils.datastructures import SortedDict from django.utils.text import capfirst from django.utils.safestring import mark_safe -from django.template import Context, Template +from django.template import RequestContext, Context, Template from .utils import OrderBy, A, AttributeDict @@ -119,7 +119,15 @@ class CheckBoxColumn(Column): used as the value for the checkbox, i.e. ```` - By default this column is not sortable. + This class implements some sensible defaults: + + - The ``name`` attribute of the input is the name of the :term:`column + name` (can be overriden via ``attrs`` argument). + - The ``sortable`` parameter defaults to :const:`False`. + - The ``type`` attribute of the input is ``checkbox`` (can be overriden via + ``attrs`` argument). + - The header checkbox is left bare, i.e. ```` (use + the ``header_attrs`` argument to customise). .. note:: The "apply some operation onto the selection" functionality is not implemented in this column, and requires manually implemention. @@ -127,17 +135,24 @@ class CheckBoxColumn(Column): :param attrs: a :class:`dict` of HTML attributes that are added to the rendered ```` tag + :param header_attrs: + same as *attrs*, but applied **only** to the header checkbox """ - def __init__(self, attrs=None, **extra): + def __init__(self, attrs=None, header_attrs=None, **extra): params = {'sortable': False} params.update(extra) super(CheckBoxColumn, self).__init__(**params) self.attrs = attrs or {} + self.header_attrs = header_attrs or {} @property def header(self): - return mark_safe('') + attrs = AttributeDict({ + 'type': 'checkbox', + }) + attrs.update(self.header_attrs) + return mark_safe('' % attrs.as_html()) def render(self, value, bound_column, **kwargs): attrs = AttributeDict({ @@ -261,14 +276,24 @@ class TemplateColumn(Column): Both columns will have the same output. + + .. important:: + In order to use template tags or filters that require a + ``RequestContext``, the table **must** be rendered via + :ref:`{% render_table %} `. + """ def __init__(self, template_code=None, **extra): super(TemplateColumn, self).__init__(**extra) self.template_code = template_code - def render(self, record, **kwargs): + def render(self, record, table, **kwargs): t = Template(self.template_code) - return t.render(Context({'record': record})) + if hasattr(table, 'request'): + context = RequestContext(table.request, {'record': record}) + else: + context = Context({'record': record}) + return t.render(context) class BoundColumn(object): @@ -334,7 +359,7 @@ class BoundColumn(object): column. """ - return self.verbose_name + return self.column.header or self.verbose_name @property def name(self): diff --git a/django_tables/proxies.py b/django_tables/proxies.py new file mode 100644 index 0000000..95b919d --- /dev/null +++ b/django_tables/proxies.py @@ -0,0 +1,154 @@ +from django.utils.functional import Promise + + +class AbstractProxy(object): + """Delegates all operations (except ``.__subject__``) to another object""" + __slots__ = () + + #def __call__(self, *args, **kw): + # return self.__subject__(*args, **kw) + + def __getattribute__(self, attr, oga=object.__getattribute__): + subject = oga(self,'__subject__') + if attr=='__subject__': + return subject + return getattr(subject,attr) + + def __setattr__(self, attr, val, osa=object.__setattr__): + if attr == '__subject__': + osa(self, attr, val) + else: + setattr(self.__subject__, attr, val) + + def __delattr__(self, attr, oda=object.__delattr__): + if attr=='__subject__': + oda(self,attr) + else: + delattr(self.__subject__, attr) + + def __nonzero__(self): + return bool(self.__subject__) + + def __getitem__(self, arg): + return self.__subject__[arg] + + def __setitem__(self, arg, val): + self.__subject__[arg] = val + + def __delitem__(self, arg): + del self.__subject__[arg] + + def __getslice__(self, i, j): + return self.__subject__[i:j] + + + def __setslice__(self, i, j, val): + self.__subject__[i:j] = val + + def __delslice__(self, i, j): + del self.__subject__[i:j] + + def __contains__(self, ob): + return ob in self.__subject__ + + for name in 'repr str hash len abs complex int long float iter oct hex'.split(): + exec "def __%s__(self): return %s(self.__subject__)" % (name, name) + + for name in 'cmp', 'coerce', 'divmod': + exec "def __%s__(self,ob): return %s(self.__subject__,ob)" % (name, name) + + for name, op in [ + ('lt','<'), ('gt','>'), ('le','<='), ('ge','>='), + ('eq','=='), ('ne','!=') + ]: + exec "def __%s__(self,ob): return self.__subject__ %s ob" % (name, op) + + for name, op in [('neg','-'), ('pos','+'), ('invert','~')]: + exec "def __%s__(self): return %s self.__subject__" % (name, op) + + for name, op in [ + ('or','|'), ('and','&'), ('xor','^'), ('lshift','<<'), ('rshift','>>'), + ('add','+'), ('sub','-'), ('mul','*'), ('div','/'), ('mod','%'), + ('truediv','/'), ('floordiv','//') + ]: + exec ( + "def __%(name)s__(self,ob):\n" + " return self.__subject__ %(op)s ob\n" + "\n" + "def __r%(name)s__(self,ob):\n" + " return ob %(op)s self.__subject__\n" + "\n" + "def __i%(name)s__(self,ob):\n" + " self.__subject__ %(op)s=ob\n" + " return self\n" + ) % locals() + + del name, op + + # Oddball signatures + + def __rdivmod__(self,ob): + return divmod(ob, self.__subject__) + + def __pow__(self, *args): + return pow(self.__subject__, *args) + + def __ipow__(self, ob): + self.__subject__ **= ob + return self + + def __rpow__(self, ob): + return pow(ob, self.__subject__) + + +class ObjectProxy(AbstractProxy): + """Proxy for a specific object""" + + __slots__ = "__subject__" + + def __init__(self, subject): + self.__subject__ = subject + + +class CallbackProxy(AbstractProxy): + """Proxy for a dynamically-chosen object""" + + __slots__ = '__callback__' + + def __init__(self, func): + set_callback(self, func) + +set_callback = CallbackProxy.__callback__.__set__ +get_callback = CallbackProxy.__callback__.__get__ +CallbackProxy.__subject__ = property(lambda self, gc=get_callback: gc(self)()) + + +class LazyProxy(CallbackProxy): + """Proxy for a lazily-obtained object, that is cached on first use""" + __slots__ = "__cache__" + +get_cache = LazyProxy.__cache__.__get__ +set_cache = LazyProxy.__cache__.__set__ + +def __subject__(self, get_cache=get_cache, set_cache=set_cache): + try: + return get_cache(self) + except AttributeError: + set_cache(self, get_callback(self)()) + return get_cache(self) + +LazyProxy.__subject__ = property(__subject__, set_cache) +del __subject__ + + +class TemplateSafeLazyProxy(LazyProxy): + """ + A version of LazyProxy suitable for use in Django templates. + + It's important that an ``alters_data`` attribute returns :const:`False`. + + """ + def __getattribute__(self, attr, *args, **kwargs): + if attr == 'alters_data': + return False + return LazyProxy.__getattribute__(self, attr, *args, **kwargs) diff --git a/django_tables/rows.py b/django_tables/rows.py index 6fab202..1b662b9 100644 --- a/django_tables/rows.py +++ b/django_tables/rows.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- from django.utils.safestring import EscapeUnicode, SafeData +from .proxies import TemplateSafeLazyProxy +import itertools class BoundRow(object): - """Represents a *specific* row in a table. + """ + Represents a *specific* row in a table. - :class:`BoundRow` objects are a container that make it easy to access the + :class:`.BoundRow` objects are a container that make it easy to access the final 'rendered' values for cells in a row. You can simply iterate over a - :class:`BoundRow` object and it will take care to return values rendered - using the correct method (e.g. :meth:`Column.render_FOO`) + :class:`.BoundRow` object and it will take care to return values rendered + using the correct method (e.g. :meth:`.Column.render_FOO`) To access the rendered value of each cell in a row, just iterate over it: @@ -55,37 +58,36 @@ class BoundRow(object): ... KeyError: 'c' + :param table: is the :class:`Table` in which this row exists. + :param record: a single record from the :term:`table data` that is used to + populate the row. A record could be a :class:`Model` object, a + :class:`dict`, or something else. + """ def __init__(self, table, record): - """Initialise a new :class:`BoundRow` object where: - - * *table* is the :class:`Table` in which this row exists. - * *record* is a single record from the data source that is posed to - populate the row. A record could be a :class:`Model` object, a - ``dict``, or something else. - - """ self._table = table self._record = record @property def table(self): - """The associated :term:`table`.""" + """The associated :class:`.Table` object.""" return self._table @property def record(self): - """The data record from the data source which is used to populate this - row with data. + """ + The data record from the data source which is used to populate this row + with data. """ return self._record def __iter__(self): - """Iterate over the rendered values for cells in the row. + """ + Iterate over the rendered values for cells in the row. - Under the hood this method just makes a call to :meth:`__getitem__` for - each cell. + Under the hood this method just makes a call to + :meth:`.BoundRow.__getitem__` for each cell. """ for column in self.table.columns: @@ -94,14 +96,22 @@ class BoundRow(object): yield self[column.name] def __getitem__(self, name): - """Returns the final rendered value for a cell in the row, given the - name of a column. + """ + Returns the final rendered value for a cell in the row, given the name + of a column. """ bound_column = self.table.columns[name] - raw = bound_column.accessor.resolve(self.record) + + def value(): + try: + raw = bound_column.accessor.resolve(self.record) + except (TypeError, AttributeError, KeyError, ValueError) as e: + raw = None + return raw if raw is not None else bound_column.default + kwargs = { - 'value': raw if raw is not None else bound_column.default, + 'value': TemplateSafeLazyProxy(value), 'record': self.record, 'column': bound_column.column, 'bound_column': bound_column, @@ -137,22 +147,20 @@ class BoundRows(object): """ Container for spawning :class:`.BoundRow` objects. - The :attr:`.tables.Table.rows` attribute is a :class:`.BoundRows` object. + The :attr:`.Table.rows` attribute is a :class:`.BoundRows` object. It provides functionality that would not be possible with a simple iterator in the table class. + :type table: :class:`.Table` object + :param table: the table in which the rows exist. + """ def __init__(self, table): - """ - Initialise a :class:`Rows` object. *table* is the :class:`Table` object - in which the rows exist. - - """ self.table = table def all(self): """ - Return an iterable for all :class:`BoundRow` objects in the table. + Return an iterable for all :class:`.BoundRow` objects in the table. """ for record in self.table.data: @@ -161,8 +169,9 @@ class BoundRows(object): def page(self): """ If the table is paginated, return an iterable of :class:`.BoundRow` - objects that appear on the current page, otherwise :const:`None`. + objects that appear on the current page. + :rtype: iterable of :class:`.BoundRow` objects, or :const:`None`. """ if not hasattr(self.table, 'page'): return None @@ -182,10 +191,8 @@ class BoundRows(object): def __getitem__(self, key): """Allows normal list slicing syntax to be used.""" if isinstance(key, slice): - result = list() - for row in self.table.data[key]: - result.append(BoundRow(self.table, row)) - return result + return itertools.imap(lambda record: BoundRow(self.table, record), + self.table.data[key]) elif isinstance(key, int): return BoundRow(self.table, self.table.data[key]) else: diff --git a/django_tables/static/django_tables/themes/paleblue/css/screen.css b/django_tables/static/django_tables/themes/paleblue/css/screen.css index edede86..767e041 100644 --- a/django_tables/static/django_tables/themes/paleblue/css/screen.css +++ b/django_tables/static/django_tables/themes/paleblue/css/screen.css @@ -6,7 +6,8 @@ table.paleblue { } table.paleblue a:link, -table.paleblue a:visited { +table.paleblue a:visited, +table.paleblue + ul.pagination > li > a { color: #5B80B2; text-decoration: none; font-weight: bold; @@ -31,7 +32,7 @@ table.paleblue thead td:first-child { table.paleblue thead th, table.paleblue thead td { - background: #FCFCFC url(../img/nav-bg.gif) top left repeat-x; + background: #FCFCFC url(../img/header-bg.gif) top left repeat-x; border-bottom: 1px solid #DDD; padding: 2px 5px; font-size: 11px; @@ -64,3 +65,24 @@ table.paleblue tr.odd { table.paleblue tr.even { background-color: white; } + +table.paleblue + ul.pagination { + background: white url(../img/pagination-bg.gif) left 180% repeat-x; + overflow: auto; + padding: 10px; + border: 1px solid #DDD; +} + +table.paleblue + ul.pagination > li { + float: left; + line-height: 22px; + margin-left: 10px; +} + +table.paleblue + ul.pagination > li:first-child { + margin-left: 0; +} + +div.table-container { + display: inline-block; +} diff --git a/django_tables/static/django_tables/themes/paleblue/img/nav-bg.gif b/django_tables/static/django_tables/themes/paleblue/img/header-bg.gif similarity index 100% rename from django_tables/static/django_tables/themes/paleblue/img/nav-bg.gif rename to django_tables/static/django_tables/themes/paleblue/img/header-bg.gif diff --git a/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif b/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif new file mode 100644 index 0000000..f8402b8 Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif differ diff --git a/django_tables/tables.py b/django_tables/tables.py index cba5d5f..0b3efa0 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -27,18 +27,12 @@ class TableData(object): if isinstance(data, QuerySet): self.queryset = data elif isinstance(data, list): - self.list = data + self.list = data[:] else: raise ValueError('data must be a list or QuerySet object, not %s' % data.__class__.__name__) self._table = table - # work with a copy of the data that has missing values populated with - # defaults. - if hasattr(self, 'list'): - self.list = copy.copy(self.list) - self._populate_missing_values(self.list) - def __len__(self): # Use the queryset count() method to get the length, instead of # loading all results into memory. This allows, for example, @@ -77,41 +71,6 @@ class TableData(object): translated.append(prefix + column.accessor) return OrderByTuple(translated) - def _populate_missing_values(self, data): - """ - Populates self._data with missing values based on the default value - for each column. It will create new items in the dataset (not modify - existing ones). - - """ - for i, item in enumerate(data): - # add data that is missing from the source. we do this now - # so that the column's ``default`` values can affect - # sorting (even when callables are used)! - # - # This is a design decision - the alternative would be to - # resolve the values when they are accessed, and either do - # not support sorting them at all, or run the callables - # during sorting. - modified_item = None - for bound_column in self._table.columns.all(): - # the following will be True if: - # * the source does not provide a value for the column - # or the value is None - # * the column did provide a data callable that - # returned None - accessor = Accessor(bound_column.accessor) - try: - if accessor.resolve(item) is None: # may raise ValueError - raise ValueError('None values also need replacing') - except ValueError: - if modified_item is None: - modified_item = copy.copy(item) - modified_item[accessor.bits[0]] = bound_column.default - if modified_item is not None: - data[i] = modified_item - - def __getitem__(self, index): return (self.list if hasattr(self, 'list') else self.queryset)[index] @@ -230,8 +189,10 @@ class Table(StrAndUnicode): @order_by.setter def order_by(self, value): - """Order the rows of the table based columns. ``value`` must be a - sequence of column names. + """ + Order the rows of the table based columns. ``value`` must be a sequence + of column names. + """ # accept both string and tuple instructions order_by = value.split(',') if isinstance(value, basestring) else value @@ -271,13 +232,13 @@ class Table(StrAndUnicode): """The attributes that should be applied to the ```` tag when rendering HTML. - :returns: :class:`~.utils.AttributeDict` object. + :rtype: :class:`~.utils.AttributeDict` object. """ return self._meta.attrs - def paginate(self, klass=Paginator, page=1, *args, **kwargs): - self.paginator = klass(self.rows, *args, **kwargs) + def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs): + self.paginator = klass(self.rows, per_page, *args, **kwargs) try: self.page = self.paginator.page(page) except Exception as e: diff --git a/django_tables/templates/django_tables/table.html b/django_tables/templates/django_tables/table.html index 9ecf79a..c6d12c0 100644 --- a/django_tables/templates/django_tables/table.html +++ b/django_tables/templates/django_tables/table.html @@ -1,5 +1,8 @@ {% spaceless %} {% load django_tables %} +{% if table.page %} +
+{% endif %}
@@ -9,13 +12,13 @@ {% endwith %} {% else %} - + {% endif %} {% endfor %} - {% for row in table.rows %} + {% for row in table.page.object_list|default:table.rows %} {# support pagination #} {% for cell in row %} @@ -24,4 +27,17 @@ {% endfor %}
{{ column.header }}{{ column.verbose_name }}{{ column.header }}
{{ cell }}
+{% if table.page %} +