db: Create a database row Filter

This filter takes a set of primary keys for rows that should be visible
during filtering. Passing None as a value means that all rows are shown.
It also has functions for adding or removing individual rows from the
filter.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2022-09-23 10:16:21 -04:00
parent 2eef68f76f
commit 651f24672b
2 changed files with 191 additions and 0 deletions

View File

@ -2,6 +2,7 @@
"""Base classes for database objects."""
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
class Row(GObject.GObject):
@ -31,3 +32,76 @@ class Row(GObject.GObject):
def primary_key(self) -> None:
"""Get the primary key for this row."""
raise NotImplementedError
class Filter(Gtk.Filter):
"""A Filter that can be used to search playlists."""
n_keys = GObject.Property(type=int)
def __init__(self, keys: set | None = None, **kwargs):
"""Set up our Filter."""
super().__init__(**kwargs)
self._keys = keys
self.n_keys = len(keys) if keys is not None else -1
def __sub__(self, rhs: Gtk.Filter) -> set[int]:
"""Subtract two Filters and return the result."""
match (self._keys, rhs._keys):
case (None, _): return None
case (_, None): return self._keys
case (_, _): return self._keys - rhs._keys
def __find_change(self, keys: set[any] | None) -> Gtk.FilterChange | None:
if keys == self._keys:
return None
elif keys is None:
return Gtk.FilterChange.LESS_STRICT
elif self._keys is None:
return Gtk.FilterChange.MORE_STRICT
elif keys.issuperset(self._keys):
return Gtk.FilterChange.LESS_STRICT
elif keys.issubset(self._keys):
return Gtk.FilterChange.MORE_STRICT
return Gtk.FilterChange.DIFFERENT
def changed(self, how: Gtk.FilterChange) -> None:
"""Notify that the filter has changed."""
self.n_keys = len(self._keys) if self._keys is not None else -1
super().changed(how)
def do_get_strictness(self) -> Gtk.FilterMatch:
"""Get the strictness of the filter."""
if self._keys is None:
return Gtk.FilterMatch.ALL
if len(self._keys) == 0:
return Gtk.FilterMatch.NONE
return Gtk.FilterMatch.SOME
def do_match(self, row: Row) -> bool:
"""Check if the Row matches the filter."""
return self._keys is None or row.primary_key in self._keys
def add_row(self, row: Row) -> None:
"""Add a Row to the Filter."""
if self._keys is not None:
self._keys.add(row.primary_key)
self.changed(Gtk.FilterChange.LESS_STRICT)
def remove_row(self, row: Row) -> None:
"""Remove a Row from the Filter."""
if self._keys is not None:
self._keys.discard(row.primary_key)
self.changed(Gtk.FilterChange.MORE_STRICT)
@property
def keys(self) -> set[any]:
"""Return the set of matching primary keys."""
return self._keys
@keys.setter
def keys(self, keys: set[any] | None) -> None:
"""Set the matching primary keys."""
if (how := self.__find_change(keys)) is not None:
self._keys = keys
self.changed(how)

View File

@ -6,6 +6,7 @@ import emmental.db.table
import tests.util.table
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
class TestRow(unittest.TestCase):
@ -45,3 +46,119 @@ class TestRow(unittest.TestCase):
"""Test deleting a Row."""
self.assertTrue(self.row.delete())
self.table.delete.assert_called_with(self.row)
@unittest.mock.patch("gi.repository.Gtk.Filter.changed")
class TestFilter(unittest.TestCase):
"""Tests our database row Filter."""
def setUp(self):
"""Set up common variables."""
self.filter = emmental.db.table.Filter()
self.table = Gio.ListStore()
self.row1 = tests.util.table.MockRow(number=1, table=self.table)
self.row2 = tests.util.table.MockRow(number=2, table=self.table)
def test_init(self, mock_changed: unittest.mock.Mock):
"""Test that the filter is created correctly."""
self.assertIsInstance(self.filter, Gtk.Filter)
self.assertIsNone(self.filter._keys, None)
self.assertEqual(self.filter.n_keys, -1)
filter2 = emmental.db.table.Filter(keys={1, 2, 3})
self.assertSetEqual(filter2._keys, {1, 2, 3})
self.assertEqual(filter2.n_keys, 3)
def test_subtract(self, mock_changed: unittest.mock.Mock):
"""Test subtracting two filters."""
filter2 = emmental.db.table.Filter(keys={2, 3})
self.assertIsNone(self.filter - self.filter)
self.assertIsNone(self.filter - filter2)
self.assertSetEqual(filter2 - self.filter, {2, 3})
self.filter.keys = {1, 2, 3, 4, 5}
self.assertSetEqual(self.filter - filter2, {1, 4, 5})
self.assertSetEqual(filter2 - self.filter, set())
def test_strictness(self, mock_changed: unittest.mock.Mock):
"""Test checking strictness."""
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.ALL)
self.filter._keys = set()
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.NONE)
self.filter._keys = {1, 2, 3}
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
def test_add_row(self, mock_changed: unittest.mock.Mock):
"""Test adding Rows to the filter."""
self.filter.add_row(self.row1)
self.assertIsNone(self.filter.keys)
self.filter.keys = set()
self.filter.add_row(self.row1)
self.assertSetEqual(self.filter.keys, {1})
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, 1)
self.filter.add_row(self.row2)
self.assertSetEqual(self.filter.keys, {1, 2})
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, 2)
def test_remove_row(self, mock_changed: unittest.mock.Mock):
"""Test removing Rows from the filter."""
self.filter.remove_row(self.row1)
mock_changed.assert_not_called()
self.filter.keys = {1, 2}
self.filter.remove_row(self.row1)
self.assertSetEqual(self.filter._keys, {2})
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 1)
mock_changed.reset_mock()
self.filter.remove_row(self.row2)
self.assertSetEqual(self.filter._keys, set())
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 0)
def test_keys(self, mock_changed: unittest.mock.Mock):
"""Test setting and getting the filter keys property."""
self.assertIsNone(self.filter.keys)
self.filter.keys = {1, 2, 3}
self.assertSetEqual(self.filter._keys, {1, 2, 3})
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 3)
mock_changed.reset_mock()
self.filter.keys = {1, 2}
self.assertSetEqual(self.filter.keys, {1, 2})
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 2)
mock_changed.reset_mock()
self.filter.keys = {1, 2}
mock_changed.assert_not_called()
self.filter.keys = {1, 2, 3}
self.assertSetEqual(self.filter.keys, {1, 2, 3})
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.filter.keys = {4, 5, 6}
self.assertSetEqual(self.filter._keys, {4, 5, 6})
mock_changed.assert_called_with(Gtk.FilterChange.DIFFERENT)
self.filter.keys = None
self.assertIsNone(self.filter._keys)
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, -1)
def test_match(self, mock_changed: unittest.mock.Mock):
"""Test matching playlists."""
self.assertTrue(self.filter.match(self.row1))
self.filter.keys = {1, 2, 3}
self.assertTrue(self.filter.match(self.row1))
self.filter.keys = {4, 5, 6}
self.assertFalse(self.filter.match(self.row1))
self.filter.keys = set()
self.assertFalse(self.filter.match(self.row1))