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