# encoding=utf-8 # # Copyright 2012 Nebula, Inc. # Copyright 2014 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import unittest from unittest import mock import uuid from django import forms from django import http from django import shortcuts from django.template import defaultfilters from django.test.utils import override_settings from django.urls import reverse from django.utils.translation import ngettext_lazy from horizon import exceptions from horizon import tables from horizon.tables import actions from horizon.tables import formset as table_formset from horizon.tables import views as table_views from horizon.test import helpers as test class FakeObject(object): def __init__(self, id, name, value, status, optional=None, excluded=None): self.id = id self.name = name self.value = value self.status = status self.optional = optional self.excluded = excluded self.extra = "extra" def __str__(self): return "%s: %s" % (self.__class__.__name__, self.name) TEST_DATA = ( FakeObject('1', 'object_1', 'value_1', 'up', 'optional_1', 'excluded_1'), FakeObject('2', 'object_2', 'evil', 'down', 'optional_2'), FakeObject('3', 'object_3', 'value_3', 'up'), FakeObject('4', 'öbject_4', 'välue_1', 'üp', 'öptional_1', 'exclüded_1'), ) TEST_DATA_2 = ( FakeObject('1', 'object_1', 'value_1', 'down', 'optional_1', 'excluded_1'), ) 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'), ) TEST_DATA_5 = ( FakeObject('1', 'object_1', 'value_1', 'A Status that is longer than 35 characters!', 'optional_1'), ) TEST_DATA_6 = ( FakeObject('1', 'object_1', 'DELETED', 'down'), FakeObject('2', 'object_2', 'CREATED', 'up'), FakeObject('3', 'object_3', 'STANDBY', 'standby'), ) TEST_DATA_7 = ( FakeObject('1', 'wrapped name', 'wrapped value', 'status', 'not wrapped optional'), ) TEST_DATA_8 = ( FakeObject('1', 'object_1', 'value_1', 'started', 'optional_1', 'excluded_1'), FakeObject('2', 'object_1', 'value_1', 'half', 'optional_1', 'excluded_1'), FakeObject('3', 'object_1', 'value_1', 'finished', 'optional_1', 'excluded_1'), ) class MyLinkAction(tables.LinkAction): name = "login" verbose_name = "Log In" url = "login" attrs = { "class": "ajax-modal", } def get_link_url(self, datum=None, *args, **kwargs): return reverse(self.url) class MyAction(tables.Action): name = "delete" verbose_name = "Delete Me" verbose_name_plural = "Delete Them" def allowed(self, request, obj=None): return getattr(obj, 'status', None) != 'down' def handle(self, data_table, request, object_ids): return shortcuts.redirect('http://example.com/?ids=%s' % ",".join(object_ids)) class MyColumn(tables.Column): pass class MyRowSelectable(tables.Row): ajax = True def can_be_selected(self, datum): return datum.value != 'DELETED' class MyRowSortable(tables.Row): ajax = True @classmethod def get_data(cls, request, obj_id): return TEST_DATA_8[0] class MyRow(tables.Row): ajax = True @classmethod def get_data(cls, request, obj_id): return TEST_DATA_2[0] class MyBatchAction(tables.BatchAction): name = "batch" def action(self, request, object_ids): pass @staticmethod def action_present(count): # Translators: test code, don't really have to translate return ngettext_lazy( "Batch Item", "Batch Items", count ) @staticmethod def action_past(count): # Translators: test code, don't really have to translate return ngettext_lazy( "Batched Item", "Batched Items", count ) class MyBatchActionWithHelpText(MyBatchAction): name = "batch_help" help_text = "this is help." @staticmethod def action_present(count): # No translation return "BatchHelp Item" @staticmethod def action_past(count): # No translation return "BatchedHelp Item" class MyToggleAction(tables.BatchAction): name = "toggle" def action_present(self, count): if self.current_present_action: # Translators: test code, don't really have to translate return ngettext_lazy( "Up Item", "Up Items", count ) else: # Translators: test code, don't really have to translate return ngettext_lazy( "Down Item", "Down Items", count ) def action_past(self, count): if self.current_past_action: # Translators: test code, don't really have to translate return ngettext_lazy( "Upped Item", "Upped Items", count ) else: # Translators: test code, don't really have to translate return ngettext_lazy( "Downed Item", "Downed Items", count ) def allowed(self, request, obj=None): if not obj: return False self.down = getattr(obj, 'status', None) == 'down' if self.down: self.current_present_action = 1 else: self.current_present_action = 0 return self.down or getattr(obj, 'status', None) == 'up' def action(self, request, object_ids): if self.down: # up it self.current_past_action = 1 else: self.current_past_action = 0 class MyDisabledAction(MyToggleAction): def allowed(self, request, obj=None): return False class MyFilterAction(tables.FilterAction): def filter(self, table, objs, filter_string): q = filter_string.lower() def comp(obj): if q in obj.name.lower(): return True return False return filter(comp, objs) class MyServerFilterAction(tables.FilterAction): filter_type = 'server' filter_choices = (('name', 'Name', False), ('status', 'Status', True)) needs_preloading = True def filter(self, table, items, filter_string): filter_field = table.get_filter_field() if filter_field == 'name' and filter_string: return [item for item in items if filter_string in item.name] return items def get_name(obj): return "custom %s" % obj.name def get_link(obj): return reverse('login') class MyTable(tables.DataTable): tooltip_dict = {'up': {'title': 'service is up and running', 'style': 'color:green;cursor:pointer'}, 'down': {'title': 'service is not available', 'style': 'color:red;cursor:pointer'}} id = tables.Column('id', hidden=True, sortable=False) name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True, form_field=forms.CharField(), form_field_attributes={'class': 'test'}) value = tables.Column('value', sortable=True, link='http://example.com/', attrs={'class': 'green blue'}, summation="average", link_classes=('link-modal',), link_attrs={'data-type': 'modal dialog', 'data-tip': 'click for dialog'}) status = tables.Column('status', link=get_link, truncate=35, cell_attributes_getter=tooltip_dict.get) optional = tables.Column('optional', empty_value='N/A') excluded = tables.Column('excluded') class Meta(object): name = "my_table" verbose_name = "My Table" status_columns = ["status"] columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow column_class = MyColumn table_actions = (MyFilterAction, MyAction, MyBatchAction, MyBatchActionWithHelpText) row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction, MyBatchActionWithHelpText) class MyProgressTable(MyTable): tooltip_dict = {'started': {'percent': '10%'}, 'half': {'percent': '50%'}, 'finished': {'percent': '100%'}} status = tables.Column('status', truncate=35, status=True, cell_attributes_getter=tooltip_dict.get) class Meta(object): name = "my_table" verbose_name = "My Table" status_columns = ["status"] columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRowSortable column_class = MyColumn class TableWithColumnsPolicy(tables.DataTable): name = tables.Column('name') restricted = tables.Column('restricted', policy_rules=[('compute', 'role:admin')]) class MyServerFilterTable(MyTable): class Meta(object): name = "my_table" verbose_name = "My Table" status_columns = ["status"] columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow column_class = MyColumn table_actions = (MyServerFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction, MyBatchActionWithHelpText) class MyTableSelectable(MyTable): class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'status') row_class = MyRowSelectable status_columns = ["status"] multi_select = True class MyTableNotAllowedInlineEdit(MyTable): name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True, form_field=forms.CharField(), form_field_attributes={'class': 'test'}) class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'optional', 'status') row_class = MyRow class MyTableWrapList(MyTable): name = tables.Column('name', form_field=forms.CharField(), form_field_attributes={'class': 'test'}, wrap_list=True) value = tables.Column('value', wrap_list=True) optional = tables.Column('optional', wrap_list=False) class NoActionsTable(tables.DataTable): id = tables.Column('id') class Meta(object): name = "no_actions_table" verbose_name = "No Actions Table" table_actions = () row_actions = () class DisabledActionsTable(tables.DataTable): id = tables.Column('id') class Meta(object): name = "disabled_actions_table" verbose_name = "Disabled Actions Table" table_actions = (MyDisabledAction,) row_actions = () multi_select = True class DataTableTests(test.TestCase): def test_table_instantiation(self): """Tests everything that happens when the table is instantiated.""" self.table = MyTable(self.request, TEST_DATA) # Properties defined on the table self.assertEqual(TEST_DATA, self.table.data) self.assertEqual("my_table", self.table.name) # Verify calculated options that weren't specified explicitly self.assertTrue(self.table._meta.actions_column) self.assertTrue(self.table._meta.multi_select) # Test for verbose_name self.assertEqual("My Table", str(self.table)) # Column ordering and exclusion. # This should include auto-columns for multi_select and actions, # but should not contain the excluded column. # Additionally, auto-generated columns should use the custom # column class specified on the table. self.assertQuerysetEqual(self.table.get_columns(), ['', '', '', '', '', '', ''], transform=repr) # Actions (these also test ordering) self.assertQuerysetEqual(list(self.table.base_actions.values()), ['', '', '', '', '', ''], transform=repr) self.assertQuerysetEqual(self.table.get_table_actions(), ['', '', '', ''], transform=repr) self.assertQuerysetEqual(self.table.get_row_actions(TEST_DATA[0]), ['', '', '', '', ''], transform=repr) # Auto-generated columns multi_select = self.table.columns['multi_select'] self.assertEqual("multi_select", multi_select.auto) self.assertEqual("multi_select_column", multi_select.get_final_attrs().get('class', "")) actions = self.table.columns['actions'] self.assertEqual("actions", actions.auto) self.assertEqual("actions_column", actions.get_final_attrs().get('class', "")) # In-line edit action on column. name_column = self.table.columns['name'] self.assertEqual(forms.CharField, name_column.form_field.__class__) self.assertEqual({'class': 'test'}, name_column.form_field_attributes) @override_settings(POLICY_CHECK_FUNCTION=lambda *args: False) def test_table_column_policy_not_allowed(self): self.table = TableWithColumnsPolicy(self.request, TEST_DATA) self.assertEqual(TEST_DATA, self.table.data) # The column "restricted" is not rendered because of policy expected_columns = [''] self.assertQuerysetEqual(self.table.get_columns(), expected_columns, transform=repr) @override_settings(POLICY_CHECK_FUNCTION=lambda *args: True) def test_table_column_policy_allowed(self): self.table = TableWithColumnsPolicy(self.request, TEST_DATA) self.assertEqual(TEST_DATA, self.table.data) # Policy check returns True so the column "restricted" is rendered expected_columns = ['', ''] self.assertQuerysetEqual(self.table.get_columns(), expected_columns, transform=repr) def test_table_force_no_multiselect(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) multi_select = False self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.get_columns(), ['', ''], transform=repr) def test_table_force_no_actions_column(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction,) actions_column = False self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.get_columns(), ['', ''], transform=repr) def test_table_natural_no_inline_editing(self): class TempTable(MyTable): name = tables.Column(get_name, verbose_name="Verbose Name", sortable=True) class Meta(object): name = "my_table" columns = ('id', 'name', 'value', 'optional', 'status') self.table = TempTable(self.request, TEST_DATA_2) name_column = self.table.columns['name'] self.assertIsNone(name_column.update_action) self.assertIsNone(name_column.form_field) self.assertEqual({}, name_column.form_field_attributes) def test_table_natural_no_actions_column(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction, MyBatchAction) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.get_columns(), ['', ''], transform=repr) def test_table_natural_no_multiselect(self): class TempTable(MyTable): class Meta(object): columns = ('id',) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.get_columns(), ['', ''], transform=repr) def test_table_column_inheritance(self): class TempTable(MyTable): extra = tables.Column('extra') class Meta(object): name = "temp_table" table_actions = (MyFilterAction, MyAction, MyBatchAction) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.get_columns(), ['', '', '', '', '', '', '', '', ''], transform=repr) def test_table_construction(self): self.table = MyTable(self.request, TEST_DATA) # Verify we retrieve the right columns for headers columns = self.table.get_columns() self.assertQuerysetEqual(columns, ['', '', '', '', '', '', ''], transform=repr) # Verify we retrieve the right rows from our data rows = self.table.get_rows() self.assertQuerysetEqual(rows, ['', '', '', ''], transform=repr) # Verify each row contains the right cells self.assertQuerysetEqual(rows[0].get_cells(), ['', '', '', '', '', '', ''], transform=repr) def test_table_column(self): self.table = MyTable(self.request, TEST_DATA) row = self.table.get_rows()[0] row3 = self.table.get_rows()[2] id_col = self.table.columns['id'] name_col = self.table.columns['name'] value_col = self.table.columns['value'] # transform self.assertEqual('1', row.cells['id'].data) # Standard attr access self.assertEqual('custom object_1', row.cells['name'].data) # Callable # name and verbose_name self.assertEqual("Id", str(id_col)) self.assertEqual("Verbose Name", str(name_col)) # sortable self.assertFalse(id_col.sortable) self.assertNotIn("sortable", id_col.get_final_attrs().get('class', "")) self.assertTrue(name_col.sortable) self.assertIn("sortable", name_col.get_final_attrs().get('class', "")) # hidden self.assertTrue(id_col.hidden) self.assertIn("hide", id_col.get_final_attrs().get('class', "")) self.assertFalse(name_col.hidden) self.assertNotIn("hide", name_col.get_final_attrs().get('class', "")) # link, link_classes, link_attrs, and get_link_url self.assertIn('href="http://example.com/"', row.cells['value'].value) self.assertIn('class="link-modal"', row.cells['value'].value) self.assertIn('data-type="modal dialog"', row.cells['value'].value) self.assertIn('data-tip="click for dialog"', row.cells['value'].value) self.assertIn('href="/auth/login/"', row.cells['status'].value) # empty_value self.assertEqual("N/A", row3.cells['optional'].value) # classes self.assertEqual("green blue sortable anchor normal_column", value_col.get_final_attrs().get('class', "")) # status cell_status = row.cells['status'].status self.assertTrue(cell_status) self.assertEqual('status_up', row.cells['status'].get_status_class(cell_status)) # status_choices id_col.status = True id_col.status_choices = (('1', False), ('2', True), ('3', None)) cell_status = row.cells['id'].status self.assertFalse(cell_status) self.assertEqual('status_down', row.cells['id'].get_status_class(cell_status)) cell_status = row3.cells['id'].status self.assertIsNone(cell_status) self.assertEqual('warning', row.cells['id'].get_status_class(cell_status)) # Ensure data is not cached on the column across table instances self.table = MyTable(self.request, TEST_DATA_2) row = self.table.get_rows()[0] self.assertIn("down", row.cells['status'].value) def test_table_row(self): self.table = MyTable(self.request, TEST_DATA) row = self.table.get_rows()[0] self.assertEqual(self.table, row.table) self.assertEqual(TEST_DATA[0], row.datum) self.assertEqual('my_table__row__1', row.id) # Verify row status works even if status isn't set on the column self.assertTrue(row.status) self.assertEqual('status_up', row.status_class) # Check the cells as well cell_status = row.cells['status'].status self.assertTrue(cell_status) self.assertEqual('status_up', row.cells['status'].get_status_class(cell_status)) def test_table_column_truncation(self): self.table = MyTable(self.request, TEST_DATA_5) row = self.table.get_rows()[0] self.assertEqual(35, len(row.cells['status'].data)) self.assertEqual('A Status that is longer than 35 ch…', row.cells['status'].data) def test_table_rendering(self): self.table = MyTable(self.request, TEST_DATA) # Table actions table_actions = self.table.render_table_actions() resp = http.HttpResponse(table_actions) self.assertContains(resp, "table_search", 1) self.assertContains(resp, "my_table__filter__q", 1) self.assertContains(resp, "my_table__delete", 1) self.assertContains(resp, 'id="my_table__action_delete"', 1) # Table BatchActions self.assertContains(resp, 'id="my_table__action_batch_help"', 1) self.assertContains(resp, 'help_text="this is help."', 1) self.assertContains(resp, 'BatchHelp Item', 1) # Row actions row_actions = self.table.render_row_actions(TEST_DATA[0]) resp = http.HttpResponse(row_actions) self.assertContains(resp, "