From 445903f90d2564d9945c8d906ae61b06c6eb55b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20T=C4=99=C5=BCycki?= Date: Tue, 31 Jan 2017 21:37:12 +0100 Subject: [PATCH] Implemented Pinned Rows for issue: #406 --- django_tables2/rows.py | 83 ++++++++++++++++++++++++++++++++++++++-- django_tables2/tables.py | 37 ++++++++++++++++-- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/django_tables2/rows.py b/django_tables2/rows.py index e8c0382b..37d815c8 100644 --- a/django_tables2/rows.py +++ b/django_tables2/rows.py @@ -205,6 +205,46 @@ def items(self): yield (column, self.get_cell(column.name)) +class BoundPinnedRow(BoundRow): + + @property + def attrs(self): + ''' + Return the attributes for a certain pinned row. + Add 'pinned-row' to css class attribute + ''' + row_attrs = computed_values(self._table.pinned_row_attrs, self._record) + cssClass = "pinned-row" + cssClass = " ".join([ + 'even' if next(self._table._counter) % 2 == 0 else 'odd', + cssClass, + row_attrs.get('class', "") + ]) + row_attrs['class'] = cssClass + return AttributeDict(row_attrs) + + def __iter__(self): + ''' + Iterate over the rendered values for cells in the row. + + Under the hood this method just makes a call to + `.BoundPinnedRow.__getitem__` for each cell. + ''' + for column, value in self.items(): + # this uses __getitem__, using the name (rather than the accessor) + # is correct – it's what __getitem__ expects. + yield value + + def _get_and_render_with(self, name, render_func): + """ + Get raw value from record render + this value using by render_func + """ + accessor = A(name) + value = accessor.resolve(context=self._record, quiet=True) or self._table.default + return value + + class BoundRows(object): ''' Container for spawning `.BoundRow` objects. @@ -212,17 +252,45 @@ class BoundRows(object): Arguments: data: iterable of records table: the `~.Table` in which the rows exist + pinned_data: dictionary with iterable of records for top and/or + bottom pinned rows. Example + { + 'top' : [ iterable structure ] | None, + 'bottom' : [ iterable structure ] | None + } This is used for `~.Table.rows`. ''' - def __init__(self, data, table): + def __init__(self, data, table, pinned_data=None): self.data = data self.table = table + self.top_pinned_data = pinned_data.get('top') + self.bottom_pinned_data = pinned_data.get('bottom') + + def generator_pined_row(self, data): + """ + Generator for top and bottom pinned rows + """ + if data is not None: + if hasattr(data, '__iter__') is False: + raise ValueError('The data for pinned rows must be iterable') + else: + # If pinned data is iterable + for pinned_record in data: + yield BoundPinnedRow(pinned_record, table=self.table) def __iter__(self): + # Top pinned rows + for pinned_record in self.generator_pined_row(self.top_pinned_data): + yield pinned_record + for record in self.data: yield BoundRow(record, table=self.table) + # Bottom pinned rows + for pinned_record in self.generator_pined_row(self.bottom_pinned_data): + yield pinned_record + def __len__(self): return len(self.data) @@ -231,5 +299,14 @@ def __getitem__(self, key): Slicing returns a new `~.BoundRows` instance, indexing returns a single `~.BoundRow` instance. ''' - container = BoundRows if isinstance(key, slice) else BoundRow - return container(self.data[key], table=self.table) + if isinstance(key, slice): + return BoundRows( + self.data[key], + table=self.table, + pinned_data={ + 'top': self.top_pinned_data, + 'bottom': self.bottom_pinned_data + } + ) + else: + return BoundRow(self.data[key], table=self.table) diff --git a/django_tables2/tables.py b/django_tables2/tables.py index 9947bc13..8021ac2a 100644 --- a/django_tables2/tables.py +++ b/django_tables2/tables.py @@ -259,6 +259,7 @@ def __init__(self, options=None): super(TableOptions, self).__init__() self.attrs = AttributeDict(getattr(options, 'attrs', {})) self.row_attrs = getattr(options, 'row_attrs', {}) + self.pinned_row_attrs = getattr(options, 'pinned_row_attrs', {}) self.default = getattr(options, 'default', '—') self.empty_text = getattr(options, 'empty_text', None) self.fields = getattr(options, 'fields', None) @@ -308,6 +309,8 @@ class TableBase(object): Allows custom HTML attributes to be specified which will be added to the ```` tag of the rendered table. + pinned_row_attrs: Same as row_attrs but for pinned rows + sequence (iterable): The sequence/order of columns the columns (from left to right). @@ -344,18 +347,28 @@ class TableBase(object): TableDataClass = TableData def __init__(self, data, order_by=None, orderable=None, empty_text=None, - exclude=None, attrs=None, row_attrs=None, sequence=None, - prefix=None, order_by_field=None, page_field=None, + exclude=None, attrs=None, row_attrs=None, pinned_row_attrs=None, + sequence=None, prefix=None, order_by_field=None, page_field=None, per_page_field=None, template=None, default=None, request=None, show_header=None, show_footer=True): + super(TableBase, self).__init__() + self.exclude = exclude or self._meta.exclude self.sequence = sequence self.data = self.TableDataClass(data=data, table=self) if default is None: default = self._meta.default self.default = default - self.rows = BoundRows(data=self.data, table=self) + + # Pinned rows #406 + self.pinned_row_attrs = AttributeDict(pinned_row_attrs or self._meta.pinned_row_attrs) + self.pinned_data = { + 'top': self.get_top_pinned_data(), + 'bottom': self.get_bottom_pinned_data() + } + + self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data) attrs = computed_values(attrs if attrs is not None else self._meta.attrs) self.attrs = AttributeDict(attrs) self.row_attrs = AttributeDict(row_attrs or self._meta.row_attrs) @@ -411,6 +424,24 @@ def __init__(self, data, order_by=None, orderable=None, empty_text=None, self._counter = count() + def get_top_pinned_data(self): + """ + Return data for top pinned rows containing data for each row. + Iterable type like: queryset, list of dicts, list of objects. + Default return None and should be override in subclass of ~.Table\ + For None value top pinned rows are not render. + """ + return None + + def get_bottom_pinned_data(self): + """ + Return data for bottom pinned rows containing data for each row. + Iterable type like: queryset, list of dicts, list of objects. + Default return None and should be override in subclass of ~.Table\ + For None value bottom pinned rows are not render. + """ + return None + def as_html(self, request): ''' Render the table to an HTML table, adding `request` to the context.