)\r
\r
\r
-class BaseTable(object):\r
- """A collection of columns, plus their associated data rows.\r
- """\r
-\r
- __metaclass__ = DeclarativeColumnsMetaclass\r
-\r
- def __init__(self, data, order_by=None):\r
- """Create a new table instance with the iterable ``data``.\r
-\r
- If ``order_by`` is specified, the data will be sorted accordingly.\r
-\r
- Note that unlike a ``Form``, tables are always bound to data. Also\r
- unlike a form, the ``columns`` attribute is read-only and returns\r
- ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
- when iterating over a form. This is because the table iterator\r
- already yields rows, and we need an attribute via which to expose\r
- the (visible) set of (bound) columns - ``Table.columns`` is simply\r
- the perfect fit for this. Instead, ``base_colums`` is copied to\r
- table instances, so modifying that will not touch the class-wide\r
- column list.\r
- """\r
- self._data = data\r
- self._snapshot = None # will store output dataset (ordered...)\r
- self._rows = Rows(self)\r
- self._columns = Columns(self)\r
-\r
- self.order_by = order_by\r
-\r
- # Make a copy so that modifying this will not touch the class\r
- # definition. Note that this is different from forms, where the\r
- # copy is made available in a ``fields`` attribute. See the\r
- # ``Table`` class docstring for more information.\r
- self.base_columns = copy.deepcopy(type(self).base_columns)\r
-\r
- def _build_snapshot(self):\r
- """Rebuild the table for the current set of options.\r
-\r
- Whenver the table options change, e.g. say a new sort order,\r
- this method will be asked to regenerate the actual table from\r
- the linked data source.\r
-\r
- Subclasses should override this.\r
- """\r
- self._snapshot = copy.copy(self._data)\r
-\r
- def _get_data(self):\r
- if self._snapshot is None:\r
- self._build_snapshot()\r
- return self._snapshot\r
- data = property(lambda s: s._get_data())\r
-\r
- def _resolve_sort_directions(self, order_by):\r
- """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
- according to each column's ``direction`` option, e.g. it translates\r
- between the ascending/descending and the straight/reverse terminology.\r
- """\r
- result = []\r
- for inst in order_by:\r
- if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
- inst = toggleprefix(inst)\r
- result.append(inst)\r
- return result\r
-\r
- def _cols_to_fields(self, names):\r
- """Utility function. Given a list of column names (as exposed to\r
- the user), converts column names to the names we have to use to\r
- retrieve a column's data from the source.\r
-\r
- Usually, the name used in the table declaration is used for accessing\r
- the source (while a column can define an alias-like name that will\r
- be used to refer to it from the "outside"). However, a column can\r
- override this by giving a specific source field name via ``data``.\r
-\r
- Supports prefixed column names as used e.g. in order_by ("-field").\r
- """\r
- result = []\r
- for ident in names:\r
- # handle order prefix\r
- if ident[:1] == '-':\r
- name = ident[1:]\r
- prefix = '-'\r
- else:\r
- name = ident\r
- prefix = ''\r
- # find the field name\r
- column = self.columns[name]\r
- if column.column.data and not callable(column.column.data):\r
- name_in_source = column.column.data\r
- else:\r
- name_in_source = column.declared_name\r
- result.append(prefix + name_in_source)\r
- return result\r
-\r
- def _validate_column_name(self, name, purpose):\r
- """Return True/False, depending on whether the column ``name`` is\r
- valid for ``purpose``. Used to validate things like ``order_by``\r
- instructions.\r
-\r
- Can be overridden by subclasses to impose further restrictions.\r
- """\r
- if purpose == 'order_by':\r
- return name in self.columns and\\r
- self.columns[name].sortable\r
- else:\r
- return True\r
-\r
- def _set_order_by(self, value):\r
- if self._snapshot is not None:\r
- self._snapshot = None\r
- # accept both string and tuple instructions\r
- order_by = (isinstance(value, basestring) \\r
- and [value.split(',')] \\r
- or [value])[0]\r
- if order_by:\r
- # validate, remove all invalid order instructions\r
- validated_order_by = []\r
- for o in order_by:\r
- if self._validate_column_name(rmprefix(o), "order_by"):\r
- validated_order_by.append(o)\r
- elif not options.IGNORE_INVALID_OPTIONS:\r
- raise ValueError('Column name %s is invalid.' % o)\r
- self._order_by = OrderByTuple(validated_order_by)\r
- else:\r
- self._order_by = OrderByTuple()\r
- order_by = property(lambda s: s._order_by, _set_order_by)\r
-\r
- def __unicode__(self):\r
- return self.as_html()\r
-\r
- def __iter__(self):\r
- for row in self.rows:\r
- yield row\r
-\r
- def __getitem__(self, key):\r
- return self.rows[key]\r
-\r
- # just to make those readonly\r
- columns = property(lambda s: s._columns)\r
- rows = property(lambda s: s._rows)\r
-\r
- def as_html(self):\r
- pass\r
-\r
- def update(self):\r
- """Update the table based on it's current options.\r
-\r
- Normally, you won't have to call this method, since the table\r
- updates itself (it's caches) automatically whenever you change\r
- any of the properties. However, in some rare cases those\r
- changes might not be picked up, for example if you manually\r
- change ``base_columns`` or any of the columns in it.\r
- """\r
- self._build_snapshot()\r
-\r
- def paginate(self, klass, *args, **kwargs):\r
- page = kwargs.pop('page', 1)\r
- self.paginator = klass(self.rows, *args, **kwargs)\r
- try:\r
- self.page = self.paginator.page(page)\r
- except paginator.InvalidPage, e:\r
- raise Http404(str(e))\r
-\r
-\r
class Columns(object):\r
"""Container for spawning BoundColumns.\r
\r
def as_html(self):\r
pass\r
\r
+\r
+class BoundRow(object):\r
+ """Represents a single row of data, bound to a table.\r
+\r
+ Tables will spawn these row objects, wrapping around the actual data\r
+ stored in a row.\r
+ """\r
+ def __init__(self, table, data):\r
+ self.table = table\r
+ self.data = data\r
+\r
+ def __iter__(self):\r
+ for value in self.values:\r
+ yield value\r
+\r
+ def __getitem__(self, name):\r
+ """Returns this row's value for a column. All other access methods,\r
+ e.g. __iter__, lead ultimately to this."""\r
+\r
+ # We are supposed to return ``name``, but the column might be\r
+ # named differently in the source data.\r
+ result = self.data[self.table._cols_to_fields([name])[0]]\r
+\r
+ # if the field we are pointing to is a callable, remove it\r
+ if callable(result):\r
+ result = result(self)\r
+ return result\r
+\r
+ def __contains__(self, item):\r
+ """Check by both row object and column name."""\r
+ if isinstance(item, basestring):\r
+ return item in self.table._columns\r
+ else:\r
+ return item in self\r
+\r
+ def _get_values(self):\r
+ for column in self.table.columns:\r
+ yield self[column.name]\r
+ values = property(_get_values)\r
+\r
+ def as_html(self):\r
+ pass\r
+\r
+\r
class Rows(object):\r
"""Container for spawning BoundRows.\r
\r
provides functionality that would not be possible with a simple\r
iterator in the table class.\r
"""\r
- def __init__(self, table, row_klass=None):\r
+\r
+ row_class = BoundRow\r
+\r
+ def __init__(self, table):\r
self.table = table\r
- self.row_klass = row_klass and row_klass or BoundRow\r
\r
def _reset(self):\r
pass # we currently don't use a cache\r
def all(self):\r
"""Return all rows."""\r
for row in self.table.data:\r
- yield self.row_klass(self.table, row)\r
+ yield self.row_class(self.table, row)\r
\r
def page(self):\r
"""Return rows on current page (if paginated)."""\r
if isinstance(key, slice):\r
result = list()\r
for row in self.table.data[key]:\r
- result.append(self.row_klass(self.table, row))\r
+ result.append(self.row_class(self.table, row))\r
return result\r
elif isinstance(key, int):\r
- return self.row_klass(self.table, self.table.data[key])\r
+ return self.row_class(self.table, self.table.data[key])\r
else:\r
raise TypeError('Key must be a slice or integer.')\r
\r
-class BoundRow(object):\r
- """Represents a single row of data, bound to a table.\r
\r
- Tables will spawn these row objects, wrapping around the actual data\r
- stored in a row.\r
+class BaseTable(object):\r
+ """A collection of columns, plus their associated data rows.\r
"""\r
- def __init__(self, table, data):\r
- self.table = table\r
- self.data = data\r
\r
- def __iter__(self):\r
- for value in self.values:\r
- yield value\r
+ __metaclass__ = DeclarativeColumnsMetaclass\r
\r
- def __getitem__(self, name):\r
- """Returns this row's value for a column. All other access methods,\r
- e.g. __iter__, lead ultimately to this."""\r
+ rows_class = Rows\r
\r
- # We are supposed to return ``name``, but the column might be\r
- # named differently in the source data.\r
- result = self.data[self.table._cols_to_fields([name])[0]]\r
+ def __init__(self, data, order_by=None):\r
+ """Create a new table instance with the iterable ``data``.\r
\r
- # if the field we are pointing to is a callable, remove it\r
- if callable(result):\r
- result = result(self)\r
+ If ``order_by`` is specified, the data will be sorted accordingly.\r
+\r
+ Note that unlike a ``Form``, tables are always bound to data. Also\r
+ unlike a form, the ``columns`` attribute is read-only and returns\r
+ ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
+ when iterating over a form. This is because the table iterator\r
+ already yields rows, and we need an attribute via which to expose\r
+ the (visible) set of (bound) columns - ``Table.columns`` is simply\r
+ the perfect fit for this. Instead, ``base_colums`` is copied to\r
+ table instances, so modifying that will not touch the class-wide\r
+ column list.\r
+ """\r
+ self._data = data\r
+ self._snapshot = None # will store output dataset (ordered...)\r
+ self._rows = self.rows_class(self)\r
+ self._columns = Columns(self)\r
+\r
+ self.order_by = order_by\r
+\r
+ # Make a copy so that modifying this will not touch the class\r
+ # definition. Note that this is different from forms, where the\r
+ # copy is made available in a ``fields`` attribute. See the\r
+ # ``Table`` class docstring for more information.\r
+ self.base_columns = copy.deepcopy(type(self).base_columns)\r
+\r
+ def _build_snapshot(self):\r
+ """Rebuild the table for the current set of options.\r
+\r
+ Whenver the table options change, e.g. say a new sort order,\r
+ this method will be asked to regenerate the actual table from\r
+ the linked data source.\r
+\r
+ Subclasses should override this.\r
+ """\r
+ self._snapshot = copy.copy(self._data)\r
+\r
+ def _get_data(self):\r
+ if self._snapshot is None:\r
+ self._build_snapshot()\r
+ return self._snapshot\r
+ data = property(lambda s: s._get_data())\r
+\r
+ def _resolve_sort_directions(self, order_by):\r
+ """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
+ according to each column's ``direction`` option, e.g. it translates\r
+ between the ascending/descending and the straight/reverse terminology.\r
+ """\r
+ result = []\r
+ for inst in order_by:\r
+ if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
+ inst = toggleprefix(inst)\r
+ result.append(inst)\r
return result\r
\r
- def __contains__(self, item):\r
- """Check by both row object and column name."""\r
- if isinstance(item, basestring):\r
- return item in self.table._columns\r
+ def _cols_to_fields(self, names):\r
+ """Utility function. Given a list of column names (as exposed to\r
+ the user), converts column names to the names we have to use to\r
+ retrieve a column's data from the source.\r
+\r
+ Usually, the name used in the table declaration is used for accessing\r
+ the source (while a column can define an alias-like name that will\r
+ be used to refer to it from the "outside"). However, a column can\r
+ override this by giving a specific source field name via ``data``.\r
+\r
+ Supports prefixed column names as used e.g. in order_by ("-field").\r
+ """\r
+ result = []\r
+ for ident in names:\r
+ # handle order prefix\r
+ if ident[:1] == '-':\r
+ name = ident[1:]\r
+ prefix = '-'\r
+ else:\r
+ name = ident\r
+ prefix = ''\r
+ # find the field name\r
+ column = self.columns[name]\r
+ if column.column.data and not callable(column.column.data):\r
+ name_in_source = column.column.data\r
+ else:\r
+ name_in_source = column.declared_name\r
+ result.append(prefix + name_in_source)\r
+ return result\r
+\r
+ def _validate_column_name(self, name, purpose):\r
+ """Return True/False, depending on whether the column ``name`` is\r
+ valid for ``purpose``. Used to validate things like ``order_by``\r
+ instructions.\r
+\r
+ Can be overridden by subclasses to impose further restrictions.\r
+ """\r
+ if purpose == 'order_by':\r
+ return name in self.columns and\\r
+ self.columns[name].sortable\r
else:\r
- return item in self\r
+ return True\r
\r
- def _get_values(self):\r
- for column in self.table.columns:\r
- yield self[column.name]\r
- values = property(_get_values)\r
+ def _set_order_by(self, value):\r
+ if self._snapshot is not None:\r
+ self._snapshot = None\r
+ # accept both string and tuple instructions\r
+ order_by = (isinstance(value, basestring) \\r
+ and [value.split(',')] \\r
+ or [value])[0]\r
+ if order_by:\r
+ # validate, remove all invalid order instructions\r
+ validated_order_by = []\r
+ for o in order_by:\r
+ if self._validate_column_name(rmprefix(o), "order_by"):\r
+ validated_order_by.append(o)\r
+ elif not options.IGNORE_INVALID_OPTIONS:\r
+ raise ValueError('Column name %s is invalid.' % o)\r
+ self._order_by = OrderByTuple(validated_order_by)\r
+ else:\r
+ self._order_by = OrderByTuple()\r
+ order_by = property(lambda s: s._order_by, _set_order_by)\r
+\r
+ def __unicode__(self):\r
+ return self.as_html()\r
+\r
+ def __iter__(self):\r
+ for row in self.rows:\r
+ yield row\r
+\r
+ def __getitem__(self, key):\r
+ return self.rows[key]\r
+\r
+ # just to make those readonly\r
+ columns = property(lambda s: s._columns)\r
+ rows = property(lambda s: s._rows)\r
\r
def as_html(self):\r
- pass
\ No newline at end of file
+ pass\r
+\r
+ def update(self):\r
+ """Update the table based on it's current options.\r
+\r
+ Normally, you won't have to call this method, since the table\r
+ updates itself (it's caches) automatically whenever you change\r
+ any of the properties. However, in some rare cases those\r
+ changes might not be picked up, for example if you manually\r
+ change ``base_columns`` or any of the columns in it.\r
+ """\r
+ self._build_snapshot()\r
+\r
+ def paginate(self, klass, *args, **kwargs):\r
+ page = kwargs.pop('page', 1)\r
+ self.paginator = klass(self.rows, *args, **kwargs)\r
+ try:\r
+ self.page = self.paginator.page(page)\r
+ except paginator.InvalidPage, e:\r
+ raise Http404(str(e))\r
field_list.append((f.name, column))\r
return SortedDict(field_list)\r
\r
+\r
+class BoundModelRow(BoundRow):\r
+ """Special version of the BoundRow class that can handle model instances\r
+ as data.\r
+\r
+ We could simply have ModelTable spawn the normal BoundRow objects\r
+ with the instance converted to a dict instead. However, this way allows\r
+ us to support non-field attributes and methods on the model as well.\r
+ """\r
+ def __getitem__(self, name):\r
+ """Overridden. Return this row's data for a certain column, with\r
+ custom handling for model tables.\r
+ """\r
+\r
+ # find the column for the requested field, for reference\r
+ boundcol = self.table._columns[name]\r
+\r
+ # If the column has a name override (we know then that is was also\r
+ # used for access, e.g. if the condition is true, then\r
+ # ``boundcol.column.name == name``), we need to make sure we use the\r
+ # declaration name to access the model field.\r
+ if boundcol.column.data:\r
+ if callable(boundcol.column.data):\r
+ result = boundcol.column.data(self)\r
+ if not result:\r
+ if boundcol.column.default is not None:\r
+ return boundcol.get_default(self)\r
+ return result\r
+ else:\r
+ name = boundcol.column.data\r
+ else:\r
+ name = boundcol.declared_name\r
+\r
+\r
+ # try to resolve relationships spanning attributes\r
+ bits = name.split('__')\r
+ current = self.data\r
+ for bit in bits:\r
+ # note the difference between the attribute being None and not\r
+ # existing at all; assume "value doesn't exist" in the former\r
+ # (e.g. a relationship has no value), raise error in the latter.\r
+ # a more proper solution perhaps would look at the model meta\r
+ # data instead to find out whether a relationship is valid; see\r
+ # also ``_validate_column_name``, where such a mechanism is\r
+ # already implemented).\r
+ if not hasattr(current, bit):\r
+ raise ValueError("Could not resolve %s from %s" % (bit, name))\r
+\r
+ current = getattr(current, bit)\r
+ if callable(current):\r
+ current = current()\r
+ # important that we break in None case, or a relationship\r
+ # spanning across a null-key will raise an exception in the\r
+ # next iteration, instead of defaulting.\r
+ if current is None:\r
+ break\r
+\r
+ if current is None:\r
+ # ...the whole name (i.e. the last bit) resulted in None\r
+ if boundcol.column.default is not None:\r
+ return boundcol.get_default(self)\r
+ return current\r
+\r
+\r
+class ModelRows(Rows):\r
+ row_class = BoundModelRow\r
+\r
+ def __init__(self, *args, **kwargs):\r
+ super(ModelRows, self).__init__(*args, **kwargs)\r
+\r
+ def _reset(self):\r
+ self._length = None\r
+\r
+ def __len__(self):\r
+ """Use the queryset count() method to get the length, instead of\r
+ loading all results into memory. This allows, for example,\r
+ smart paginators that use len() to perform better.\r
+ """\r
+ if getattr(self, '_length', None) is None:\r
+ self._length = self.table.data.count()\r
+ return self._length\r
+\r
+ # for compatibility with QuerySetPaginator\r
+ count = __len__\r
+\r
+\r
class ModelTableMetaclass(DeclarativeColumnsMetaclass):\r
def __new__(cls, name, bases, attrs):\r
# Let the default form meta class get the declared columns; store\r
\r
__metaclass__ = ModelTableMetaclass\r
\r
+ rows_class = ModelRows\r
+\r
def __init__(self, data=None, *args, **kwargs):\r
if data == None:\r
if self._meta.model is None:\r
self.queryset = data\r
\r
super(ModelTable, self).__init__(self.queryset, *args, **kwargs)\r
- self._rows = ModelRows(self)\r
\r
def _validate_column_name(self, name, purpose):\r
"""Overridden. Only allow model-based fields and valid model\r
actual_order_by = self._resolve_sort_directions(self.order_by)\r
queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))\r
self._snapshot = queryset\r
-\r
- def _get_rows(self):\r
- for row in self.data:\r
- yield BoundModelRow(self, row)\r
-\r
-\r
-class ModelRows(Rows):\r
- def __init__(self, *args, **kwargs):\r
- super(ModelRows, self).__init__(*args, **kwargs)\r
- self.row_klass = BoundModelRow\r
-\r
- def _reset(self):\r
- self._length = None\r
-\r
- def __len__(self):\r
- """Use the queryset count() method to get the length, instead of\r
- loading all results into memory. This allows, for example,\r
- smart paginators that use len() to perform better.\r
- """\r
- if getattr(self, '_length', None) is None:\r
- self._length = self.table.data.count()\r
- return self._length\r
-\r
- # for compatibility with QuerySetPaginator\r
- count = __len__\r
-\r
-class BoundModelRow(BoundRow):\r
- """Special version of the BoundRow class that can handle model instances\r
- as data.\r
-\r
- We could simply have ModelTable spawn the normal BoundRow objects\r
- with the instance converted to a dict instead. However, this way allows\r
- us to support non-field attributes and methods on the model as well.\r
- """\r
- def __getitem__(self, name):\r
- """Overridden. Return this row's data for a certain column, with\r
- custom handling for model tables.\r
- """\r
-\r
- # find the column for the requested field, for reference\r
- boundcol = self.table._columns[name]\r
-\r
- # If the column has a name override (we know then that is was also\r
- # used for access, e.g. if the condition is true, then\r
- # ``boundcol.column.name == name``), we need to make sure we use the\r
- # declaration name to access the model field.\r
- if boundcol.column.data:\r
- if callable(boundcol.column.data):\r
- result = boundcol.column.data(self)\r
- if not result:\r
- if boundcol.column.default is not None:\r
- return boundcol.get_default(self)\r
- return result\r
- else:\r
- name = boundcol.column.data\r
- else:\r
- name = boundcol.declared_name\r
-\r
-\r
- # try to resolve relationships spanning attributes\r
- bits = name.split('__')\r
- current = self.data\r
- for bit in bits:\r
- # note the difference between the attribute being None and not\r
- # existing at all; assume "value doesn't exist" in the former\r
- # (e.g. a relationship has no value), raise error in the latter.\r
- # a more proper solution perhaps would look at the model meta\r
- # data instead to find out whether a relationship is valid; see\r
- # also ``_validate_column_name``, where such a mechanism is\r
- # already implemented).\r
- if not hasattr(current, bit):\r
- raise ValueError("Could not resolve %s from %s" % (bit, name))\r
-\r
- current = getattr(current, bit)\r
- if callable(current):\r
- current = current()\r
- # important that we break in None case, or a relationship\r
- # spanning across a null-key will raise an exception in the\r
- # next iteration, instead of defaulting.\r
- if current is None:\r
- break\r
-\r
- if current is None:\r
- # ...the whole name (i.e. the last bit) resulted in None\r
- if boundcol.column.default is not None:\r
- return boundcol.get_default(self)\r
- return current
\ No newline at end of file