# 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 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 ungettext_lazy import mock from mox3.mox import IsA import six 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 u"%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', u'öbject_4', u'välue_1', u'üp', u'öptional_1', u'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'), ) 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 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 ungettext_lazy( u"Batch Item", u"Batch Items", count ) @staticmethod def action_past(count): # Translators: test code, don't really have to translate return ungettext_lazy( u"Batched Item", u"Batched Items", count ) class MyBatchActionWithHelpText(MyBatchAction): name = "batch_help" help_text = "this is help." @staticmethod def action_present(count): # No translation return u"BatchHelp Item" @staticmethod def action_past(count): # No translation return u"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 ungettext_lazy( u"Up Item", u"Up Items", count ) else: # Translators: test code, don't really have to translate return ungettext_lazy( u"Down Item", u"Down Items", count ) def action_past(self, count): if self.current_past_action: # Translators: test code, don't really have to translate return ungettext_lazy( u"Upped Item", u"Upped Items", count ) else: # Translators: test code, don't really have to translate return ungettext_lazy( u"Downed Item", u"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 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 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 class MyUpdateAction(tables.UpdateAction): def allowed(self, *args): return True def update_cell(self, *args): pass class MyUpdateActionNotAllowed(MyUpdateAction): def allowed(self, *args): return False 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(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateAction) 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 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(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateActionNotAllowed) 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(required=True), form_field_attributes={'class': 'test'}, update_action=MyUpdateActionNotAllowed, 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): use_mox = True 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(u"My Table", six.text_type(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.columns.values(), ['', '', '', '', '', '', '']) # Actions (these also test ordering) self.assertQuerysetEqual(self.table.base_actions.values(), ['', '', '', '', '', '']) self.assertQuerysetEqual(self.table.get_table_actions(), ['', '', '', '']) self.assertQuerysetEqual(self.table.get_row_actions(TEST_DATA[0]), ['', '', '', '', '']) # 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(MyUpdateAction, name_column.update_action) 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.columns.values(), expected_columns) @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.columns.values(), expected_columns) 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.columns.values(), ['', '']) def test_table_force_no_actions_column(self): class TempTable(MyTable): class Meta(object): columns = ('id',) table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) actions_column = False self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) 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,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '']) 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.columns.values(), ['', '']) def test_table_column_inheritance(self): class TempTable(MyTable): extra = tables.Column('extra') class Meta(object): name = "temp_table" table_actions = (MyFilterAction, MyAction,) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) self.assertQuerysetEqual(self.table.columns.values(), ['', '', '', '', '', '', '', '', '']) 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, ['', '', '', '', '', '', '']) # Verify we retrieve the right rows from our data rows = self.table.get_rows() self.assertQuerysetEqual(rows, ['', '', '', '']) # Verify each row contains the right cells self.assertQuerysetEqual(rows[0].get_cells(), ['', '', '', '', '', '', '']) 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", six.text_type(id_col)) self.assertEqual("Verbose Name", six.text_type(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(u'A Status that is longer than 35 ...', 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, "