Adds a summation row option to the datatables.

Implements blueprint summation-row.

Change-Id: I81c984bbf830182854e8f2227e6eae8909e91eef
This commit is contained in:
Gabriel Hurley 2012-06-01 17:04:18 -07:00
parent 96388c7fa9
commit 8fd77f047f
3 changed files with 95 additions and 13 deletions

View File

@ -120,6 +120,13 @@ class Column(html.HTMLElement):
A string or callable to be used for cells which have no data.
Defaults to the string ``"-"``.
.. attribute:: summation
A string containing the name of a summation method to be used in
the generation of a summary row for this column. By default the
options are ``"sum"`` or ``"average"``, which behave as expected.
Optional.
.. attribute:: filters
A list of functions (often template filters) to be applied to the
@ -136,6 +143,10 @@ class Column(html.HTMLElement):
A dict of HTML attribute strings which should be added to this column.
Example: ``attrs={"data-foo": "bar"}``.
"""
summation_methods = {
"sum": sum,
"average": lambda data: sum(data, 0.0) / len(data)
}
# Used to retain order when instantiating columns on a table
creation_counter = 0
# Used for special auto-generated columns
@ -162,8 +173,8 @@ class Column(html.HTMLElement):
def __init__(self, transform, verbose_name=None, sortable=False,
link=None, hidden=False, attrs=None, status=False,
status_choices=None, display_choices=None,
empty_value=None, filters=None, classes=None):
status_choices=None, display_choices=None, empty_value=None,
filters=None, classes=None, summation=None):
self.classes = classes or getattr(self, "classes", [])
super(Column, self).__init__()
self.attrs.update(attrs or {})
@ -190,6 +201,12 @@ class Column(html.HTMLElement):
self.status_choices = status_choices
self.display_choices = display_choices
if summation is not None and summation not in self.summation_methods:
raise ValueError("Summation method %s must be one of %s."
% (summation,
", ".join(self.summation_methods.keys())))
self.summation = summation
self.creation_counter = Column.creation_counter
Column.creation_counter += 1
@ -204,18 +221,12 @@ class Column(html.HTMLElement):
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.name)
def get_data(self, datum):
def get_raw_data(self, datum):
"""
Returns the appropriate data for this column from the given input.
The return value will be either the attribute specified for this column
or the return value of the attr:`~horizon.tables.Column.transform`
method for this column.
Returns the raw data for this column, before any filters or formatting
are applied to it. This is useful when doing calculations on data in
the table.
"""
datum_id = self.table.get_object_id(datum)
if datum_id in self.table._data_cache[self]:
return self.table._data_cache[self][datum_id]
# Callable transformations
if callable(self.transform):
data = self.transform(datum)
@ -233,6 +244,20 @@ class Column(html.HTMLElement):
msg = termcolors.colorize(msg, **PALETTE['ERROR'])
LOG.warning(msg)
data = None
return data
def get_data(self, datum):
"""
Returns the final display data for this column from the given inputs.
The return value will be either the attribute specified for this column
or the return value of the attr:`~horizon.tables.Column.transform`
method for this column.
"""
datum_id = self.table.get_object_id(datum)
if datum_id in self.table._data_cache[self]:
return self.table._data_cache[self][datum_id]
data = self.get_raw_data(datum)
display_value = None
if self.display_choices:
display_value = [display for (value, display) in
@ -262,6 +287,20 @@ class Column(html.HTMLElement):
except urlresolvers.NoReverseMatch:
return self.link
def get_summation(self):
"""
Returns the summary value for the data in this column if a
valid summation method is specified for it. Otherwise returns ``None``.
"""
if self.summation not in self.summation_methods:
return None
summation_function = self.summation_methods[self.summation]
data = [self.get_raw_data(datum) for datum in self.table.data]
summation = summation_function(data)
for filter_func in self.filters:
summation = filter_func(summation)
return summation
class Row(html.HTMLElement):
""" Represents a row in the table.
@ -743,6 +782,9 @@ class DataTable(object):
for action in self.base_actions.values():
action.table = self
self.needs_summary_row = any([col.summation
for col in self.columns.values()])
def __unicode__(self):
return unicode(self._meta.verbose_name)

View File

@ -26,6 +26,17 @@
{% endfor %}
</tbody>
<tfoot>
{% if table.needs_summary_row %}
<tr class="summation">
{% for column in columns %}
{% if forloop.first %}
<td>{% trans "Summary" %}</td>
{% else %}
<td>{{ column.get_summation|default:"&ndash;"}}</td>
{% endif %}
{% endfor %}
</tr>
{% endif %}
<tr>
<td colspan="{{ table.get_columns|length }}">
<span>{% blocktrans count counter=rows|length %}Displaying {{ counter }} item{% plural %}Displaying {{ counter }} items{% endblocktrans %}</span>

View File

@ -51,6 +51,11 @@ TEST_DATA_3 = (
FakeObject('1', 'object_1', 'value_1', 'up', 'optional_1', 'excluded_1'),
)
TEST_DATA_4 = (
FakeObject('1', 'object_1', 2, 'up'),
FakeObject('2', 'object_2', 4, 'up'),
)
class MyLinkAction(tables.LinkAction):
name = "login"
@ -147,7 +152,8 @@ class MyTable(tables.DataTable):
value = tables.Column('value',
sortable=True,
link='http://example.com/',
attrs={'class': 'green blue'})
attrs={'class': 'green blue'},
summation="average")
status = tables.Column('status', link=get_link)
optional = tables.Column('optional', empty_value='N/A')
excluded = tables.Column('excluded')
@ -582,3 +588,26 @@ class DataTableTests(test.TestCase):
id(t2cols[0].table))
self.assertNotEqual(id(t1cols[0].table._data_cache),
id(t2cols[0].table._data_cache))
def test_summation_row(self):
# Test with the "average" method.
table = MyTable(self.request, TEST_DATA_4)
res = http.HttpResponse(table.render())
self.assertContains(res, '<tr class="summation"', 1)
self.assertContains(res, '<td>Summary</td>', 1)
self.assertContains(res, '<td>3.0</td>', 1)
# Test again with the "sum" method.
table.columns['value'].summation = "sum"
res = http.HttpResponse(table.render())
self.assertContains(res, '<tr class="summation"', 1)
self.assertContains(res, '<td>Summary</td>', 1)
self.assertContains(res, '<td>6</td>', 1)
# One last test with no summation.
table.columns['value'].summation = None
table.needs_summary_row = False
res = http.HttpResponse(table.render())
self.assertNotContains(res, '<tr class="summation"')
self.assertNotContains(res, '<td>3.0</td>')
self.assertNotContains(res, '<td>6</td>')