Adds a summation row option to the datatables.
Implements blueprint summation-row. Change-Id: I81c984bbf830182854e8f2227e6eae8909e91eef
This commit is contained in:
parent
96388c7fa9
commit
8fd77f047f
@ -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)
|
||||
|
||||
|
@ -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:"–"}}</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>
|
||||
|
@ -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>')
|
||||
|
Loading…
x
Reference in New Issue
Block a user