2019-09-10 00:54:28 +00:00
|
|
|
# Created By: Virgil Dupras
|
|
|
|
# Created On: 2008-08-12
|
|
|
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
|
|
#
|
|
|
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
|
|
# which should be included with this package. The terms are also available at
|
|
|
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
|
|
|
|
from ..testutil import CallLogger, eq_
|
|
|
|
from ..gui.table import Table, GUITable, Row
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
class TestRow(Row):
|
2020-06-27 06:08:12 +00:00
|
|
|
__test__ = False
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __init__(self, table, index, is_new=False):
|
|
|
|
Row.__init__(self, table)
|
|
|
|
self.is_new = is_new
|
|
|
|
self._index = index
|
|
|
|
|
|
|
|
def load(self):
|
2021-08-21 21:25:33 +00:00
|
|
|
# Does nothing for test
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
def save(self):
|
|
|
|
self.is_new = False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def index(self):
|
|
|
|
return self._index
|
|
|
|
|
|
|
|
|
|
|
|
class TestGUITable(GUITable):
|
2020-06-27 06:08:12 +00:00
|
|
|
__test__ = False
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __init__(self, rowcount, viewclass=CallLogger):
|
|
|
|
GUITable.__init__(self)
|
|
|
|
self.view = viewclass()
|
|
|
|
self.view.model = self
|
|
|
|
self.rowcount = rowcount
|
|
|
|
self.updated_rows = None
|
|
|
|
|
|
|
|
def _do_add(self):
|
|
|
|
return TestRow(self, len(self), is_new=True), len(self)
|
|
|
|
|
|
|
|
def _is_edited_new(self):
|
|
|
|
return self.edited is not None and self.edited.is_new
|
|
|
|
|
|
|
|
def _fill(self):
|
|
|
|
for i in range(self.rowcount):
|
|
|
|
self.append(TestRow(self, i))
|
|
|
|
|
|
|
|
def _update_selection(self):
|
|
|
|
self.updated_rows = self.selected_rows[:]
|
|
|
|
|
|
|
|
|
|
|
|
def table_with_footer():
|
|
|
|
table = Table()
|
|
|
|
table.append(TestRow(table, 0))
|
|
|
|
footer = TestRow(table, 1)
|
|
|
|
table.footer = footer
|
|
|
|
return table, footer
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def table_with_header():
|
|
|
|
table = Table()
|
|
|
|
table.append(TestRow(table, 1))
|
|
|
|
header = TestRow(table, 0)
|
|
|
|
table.header = header
|
|
|
|
return table, header
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
# --- Tests
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_allow_edit_when_attr_is_property_with_fset():
|
|
|
|
# When a row has a property that has a fset, by default, make that cell editable.
|
|
|
|
class TestRow(Row):
|
|
|
|
@property
|
|
|
|
def foo(self):
|
2021-08-21 21:25:33 +00:00
|
|
|
# property only for existence checks
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def bar(self):
|
2021-08-21 21:25:33 +00:00
|
|
|
# property only for existence checks
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@bar.setter
|
|
|
|
def bar(self, value):
|
2021-08-21 21:25:33 +00:00
|
|
|
# setter only for existence checks
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
row = TestRow(Table())
|
2020-01-01 02:16:27 +00:00
|
|
|
assert row.can_edit_cell("bar")
|
|
|
|
assert not row.can_edit_cell("foo")
|
|
|
|
assert not row.can_edit_cell("baz") # doesn't exist, can't edit
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def test_can_edit_prop_has_priority_over_fset_checks():
|
|
|
|
# When a row has a cen_edit_* property, it's the result of that property that is used, not the
|
|
|
|
# result of a fset check.
|
|
|
|
class TestRow(Row):
|
|
|
|
@property
|
|
|
|
def bar(self):
|
2021-08-21 21:25:33 +00:00
|
|
|
# property only for existence checks
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@bar.setter
|
|
|
|
def bar(self, value):
|
2021-08-21 21:25:33 +00:00
|
|
|
# setter only for existence checks
|
2019-09-10 00:54:28 +00:00
|
|
|
pass
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
can_edit_bar = False
|
|
|
|
|
|
|
|
row = TestRow(Table())
|
2020-01-01 02:16:27 +00:00
|
|
|
assert not row.can_edit_cell("bar")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def test_in():
|
|
|
|
# When a table is in a list, doing "in list" with another instance returns false, even if
|
|
|
|
# they're the same as lists.
|
|
|
|
table = Table()
|
|
|
|
some_list = [table]
|
|
|
|
assert Table() not in some_list
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_del_all():
|
|
|
|
# Removing all rows doesn't crash when doing the footer check.
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
del table[:]
|
|
|
|
assert table.footer is None
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_del_row():
|
|
|
|
# Removing the footer row sets it to None
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
del table[-1]
|
|
|
|
assert table.footer is None
|
|
|
|
eq_(len(table), 1)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_is_appened_to_table():
|
|
|
|
# A footer is appended at the table's bottom
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
eq_(len(table), 2)
|
|
|
|
assert table[1] is footer
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_remove():
|
|
|
|
# remove() on footer sets it to None
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
table.remove(footer)
|
|
|
|
assert table.footer is None
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_replaces_old_footer():
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
other = Row(table)
|
|
|
|
table.footer = other
|
|
|
|
assert table.footer is other
|
|
|
|
eq_(len(table), 2)
|
|
|
|
assert table[1] is other
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_rows_and_row_count():
|
|
|
|
# rows() and row_count() ignore footer.
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
eq_(table.row_count, 1)
|
|
|
|
eq_(table.rows, table[:-1])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_setting_to_none_removes_old_one():
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
table.footer = None
|
|
|
|
assert table.footer is None
|
|
|
|
eq_(len(table), 1)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_stays_there_on_append():
|
|
|
|
# Appending another row puts it above the footer
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
table.append(Row(table))
|
|
|
|
eq_(len(table), 3)
|
|
|
|
assert table[2] is footer
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_footer_stays_there_on_insert():
|
|
|
|
# Inserting another row puts it above the footer
|
|
|
|
table, footer = table_with_footer()
|
|
|
|
table.insert(3, Row(table))
|
|
|
|
eq_(len(table), 3)
|
|
|
|
assert table[2] is footer
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_del_all():
|
|
|
|
# Removing all rows doesn't crash when doing the header check.
|
|
|
|
table, header = table_with_header()
|
|
|
|
del table[:]
|
|
|
|
assert table.header is None
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_del_row():
|
|
|
|
# Removing the header row sets it to None
|
|
|
|
table, header = table_with_header()
|
|
|
|
del table[0]
|
|
|
|
assert table.header is None
|
|
|
|
eq_(len(table), 1)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_is_inserted_in_table():
|
|
|
|
# A header is inserted at the table's top
|
|
|
|
table, header = table_with_header()
|
|
|
|
eq_(len(table), 2)
|
|
|
|
assert table[0] is header
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_remove():
|
|
|
|
# remove() on header sets it to None
|
|
|
|
table, header = table_with_header()
|
|
|
|
table.remove(header)
|
|
|
|
assert table.header is None
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_replaces_old_header():
|
|
|
|
table, header = table_with_header()
|
|
|
|
other = Row(table)
|
|
|
|
table.header = other
|
|
|
|
assert table.header is other
|
|
|
|
eq_(len(table), 2)
|
|
|
|
assert table[0] is other
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_rows_and_row_count():
|
|
|
|
# rows() and row_count() ignore header.
|
|
|
|
table, header = table_with_header()
|
|
|
|
eq_(table.row_count, 1)
|
|
|
|
eq_(table.rows, table[1:])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_setting_to_none_removes_old_one():
|
|
|
|
table, header = table_with_header()
|
|
|
|
table.header = None
|
|
|
|
assert table.header is None
|
|
|
|
eq_(len(table), 1)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_header_stays_there_on_insert():
|
|
|
|
# Inserting another row at the top puts it below the header
|
|
|
|
table, header = table_with_header()
|
|
|
|
table.insert(0, Row(table))
|
|
|
|
eq_(len(table), 3)
|
|
|
|
assert table[0] is header
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_refresh_view_on_refresh():
|
|
|
|
# If refresh_view is not False, we refresh the table's view on refresh()
|
|
|
|
table = TestGUITable(1)
|
|
|
|
table.refresh()
|
2020-01-01 02:16:27 +00:00
|
|
|
table.view.check_gui_calls(["refresh"])
|
2019-09-10 00:54:28 +00:00
|
|
|
table.view.clear_calls()
|
|
|
|
table.refresh(refresh_view=False)
|
|
|
|
table.view.check_gui_calls([])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_restore_selection():
|
|
|
|
# By default, after a refresh, selection goes on the last row
|
|
|
|
table = TestGUITable(10)
|
|
|
|
table.refresh()
|
|
|
|
eq_(table.selected_indexes, [9])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_restore_selection_after_cancel_edits():
|
|
|
|
# _restore_selection() is called after cancel_edits(). Previously, only _update_selection would
|
|
|
|
# be called.
|
|
|
|
class MyTable(TestGUITable):
|
|
|
|
def _restore_selection(self, previous_selection):
|
|
|
|
self.selected_indexes = [6]
|
|
|
|
|
|
|
|
table = MyTable(10)
|
|
|
|
table.refresh()
|
|
|
|
table.add()
|
|
|
|
table.cancel_edits()
|
|
|
|
eq_(table.selected_indexes, [6])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_restore_selection_with_previous_selection():
|
|
|
|
# By default, we try to restore the selection that was there before a refresh
|
|
|
|
table = TestGUITable(10)
|
|
|
|
table.refresh()
|
|
|
|
table.selected_indexes = [2, 4]
|
|
|
|
table.refresh()
|
|
|
|
eq_(table.selected_indexes, [2, 4])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_restore_selection_custom():
|
|
|
|
# After a _fill() called, the virtual _restore_selection() is called so that it's possible for a
|
|
|
|
# GUITable subclass to customize its post-refresh selection behavior.
|
|
|
|
class MyTable(TestGUITable):
|
|
|
|
def _restore_selection(self, previous_selection):
|
|
|
|
self.selected_indexes = [6]
|
|
|
|
|
|
|
|
table = MyTable(10)
|
|
|
|
table.refresh()
|
|
|
|
eq_(table.selected_indexes, [6])
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_row_cell_value():
|
|
|
|
# *_cell_value() correctly mangles attrnames that are Python reserved words.
|
|
|
|
row = Row(Table())
|
2020-01-01 02:16:27 +00:00
|
|
|
row.from_ = "foo"
|
|
|
|
eq_(row.get_cell_value("from"), "foo")
|
|
|
|
row.set_cell_value("from", "bar")
|
|
|
|
eq_(row.get_cell_value("from"), "bar")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def test_sort_table_also_tries_attributes_without_underscores():
|
|
|
|
# When determining a sort key, after having unsuccessfully tried the attribute with the,
|
|
|
|
# underscore, try the one without one.
|
|
|
|
table = Table()
|
|
|
|
row1 = Row(table)
|
2020-01-01 02:16:27 +00:00
|
|
|
row1._foo = "a" # underscored attr must be checked first
|
|
|
|
row1.foo = "b"
|
|
|
|
row1.bar = "c"
|
2019-09-10 00:54:28 +00:00
|
|
|
row2 = Row(table)
|
2020-01-01 02:16:27 +00:00
|
|
|
row2._foo = "b"
|
|
|
|
row2.foo = "a"
|
|
|
|
row2.bar = "b"
|
2019-09-10 00:54:28 +00:00
|
|
|
table.append(row1)
|
|
|
|
table.append(row2)
|
2020-01-01 02:16:27 +00:00
|
|
|
table.sort_by("foo")
|
2019-09-10 00:54:28 +00:00
|
|
|
assert table[0] is row1
|
|
|
|
assert table[1] is row2
|
2020-01-01 02:16:27 +00:00
|
|
|
table.sort_by("bar")
|
2019-09-10 00:54:28 +00:00
|
|
|
assert table[0] is row2
|
|
|
|
assert table[1] is row1
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_sort_table_updates_selection():
|
|
|
|
table = TestGUITable(10)
|
|
|
|
table.refresh()
|
|
|
|
table.select([2, 4])
|
2020-01-01 02:16:27 +00:00
|
|
|
table.sort_by("index", desc=True)
|
2019-09-10 00:54:28 +00:00
|
|
|
# Now, the updated rows should be 7 and 5
|
|
|
|
eq_(len(table.updated_rows), 2)
|
|
|
|
r1, r2 = table.updated_rows
|
|
|
|
eq_(r1.index, 7)
|
|
|
|
eq_(r2.index, 5)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_sort_table_with_footer():
|
|
|
|
# Sorting a table with a footer keeps it at the bottom
|
|
|
|
table, footer = table_with_footer()
|
2020-01-01 02:16:27 +00:00
|
|
|
table.sort_by("index", desc=True)
|
2019-09-10 00:54:28 +00:00
|
|
|
assert table[-1] is footer
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_sort_table_with_header():
|
|
|
|
# Sorting a table with a header keeps it at the top
|
|
|
|
table, header = table_with_header()
|
2020-01-01 02:16:27 +00:00
|
|
|
table.sort_by("index", desc=True)
|
2019-09-10 00:54:28 +00:00
|
|
|
assert table[0] is header
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def test_add_with_view_that_saves_during_refresh():
|
|
|
|
# Calling save_edits during refresh() called by add() is ignored.
|
|
|
|
class TableView(CallLogger):
|
|
|
|
def refresh(self):
|
|
|
|
self.model.save_edits()
|
|
|
|
|
|
|
|
table = TestGUITable(10, viewclass=TableView)
|
|
|
|
table.add()
|
2020-01-01 02:16:27 +00:00
|
|
|
assert table.edited is not None # still in edit mode
|