1606 lines
66 KiB
Python
1606 lines
66 KiB
Python
# 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.
|
|
|
|
from django.core.urlresolvers import reverse
|
|
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.utils.translation import ungettext_lazy
|
|
|
|
from mox3.mox import IsA # noqa
|
|
import six
|
|
|
|
from horizon import tables
|
|
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', '<strong>evil</strong>', '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):
|
|
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(),
|
|
['<MyColumn: multi_select>',
|
|
'<Column: id>',
|
|
'<Column: name>',
|
|
'<Column: value>',
|
|
'<Column: optional>',
|
|
'<Column: status>',
|
|
'<MyColumn: actions>'])
|
|
# Actions (these also test ordering)
|
|
self.assertQuerysetEqual(self.table.base_actions.values(),
|
|
['<MyBatchAction: batch>',
|
|
'<MyBatchActionWithHelpText: batch_help>',
|
|
'<MyAction: delete>',
|
|
'<MyFilterAction: filter>',
|
|
'<MyLinkAction: login>',
|
|
'<MyToggleAction: toggle>'])
|
|
self.assertQuerysetEqual(self.table.get_table_actions(),
|
|
['<MyFilterAction: filter>',
|
|
'<MyAction: delete>',
|
|
'<MyBatchAction: batch>',
|
|
'<MyBatchActionWithHelpText: batch_help>'])
|
|
self.assertQuerysetEqual(self.table.get_row_actions(TEST_DATA[0]),
|
|
['<MyAction: delete>',
|
|
'<MyLinkAction: login>',
|
|
'<MyBatchAction: batch>',
|
|
'<MyToggleAction: toggle>',
|
|
'<MyBatchActionWithHelpText: batch_help>'])
|
|
# 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 = ['<Column: name>']
|
|
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 = ['<Column: name>', '<Column: restricted>']
|
|
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(),
|
|
['<Column: id>',
|
|
'<Column: actions>'])
|
|
|
|
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(),
|
|
['<Column: multi_select>',
|
|
'<Column: id>'])
|
|
|
|
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(),
|
|
['<Column: multi_select>',
|
|
'<Column: id>'])
|
|
|
|
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(),
|
|
['<Column: id>',
|
|
'<Column: actions>'])
|
|
|
|
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(),
|
|
['<Column: multi_select>',
|
|
'<Column: id>',
|
|
'<Column: name>',
|
|
'<Column: value>',
|
|
'<Column: status>',
|
|
'<Column: optional>',
|
|
'<Column: excluded>',
|
|
'<Column: extra>',
|
|
'<Column: actions>'])
|
|
|
|
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, ['<MyColumn: multi_select>',
|
|
'<Column: id>',
|
|
'<Column: name>',
|
|
'<Column: value>',
|
|
'<Column: optional>',
|
|
'<Column: status>',
|
|
'<MyColumn: actions>'])
|
|
# Verify we retrieve the right rows from our data
|
|
rows = self.table.get_rows()
|
|
self.assertQuerysetEqual(rows, ['<MyRow: my_table__row__1>',
|
|
'<MyRow: my_table__row__2>',
|
|
'<MyRow: my_table__row__3>',
|
|
'<MyRow: my_table__row__4>'])
|
|
# Verify each row contains the right cells
|
|
self.assertQuerysetEqual(rows[0].get_cells(),
|
|
['<Cell: multi_select, my_table__row__1>',
|
|
'<Cell: id, my_table__row__1>',
|
|
'<Cell: name, my_table__row__1>',
|
|
'<Cell: value, my_table__row__1>',
|
|
'<Cell: optional, my_table__row__1>',
|
|
'<Cell: status, my_table__row__1>',
|
|
'<Cell: actions, my_table__row__1>'])
|
|
|
|
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, "<li", 4)
|
|
self.assertContains(resp, "my_table__delete__1", 1)
|
|
self.assertContains(resp, "my_table__toggle__1", 1)
|
|
self.assertContains(resp, "/auth/login/", 1)
|
|
self.assertContains(resp, "ajax-modal", 1)
|
|
self.assertContains(resp, 'id="my_table__row_1__action_delete"', 1)
|
|
|
|
# Row BatchActions
|
|
row_actions = self.table.render_row_actions(TEST_DATA[0])
|
|
resp = http.HttpResponse(row_actions)
|
|
self.assertContains(resp, 'id="my_table__row_1__action_batch_help"', 1)
|
|
self.assertContains(resp, 'help_text="this is help."', 1)
|
|
self.assertContains(resp, 'value="my_table__batch_help__1"', 1)
|
|
self.assertContains(resp, 'BatchHelp Item', 1)
|
|
|
|
# Whole table
|
|
resp = http.HttpResponse(self.table.render())
|
|
self.assertContains(resp, '<table id="my_table"', 1)
|
|
self.assertContains(resp, '<th ', 7)
|
|
self.assertContains(resp, 'id="my_table__row__1"', 1)
|
|
self.assertContains(resp, 'id="my_table__row__2"', 1)
|
|
self.assertContains(resp, 'id="my_table__row__3"', 1)
|
|
update_string = "action=row_update&table=my_table&obj_id="
|
|
self.assertContains(resp, update_string, 4)
|
|
self.assertContains(resp, "data-update-interval", 4)
|
|
# Verify no table heading
|
|
self.assertNotContains(resp, "<h3 class='table_title'")
|
|
# Verify our XSS protection
|
|
self.assertContains(resp, '<strong>evil</strong>', 1)
|
|
# Hidden Title = False shows the table title
|
|
self.table._meta.hidden_title = False
|
|
resp = http.HttpResponse(self.table.render())
|
|
self.assertContains(resp, "<span class='table-title'>", 1)
|
|
|
|
# Filter = False hides the search box
|
|
self.table._meta.filter = False
|
|
table_actions = self.table.render_table_actions()
|
|
resp = http.HttpResponse(table_actions)
|
|
self.assertContains(resp, "table_search", 0)
|
|
|
|
def test_wrap_list_rendering(self):
|
|
self.table = MyTableWrapList(self.request, TEST_DATA_7)
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
value_cell = row.cells['value']
|
|
optional_cell = row.cells['optional']
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
value_cell_rendered = value_cell.render()
|
|
optional_cell_rendered = optional_cell.render()
|
|
resp_name = http.HttpResponse(name_cell_rendered)
|
|
resp_value = http.HttpResponse(value_cell_rendered)
|
|
resp_optional = http.HttpResponse(optional_cell_rendered)
|
|
self.assertContains(resp_name, '<ul>wrapped name</ul>', 1)
|
|
self.assertContains(resp_value, '<ul>wrapped value</ul>', 1)
|
|
self.assertContains(resp_optional, 'not wrapped optional', 1)
|
|
self.assertNotContains(resp_optional, '<ul>')
|
|
self.assertNotContains(resp_optional, '</ul>')
|
|
|
|
def test_inline_edit_available_cell_rendering(self):
|
|
self.table = MyTable(self.request, TEST_DATA_2)
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
|
|
# Check if in-line edit is available in the cell,
|
|
# but is not in inline_edit_mod.
|
|
self.assertTrue(name_cell.inline_edit_available)
|
|
self.assertFalse(name_cell.inline_edit_mod)
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
resp = http.HttpResponse(name_cell_rendered)
|
|
|
|
self.assertContains(resp, '<td', 1)
|
|
self.assertContains(resp, 'inline_edit_available', 1)
|
|
self.assertContains(resp,
|
|
'data-update-url="?action=cell_update&'
|
|
'table=my_table&cell_name=name&obj_id=1"',
|
|
1)
|
|
self.assertContains(resp, 'table_cell_wrapper', 1)
|
|
self.assertContains(resp, 'table_cell_data_wrapper', 1)
|
|
self.assertContains(resp, 'table_cell_action', 1)
|
|
self.assertContains(resp, 'ajax-inline-edit', 1)
|
|
|
|
def test_inline_edit_available_not_allowed_cell_rendering(self):
|
|
self.table = MyTableNotAllowedInlineEdit(self.request, TEST_DATA_2)
|
|
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
|
|
# Check if in-line edit is available in the cell,
|
|
# but is not in inline_edit_mod.
|
|
self.assertTrue(name_cell.inline_edit_available)
|
|
self.assertFalse(name_cell.inline_edit_mod)
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
resp = http.HttpResponse(name_cell_rendered)
|
|
|
|
self.assertContains(resp, '<td', 1)
|
|
self.assertContains(resp, 'inline_edit_available', 1)
|
|
self.assertContains(resp,
|
|
'data-update-url="?action=cell_update&'
|
|
'table=my_table&cell_name=name&obj_id=1"',
|
|
1)
|
|
self.assertContains(resp, 'table_cell_wrapper', 0)
|
|
self.assertContains(resp, 'table_cell_data_wrapper', 0)
|
|
self.assertContains(resp, 'table_cell_action', 0)
|
|
self.assertContains(resp, 'ajax-inline-edit', 0)
|
|
|
|
def test_inline_edit_mod_cell_rendering(self):
|
|
self.table = MyTable(self.request, TEST_DATA_2)
|
|
name_col = self.table.columns['name']
|
|
name_col.auto = "form_field"
|
|
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
name_cell.inline_edit_mod = True
|
|
|
|
# Check if in-line edit is available in the cell,
|
|
# and is in inline_edit_mod, also column auto must be
|
|
# set as form_field.
|
|
self.assertTrue(name_cell.inline_edit_available)
|
|
self.assertTrue(name_cell.inline_edit_mod)
|
|
self.assertEqual('form_field',
|
|
name_col.auto)
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
resp = http.HttpResponse(name_cell_rendered)
|
|
|
|
self.assertContains(resp,
|
|
'<input class="test" id="name__1" name="name__1"'
|
|
' type="text" value="custom object_1" />',
|
|
count=1, html=True)
|
|
|
|
self.assertContains(resp, '<td', 1)
|
|
self.assertContains(resp, 'inline_edit_available', 1)
|
|
self.assertContains(resp,
|
|
'data-update-url="?action=cell_update&'
|
|
'table=my_table&cell_name=name&obj_id=1"',
|
|
1)
|
|
self.assertContains(resp, 'table_cell_wrapper', 1)
|
|
self.assertContains(resp, 'inline-edit-error', 1)
|
|
self.assertContains(resp, 'inline-edit-form', 1)
|
|
self.assertContains(resp, 'inline-edit-actions', 1)
|
|
self.assertContains(resp, 'inline-edit-submit', 1)
|
|
self.assertContains(resp, 'inline-edit-cancel', 1)
|
|
|
|
def test_inline_edit_mod_checkbox_with_label(self):
|
|
class TempTable(MyTable):
|
|
name = tables.Column(get_name,
|
|
verbose_name="Verbose Name",
|
|
sortable=True,
|
|
form_field=forms.BooleanField(
|
|
required=True,
|
|
label="Verbose Name"),
|
|
form_field_attributes={'class': 'test'},
|
|
update_action=MyUpdateAction)
|
|
|
|
class Meta(object):
|
|
name = "my_table"
|
|
columns = ('id', 'name', 'value', 'optional', 'status')
|
|
|
|
self.table = TempTable(self.request, TEST_DATA_2)
|
|
name_col = self.table.columns['name']
|
|
name_col.auto = "form_field"
|
|
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
name_cell.inline_edit_mod = True
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
resp = http.HttpResponse(name_cell_rendered)
|
|
|
|
self.assertContains(resp,
|
|
'<input checked="checked" class="test" '
|
|
'id="name__1" name="name__1" type="checkbox" '
|
|
'value="custom object_1" />',
|
|
count=1, html=True)
|
|
self.assertContains(resp,
|
|
'<label class="inline-edit-label" for="name__1">'
|
|
'Verbose Name</label>',
|
|
count=1, html=True)
|
|
|
|
def test_table_search_action(self):
|
|
class TempTable(MyTable):
|
|
class Meta(object):
|
|
name = "my_table"
|
|
table_actions = (tables.NameFilterAction,)
|
|
|
|
# with the filter string 2, it should return 2nd item
|
|
action_string = "my_table__filter__q"
|
|
req = self.factory.post('/my_url/', {action_string: '2'})
|
|
self.table = TempTable(req, TEST_DATA)
|
|
self.assertQuerysetEqual(self.table.get_table_actions(),
|
|
['<NameFilterAction: filter>'])
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_2'],
|
|
transform=six.text_type)
|
|
|
|
# with empty filter string, it should return all data
|
|
req = self.factory.post('/my_url/', {action_string: ''})
|
|
self.table = TempTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_1',
|
|
'FakeObject: object_2',
|
|
'FakeObject: object_3',
|
|
u'FakeObject: öbject_4'],
|
|
transform=six.text_type)
|
|
|
|
# with unknown value it should return empty list
|
|
req = self.factory.post('/my_url/', {action_string: 'horizon'})
|
|
self.table = TempTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data, [])
|
|
|
|
def test_inline_edit_mod_textarea(self):
|
|
class TempTable(MyTable):
|
|
name = tables.Column(get_name,
|
|
verbose_name="Verbose Name",
|
|
sortable=True,
|
|
form_field=forms.CharField(
|
|
widget=forms.Textarea(),
|
|
required=False),
|
|
form_field_attributes={'class': 'test'},
|
|
update_action=MyUpdateAction)
|
|
|
|
class Meta(object):
|
|
name = "my_table"
|
|
columns = ('id', 'name', 'value', 'optional', 'status')
|
|
|
|
self.table = TempTable(self.request, TEST_DATA_2)
|
|
name_col = self.table.columns['name']
|
|
name_col.auto = "form_field"
|
|
|
|
row = self.table.get_rows()[0]
|
|
name_cell = row.cells['name']
|
|
name_cell.inline_edit_mod = True
|
|
|
|
# Check if is cell is rendered correctly.
|
|
name_cell_rendered = name_cell.render()
|
|
resp = http.HttpResponse(name_cell_rendered)
|
|
|
|
self.assertContains(resp,
|
|
'<textarea class="test" cols="40" id="name__1" '
|
|
'name="name__1" rows="10">\r\ncustom object_1'
|
|
'</textarea>',
|
|
count=1, html=True)
|
|
|
|
def test_table_actions(self):
|
|
# Single object action
|
|
action_string = "my_table__delete__1"
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'delete', '1'),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("http://example.com/?ids=1", handled["location"])
|
|
|
|
# Batch action (without toggle) conjugation behavior
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, TEST_DATA_3)
|
|
toggle_action = self.table.get_row_actions(TEST_DATA_3[0])[2]
|
|
self.assertEqual("Batch Item",
|
|
six.text_type(toggle_action.verbose_name))
|
|
|
|
# Batch action with custom help text
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, TEST_DATA_3)
|
|
toggle_action = self.table.get_row_actions(TEST_DATA_3[0])[4]
|
|
self.assertEqual("BatchHelp Item",
|
|
six.text_type(toggle_action.verbose_name))
|
|
|
|
# Single object toggle action
|
|
# GET page - 'up' to 'down'
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, TEST_DATA_3)
|
|
self.assertEqual(5, len(self.table.get_row_actions(TEST_DATA_3[0])))
|
|
toggle_action = self.table.get_row_actions(TEST_DATA_3[0])[3]
|
|
self.assertEqual("Down Item",
|
|
six.text_type(toggle_action.verbose_name))
|
|
|
|
# Toggle from status 'up' to 'down'
|
|
# POST page
|
|
action_string = "my_table__toggle__1"
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'toggle', '1'),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("/my_url/", handled["location"])
|
|
self.assertEqual(u"Downed Item: object_1",
|
|
list(req._messages)[0].message)
|
|
|
|
# Toggle from status 'down' to 'up'
|
|
# GET page - 'down' to 'up'
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
self.assertEqual(4, len(self.table.get_row_actions(TEST_DATA_2[0])))
|
|
toggle_action = self.table.get_row_actions(TEST_DATA_2[0])[2]
|
|
self.assertEqual("Up Item", six.text_type(toggle_action.verbose_name))
|
|
|
|
# POST page
|
|
action_string = "my_table__toggle__2"
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'toggle', '2'),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("/my_url/", handled["location"])
|
|
self.assertEqual(u"Upped Item: object_2",
|
|
list(req._messages)[0].message)
|
|
|
|
# there are underscore in object-id.
|
|
# (because swift support custom object id)
|
|
action_string = "my_table__toggle__2__33__$$"
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'toggle', '2__33__$$'),
|
|
self.table.parse_action(action_string))
|
|
|
|
# Multiple object action
|
|
action_string = "my_table__delete"
|
|
req = self.factory.post('/my_url/', {'action': action_string,
|
|
'object_ids': [1, 2]})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'delete', None),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("http://example.com/?ids=1,2", handled["location"])
|
|
|
|
# Action with nothing selected
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'delete', None),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertEqual("Please select a row before taking that action.",
|
|
list(req._messages)[0].message)
|
|
|
|
# Action with specific id and multiple ids favors single id
|
|
action_string = "my_table__delete__3"
|
|
req = self.factory.post('/my_url/', {'action': action_string,
|
|
'object_ids': [1, 2]})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
self.assertEqual(('my_table', 'delete', '3'),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("http://example.com/?ids=3",
|
|
handled["location"])
|
|
|
|
# At least one object in table
|
|
# BatchAction is available
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
self.assertQuerysetEqual(self.table.get_table_actions(),
|
|
['<MyFilterAction: filter>',
|
|
'<MyAction: delete>',
|
|
'<MyBatchAction: batch>',
|
|
'<MyBatchActionWithHelpText: batch_help>'])
|
|
|
|
# Zero objects in table
|
|
# BatchAction not available
|
|
req = self.factory.get('/my_url/')
|
|
self.table = MyTable(req, None)
|
|
self.assertQuerysetEqual(self.table.get_table_actions(),
|
|
['<MyFilterAction: filter>',
|
|
'<MyAction: delete>'])
|
|
|
|
# Filtering
|
|
action_string = "my_table__filter__q"
|
|
req = self.factory.post('/my_url/', {action_string: '2'})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_2'],
|
|
transform=six.text_type)
|
|
|
|
# Ensure filtering respects the request method, e.g. no filter here
|
|
req = self.factory.get('/my_url/', {action_string: '2'})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_1',
|
|
'FakeObject: object_2',
|
|
'FakeObject: object_3',
|
|
u'FakeObject: öbject_4'],
|
|
transform=six.text_type)
|
|
|
|
# Updating and preemptive actions
|
|
params = {"table": "my_table", "action": "row_update", "obj_id": "1"}
|
|
req = self.factory.get('/my_url/',
|
|
params,
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.table = MyTable(req)
|
|
resp = self.table.maybe_preempt()
|
|
self.assertEqual(200, resp.status_code)
|
|
# Make sure the data returned differs from the original
|
|
self.assertContains(resp, "my_table__row__1")
|
|
self.assertContains(resp, "status_down")
|
|
|
|
# Verify that we don't get a response for a valid action with the
|
|
# wrong method.
|
|
params = {"table": "my_table", "action": "delete", "obj_id": "1"}
|
|
req = self.factory.get('/my_url/', params)
|
|
self.table = MyTable(req)
|
|
resp = self.table.maybe_preempt()
|
|
self.assertIsNone(resp)
|
|
resp = self.table.maybe_handle()
|
|
self.assertIsNone(resp)
|
|
|
|
# Verbose names
|
|
table_actions = self.table.get_table_actions()
|
|
self.assertEqual("Filter",
|
|
six.text_type(table_actions[0].verbose_name))
|
|
self.assertEqual("Delete Me",
|
|
six.text_type(table_actions[1].verbose_name))
|
|
|
|
row_actions = self.table.get_row_actions(TEST_DATA[0])
|
|
self.assertEqual("Delete Me",
|
|
six.text_type(row_actions[0].verbose_name))
|
|
self.assertEqual("Log In",
|
|
six.text_type(row_actions[1].verbose_name))
|
|
|
|
def test_server_filtering(self):
|
|
filter_value_param = "my_table__filter__q"
|
|
filter_field_param = '%s_field' % filter_value_param
|
|
|
|
# Server Filtering
|
|
req = self.factory.post('/my_url/')
|
|
req.session[filter_value_param] = '2'
|
|
req.session[filter_field_param] = 'name'
|
|
self.table = MyServerFilterTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_2'],
|
|
transform=six.text_type)
|
|
|
|
# Ensure API filtering does not filter on server, e.g. no filter here
|
|
req = self.factory.post('/my_url/')
|
|
req.session[filter_value_param] = 'up'
|
|
req.session[filter_field_param] = 'status'
|
|
self.table = MyServerFilterTable(req, TEST_DATA)
|
|
handled = self.table.maybe_handle()
|
|
self.assertIsNone(handled)
|
|
self.assertQuerysetEqual(self.table.filtered_data,
|
|
['FakeObject: object_1',
|
|
'FakeObject: object_2',
|
|
'FakeObject: object_3',
|
|
u'FakeObject: öbject_4'],
|
|
transform=six.text_type)
|
|
|
|
def test_inline_edit_update_action_get_non_ajax(self):
|
|
# Non ajax inline edit request should return None.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.get(url, {})
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
handled = self.table.maybe_preempt()
|
|
# Checking the response header.
|
|
self.assertIsNone(handled)
|
|
|
|
def test_inline_edit_update_action_get(self):
|
|
# Get request should return td field with data.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.get(url, {},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
handled = self.table.maybe_preempt()
|
|
# Checking the response header.
|
|
self.assertEqual(200, handled.status_code)
|
|
# Checking the response content.
|
|
resp = handled
|
|
self.assertContains(resp, '<td', 1)
|
|
self.assertContains(resp, 'inline_edit_available', 1)
|
|
self.assertContains(
|
|
resp,
|
|
'data-update-url="/my_url/?action=cell_update&'
|
|
'table=my_table&cell_name=name&obj_id=1"',
|
|
1)
|
|
self.assertContains(resp, 'table_cell_wrapper', 1)
|
|
self.assertContains(resp, 'table_cell_data_wrapper', 1)
|
|
self.assertContains(resp, 'table_cell_action', 1)
|
|
self.assertContains(resp, 'ajax-inline-edit', 1)
|
|
|
|
def test_inline_edit_update_action_get_not_allowed(self):
|
|
# Name column has required validation, sending blank
|
|
# will return error.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.post(url, {})
|
|
self.table = MyTableNotAllowedInlineEdit(req, TEST_DATA_2)
|
|
handled = self.table.maybe_preempt()
|
|
# Checking the response header.
|
|
self.assertEqual(401, handled.status_code)
|
|
|
|
def test_inline_edit_update_action_get_inline_edit_mod(self):
|
|
# Get request in inline_edit_mode should return td with form field.
|
|
url = ('/my_url/?inline_edit_mod=true&action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.get(url, {},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
handled = self.table.maybe_preempt()
|
|
# Checking the response header.
|
|
self.assertEqual(200, handled.status_code)
|
|
# Checking the response content.
|
|
resp = handled
|
|
self.assertContains(resp,
|
|
'<input class="test" id="name__1" name="name__1"'
|
|
' type="text" value="custom object_1" />',
|
|
count=1, html=True)
|
|
|
|
self.assertContains(resp, '<td', 1)
|
|
self.assertContains(resp, 'inline_edit_available', 1)
|
|
self.assertContains(
|
|
resp,
|
|
'data-update-url="/my_url/?action=cell_update&'
|
|
'table=my_table&cell_name=name&obj_id=1"',
|
|
1)
|
|
self.assertContains(resp, 'table_cell_wrapper', 1)
|
|
self.assertContains(resp, 'inline-edit-error', 1)
|
|
self.assertContains(resp, 'inline-edit-form', 1)
|
|
self.assertContains(resp, 'inline-edit-actions', 1)
|
|
self.assertContains(resp, '<button', 2)
|
|
self.assertContains(resp, 'inline-edit-submit', 1)
|
|
self.assertContains(resp, 'inline-edit-cancel', 1)
|
|
|
|
def test_inline_edit_update_action_post(self):
|
|
# Post request should invoke the cell update table action.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.post(url, {'name__1': 'test_name'})
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
# checking the response header
|
|
handled = self.table.maybe_preempt()
|
|
self.assertEqual(200, handled.status_code)
|
|
|
|
def test_inline_edit_update_action_post_not_allowed(self):
|
|
# Post request should invoke the cell update table action.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.post(url, {'name__1': 'test_name'})
|
|
self.table = MyTableNotAllowedInlineEdit(req, TEST_DATA_2)
|
|
# checking the response header
|
|
handled = self.table.maybe_preempt()
|
|
self.assertEqual(401, handled.status_code)
|
|
|
|
def test_inline_edit_update_action_post_validation_error(self):
|
|
# Name column has required validation, sending blank
|
|
# will return error.
|
|
url = ('/my_url/?action=cell_update'
|
|
'&table=my_table&cell_name=name&obj_id=1')
|
|
req = self.factory.post(url, {})
|
|
self.table = MyTable(req, TEST_DATA_2)
|
|
handled = self.table.maybe_preempt()
|
|
# Checking the response header.
|
|
self.assertEqual(400, handled.status_code)
|
|
self.assertEqual(('Content-Type', 'application/json'),
|
|
handled._headers['content-type'])
|
|
# Checking the response content.
|
|
resp = handled
|
|
self.assertContains(resp,
|
|
'"message": "This field is required."',
|
|
count=1, status_code=400)
|
|
|
|
def test_column_uniqueness(self):
|
|
table1 = MyTable(self.request)
|
|
table2 = MyTable(self.request)
|
|
# Regression test for launchpad bug 964345.
|
|
self.assertNotEqual(id(table1), id(table2))
|
|
self.assertNotEqual(id(table1.columns), id(table2.columns))
|
|
t1cols = list(table1.columns.values())
|
|
t2cols = list(table2.columns.values())
|
|
self.assertEqual(t1cols[0].name, t2cols[0].name)
|
|
self.assertNotEqual(id(t1cols[0]), id(t2cols[0]))
|
|
self.assertNotEqual(id(t1cols[0].table),
|
|
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>')
|
|
|
|
# Even if "average" summation method is specified,
|
|
# we have summation fields but no value is provided
|
|
# if the provided data cannot be summed.
|
|
table = MyTable(self.request, TEST_DATA)
|
|
res = http.HttpResponse(table.render())
|
|
self.assertContains(res, '<tr class="summation"')
|
|
self.assertNotContains(res, '<td>3.0</td>')
|
|
self.assertNotContains(res, '<td>6</td>')
|
|
|
|
def test_table_action_attributes(self):
|
|
table = MyTable(self.request, TEST_DATA)
|
|
self.assertTrue(table.has_actions)
|
|
self.assertTrue(table.needs_form_wrapper)
|
|
res = http.HttpResponse(table.render())
|
|
self.assertContains(res, "<form")
|
|
|
|
table = MyTable(self.request, TEST_DATA, needs_form_wrapper=False)
|
|
self.assertTrue(table.has_actions)
|
|
self.assertFalse(table.needs_form_wrapper)
|
|
res = http.HttpResponse(table.render())
|
|
self.assertNotContains(res, "<form")
|
|
|
|
table = NoActionsTable(self.request, TEST_DATA)
|
|
self.assertFalse(table.has_actions)
|
|
self.assertFalse(table.needs_form_wrapper)
|
|
res = http.HttpResponse(table.render())
|
|
self.assertNotContains(res, "<form")
|
|
|
|
def test_table_actions_not_allowed_hide_multiselect(self):
|
|
table = DisabledActionsTable(self.request, TEST_DATA)
|
|
self.assertFalse(table.has_actions)
|
|
self.assertFalse(table.needs_form_wrapper)
|
|
res = http.HttpResponse(table.render())
|
|
self.assertContains(res, "multi_select_column hidden")
|
|
|
|
def test_table_action_object_display_is_id(self):
|
|
action_string = "my_table__toggle__1"
|
|
req = self.factory.post('/my_url/', {'action': action_string})
|
|
self.table = MyTable(req, TEST_DATA)
|
|
|
|
self.mox.StubOutWithMock(self.table, 'get_object_display')
|
|
self.table.get_object_display(IsA(FakeObject)).AndReturn(None)
|
|
self.mox.ReplayAll()
|
|
|
|
self.assertEqual(('my_table', 'toggle', '1'),
|
|
self.table.parse_action(action_string))
|
|
handled = self.table.maybe_handle()
|
|
self.assertEqual(302, handled.status_code)
|
|
self.assertEqual("/my_url/", handled["location"])
|
|
self.assertEqual(u"Downed Item: 1",
|
|
list(req._messages)[0].message)
|
|
|
|
def test_table_column_can_be_selected(self):
|
|
self.table = MyTableSelectable(self.request, TEST_DATA_6)
|
|
# non selectable row
|
|
row = self.table.get_rows()[0]
|
|
# selectable
|
|
row1 = self.table.get_rows()[1]
|
|
row2 = 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))
|
|
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)
|
|
# classes
|
|
self.assertEqual("green blue sortable anchor normal_column",
|
|
value_col.get_final_attrs().get('class', ""))
|
|
|
|
self.assertQuerysetEqual(row.get_cells(),
|
|
['<Cell: multi_select, my_table__row__1>',
|
|
'<Cell: id, my_table__row__1>',
|
|
'<Cell: name, my_table__row__1>',
|
|
'<Cell: value, my_table__row__1>',
|
|
'<Cell: status, my_table__row__1>',
|
|
])
|
|
# can_be_selected = False
|
|
self.assertTrue(row.get_cells()[0].data == "")
|
|
# can_be_selected = True
|
|
self.assertIn('checkbox', row1.get_cells()[0].data)
|
|
# status
|
|
cell_status = row.cells['status'].status
|
|
self.assertEqual('status_down',
|
|
row.cells['status'].get_status_class(cell_status))
|
|
|
|
self.assertEqual(row.cells['status'].data, 'down')
|
|
self.assertEqual(row.cells['status'].attrs,
|
|
{'title': 'service is not available',
|
|
'style': 'color:red;cursor:pointer'})
|
|
self.assertEqual(row1.cells['status'].data, 'up')
|
|
self.assertEqual(row1.cells['status'].attrs,
|
|
{'title': 'service is up and running',
|
|
'style': 'color:green;cursor:pointer'})
|
|
self.assertEqual(row2.cells['status'].data, 'standby')
|
|
self.assertEqual(row2.cells['status'].attrs, {})
|
|
|
|
status_rendered = row.cells['status'].render()
|
|
resp = http.HttpResponse(status_rendered)
|
|
self.assertContains(resp, 'style="color:red;cursor:pointer"', 1)
|
|
self.assertContains(resp, 'title="service is not available"', 1)
|
|
|
|
status_rendered = row1.cells['status'].render()
|
|
resp = http.HttpResponse(status_rendered)
|
|
self.assertContains(resp, 'style="color:green;cursor:pointer"', 1)
|
|
self.assertContains(resp, 'title="service is up and running"', 1)
|
|
|
|
# status_choices
|
|
id_col.status = True
|
|
id_col.status_choices = (('1', False), ('2', True))
|
|
cell_status = row.cells['id'].status
|
|
self.assertFalse(cell_status)
|
|
self.assertEqual('status_down',
|
|
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_6)
|
|
row = self.table.get_rows()[0]
|
|
self.assertIn("down", row.cells['status'].value)
|
|
|
|
def test_broken_filter(self):
|
|
class MyTableBrokenFilter(MyTable):
|
|
value = tables.Column('value',
|
|
filters=(defaultfilters.timesince,))
|
|
|
|
value = "not_a_date"
|
|
data = TEST_DATA[0]
|
|
data.value = value
|
|
|
|
table = MyTableBrokenFilter(self.request, [data])
|
|
resp = http.HttpResponse(table.render())
|
|
self.assertContains(resp, value)
|
|
|
|
|
|
class SingleTableView(table_views.DataTableView):
|
|
table_class = MyTable
|
|
name = "Single Table"
|
|
slug = "single"
|
|
template_name = "horizon/common/_detail_table.html"
|
|
|
|
def get_data(self):
|
|
return TEST_DATA
|
|
|
|
|
|
class APIFilterTableView(SingleTableView):
|
|
table_class = MyServerFilterTable
|
|
|
|
|
|
class TableWithPermissions(tables.DataTable):
|
|
id = tables.Column('id')
|
|
|
|
class Meta(object):
|
|
name = "table_with_permissions"
|
|
permissions = ('horizon.test',)
|
|
|
|
|
|
class SingleTableViewWithPermissions(SingleTableView):
|
|
table_class = TableWithPermissions
|
|
|
|
|
|
class MultiTableView(tables.MultiTableView):
|
|
table_classes = (TableWithPermissions, MyTable)
|
|
|
|
def get_table_with_permissions_data(self):
|
|
return TEST_DATA
|
|
|
|
def get_my_table_data(self):
|
|
return TEST_DATA
|
|
|
|
|
|
class DataTableViewTests(test.TestCase):
|
|
def _prepare_view(self, cls, *args, **kwargs):
|
|
req = self.factory.get('/my_url/')
|
|
req.user = self.user
|
|
view = cls()
|
|
view.request = req
|
|
view.args = args
|
|
view.kwargs = kwargs
|
|
return view
|
|
|
|
def test_data_table_view(self):
|
|
view = self._prepare_view(SingleTableView)
|
|
context = view.get_context_data()
|
|
self.assertEqual(SingleTableView.table_class,
|
|
context['table'].__class__)
|
|
|
|
def test_data_table_view_not_authorized(self):
|
|
view = self._prepare_view(SingleTableViewWithPermissions)
|
|
context = view.get_context_data()
|
|
self.assertNotIn('table', context)
|
|
|
|
def test_data_table_view_authorized(self):
|
|
view = self._prepare_view(SingleTableViewWithPermissions)
|
|
self.set_permissions(permissions=['test'])
|
|
context = view.get_context_data()
|
|
self.assertIn('table', context)
|
|
self.assertEqual(SingleTableViewWithPermissions.table_class,
|
|
context['table'].__class__)
|
|
|
|
def test_multi_table_view_not_authorized(self):
|
|
view = self._prepare_view(MultiTableView)
|
|
context = view.get_context_data()
|
|
self.assertEqual(MyTable, context['my_table_table'].__class__)
|
|
self.assertNotIn('table_with_permissions_table', context)
|
|
|
|
def test_multi_table_view_authorized(self):
|
|
view = self._prepare_view(MultiTableView)
|
|
self.set_permissions(permissions=['test'])
|
|
context = view.get_context_data()
|
|
self.assertEqual(MyTable, context['my_table_table'].__class__)
|
|
self.assertEqual(TableWithPermissions,
|
|
context['table_with_permissions_table'].__class__)
|
|
|
|
fil_value_param = "my_table__filter__q"
|
|
fil_field_param = '%s_field' % fil_value_param
|
|
|
|
def _test_filter_setup_view(self, request):
|
|
view = APIFilterTableView()
|
|
view.request = request
|
|
view.kwargs = {}
|
|
view.handle_server_filter(request)
|
|
return view
|
|
|
|
def test_api_filter_table_view(self):
|
|
req = self.factory.post('/my_url/', {self.fil_value_param: 'up',
|
|
self.fil_field_param: 'status'})
|
|
req.user = self.user
|
|
view = self._test_filter_setup_view(req)
|
|
data = view.get_data()
|
|
context = view.get_context_data()
|
|
self.assertEqual(context['table'].__class__, MyServerFilterTable)
|
|
self.assertQuerysetEqual(data,
|
|
['FakeObject: object_1',
|
|
'FakeObject: object_2',
|
|
'FakeObject: object_3',
|
|
u'FakeObject: öbject_4'],
|
|
transform=six.text_type)
|
|
self.assertEqual(req.session.get(self.fil_value_param), 'up')
|
|
self.assertEqual(req.session.get(self.fil_field_param), 'status')
|
|
|
|
def test_filter_changed_deleted(self):
|
|
req = self.factory.post('/my_url/', {self.fil_value_param: '',
|
|
self.fil_field_param: 'status'})
|
|
req.session[self.fil_value_param] = 'up'
|
|
req.session[self.fil_field_param] = 'status'
|
|
req.user = self.user
|
|
view = self._test_filter_setup_view(req)
|
|
context = view.get_context_data()
|
|
self.assertEqual(context['table'].__class__, MyServerFilterTable)
|
|
self.assertEqual(req.session.get(self.fil_value_param), '')
|
|
self.assertEqual(req.session.get(self.fil_field_param), 'status')
|
|
|
|
def test_filter_changed_nothing_sent(self):
|
|
req = self.factory.post('/my_url/', {})
|
|
req.session[self.fil_value_param] = 'up'
|
|
req.session[self.fil_field_param] = 'status'
|
|
req.user = self.user
|
|
view = self._test_filter_setup_view(req)
|
|
context = view.get_context_data()
|
|
self.assertEqual(context['table'].__class__, MyServerFilterTable)
|
|
self.assertEqual(req.session.get(self.fil_value_param), 'up')
|
|
self.assertEqual(req.session.get(self.fil_field_param), 'status')
|
|
|
|
def test_filter_changed_new_filter_sent(self):
|
|
req = self.factory.post('/my_url/', {self.fil_value_param: 'down',
|
|
self.fil_field_param: 'status'})
|
|
req.session[self.fil_value_param] = 'up'
|
|
req.session[self.fil_field_param] = 'status'
|
|
req.user = self.user
|
|
view = self._test_filter_setup_view(req)
|
|
context = view.get_context_data()
|
|
self.assertEqual(context['table'].__class__, MyServerFilterTable)
|
|
self.assertEqual(req.session.get(self.fil_value_param), 'down')
|
|
self.assertEqual(req.session.get(self.fil_field_param), 'status')
|
|
|
|
|
|
class FormsetTableTests(test.TestCase):
|
|
|
|
def test_populate(self):
|
|
"""Create a FormsetDataTable and populate it with data."""
|
|
|
|
class TableForm(forms.Form):
|
|
name = forms.CharField()
|
|
value = forms.IntegerField()
|
|
|
|
TableFormset = forms.formsets.formset_factory(TableForm, extra=0)
|
|
|
|
class Table(table_formset.FormsetDataTable):
|
|
formset_class = TableFormset
|
|
|
|
name = tables.Column('name')
|
|
value = tables.Column('value')
|
|
|
|
class Meta(object):
|
|
name = 'table'
|
|
|
|
table = Table(self.request)
|
|
table.data = TEST_DATA_4
|
|
formset = table.get_formset()
|
|
self.assertEqual(2, len(formset))
|
|
form = formset[0]
|
|
form_data = form.initial
|
|
self.assertEqual('object_1', form_data['name'])
|
|
self.assertEqual(2, form_data['value'])
|