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. A string or callable to be used for cells which have no data.
Defaults to the string ``"-"``. 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 .. attribute:: filters
A list of functions (often template filters) to be applied to the 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. A dict of HTML attribute strings which should be added to this column.
Example: ``attrs={"data-foo": "bar"}``. 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 # Used to retain order when instantiating columns on a table
creation_counter = 0 creation_counter = 0
# Used for special auto-generated columns # Used for special auto-generated columns
@ -162,8 +173,8 @@ class Column(html.HTMLElement):
def __init__(self, transform, verbose_name=None, sortable=False, def __init__(self, transform, verbose_name=None, sortable=False,
link=None, hidden=False, attrs=None, status=False, link=None, hidden=False, attrs=None, status=False,
status_choices=None, display_choices=None, status_choices=None, display_choices=None, empty_value=None,
empty_value=None, filters=None, classes=None): filters=None, classes=None, summation=None):
self.classes = classes or getattr(self, "classes", []) self.classes = classes or getattr(self, "classes", [])
super(Column, self).__init__() super(Column, self).__init__()
self.attrs.update(attrs or {}) self.attrs.update(attrs or {})
@ -190,6 +201,12 @@ class Column(html.HTMLElement):
self.status_choices = status_choices self.status_choices = status_choices
self.display_choices = display_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 self.creation_counter = Column.creation_counter
Column.creation_counter += 1 Column.creation_counter += 1
@ -204,18 +221,12 @@ class Column(html.HTMLElement):
def __repr__(self): def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.name) 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. 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 return value will be either the attribute specified for this column the table.
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]
# Callable transformations # Callable transformations
if callable(self.transform): if callable(self.transform):
data = self.transform(datum) data = self.transform(datum)
@ -233,6 +244,20 @@ class Column(html.HTMLElement):
msg = termcolors.colorize(msg, **PALETTE['ERROR']) msg = termcolors.colorize(msg, **PALETTE['ERROR'])
LOG.warning(msg) LOG.warning(msg)
data = None 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 display_value = None
if self.display_choices: if self.display_choices:
display_value = [display for (value, display) in display_value = [display for (value, display) in
@ -262,6 +287,20 @@ class Column(html.HTMLElement):
except urlresolvers.NoReverseMatch: except urlresolvers.NoReverseMatch:
return self.link 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): class Row(html.HTMLElement):
""" Represents a row in the table. """ Represents a row in the table.
@ -743,6 +782,9 @@ class DataTable(object):
for action in self.base_actions.values(): for action in self.base_actions.values():
action.table = self action.table = self
self.needs_summary_row = any([col.summation
for col in self.columns.values()])
def __unicode__(self): def __unicode__(self):
return unicode(self._meta.verbose_name) return unicode(self._meta.verbose_name)

View File

@ -26,6 +26,17 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot> <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> <tr>
<td colspan="{{ table.get_columns|length }}"> <td colspan="{{ table.get_columns|length }}">
<span>{% blocktrans count counter=rows|length %}Displaying {{ counter }} item{% plural %}Displaying {{ counter }} items{% endblocktrans %}</span> <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'), 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): class MyLinkAction(tables.LinkAction):
name = "login" name = "login"
@ -147,7 +152,8 @@ class MyTable(tables.DataTable):
value = tables.Column('value', value = tables.Column('value',
sortable=True, sortable=True,
link='http://example.com/', link='http://example.com/',
attrs={'class': 'green blue'}) attrs={'class': 'green blue'},
summation="average")
status = tables.Column('status', link=get_link) status = tables.Column('status', link=get_link)
optional = tables.Column('optional', empty_value='N/A') optional = tables.Column('optional', empty_value='N/A')
excluded = tables.Column('excluded') excluded = tables.Column('excluded')
@ -582,3 +588,26 @@ class DataTableTests(test.TestCase):
id(t2cols[0].table)) id(t2cols[0].table))
self.assertNotEqual(id(t1cols[0].table._data_cache), self.assertNotEqual(id(t1cols[0].table._data_cache),
id(t2cols[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>')