From 8fd77f047fccfafa5659ab8c2d76bb40c7ab36bd Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Fri, 1 Jun 2012 17:04:18 -0700 Subject: [PATCH] Adds a summation row option to the datatables. Implements blueprint summation-row. Change-Id: I81c984bbf830182854e8f2227e6eae8909e91eef --- horizon/tables/base.py | 66 +++++++++++++++---- .../templates/horizon/common/_data_table.html | 11 ++++ horizon/tests/table_tests.py | 31 ++++++++- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/horizon/tables/base.py b/horizon/tables/base.py index db1e7754d..27fed72bd 100644 --- a/horizon/tables/base.py +++ b/horizon/tables/base.py @@ -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) diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html index 54c743d87..6aabcca0a 100644 --- a/horizon/templates/horizon/common/_data_table.html +++ b/horizon/templates/horizon/common/_data_table.html @@ -26,6 +26,17 @@ {% endfor %} + {% if table.needs_summary_row %} + + {% for column in columns %} + {% if forloop.first %} + {% trans "Summary" %} + {% else %} + {{ column.get_summation|default:"–"}} + {% endif %} + {% endfor %} + + {% endif %} {% blocktrans count counter=rows|length %}Displaying {{ counter }} item{% plural %}Displaying {{ counter }} items{% endblocktrans %} diff --git a/horizon/tests/table_tests.py b/horizon/tests/table_tests.py index 75de6a105..89bd881c8 100644 --- a/horizon/tests/table_tests.py +++ b/horizon/tests/table_tests.py @@ -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, 'Summary', 1) + self.assertContains(res, '3.0', 1) + + # Test again with the "sum" method. + table.columns['value'].summation = "sum" + res = http.HttpResponse(table.render()) + self.assertContains(res, 'Summary', 1) + self.assertContains(res, '6', 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, '3.0') + self.assertNotContains(res, '6')