Compare commits
10 Commits
0c66b13209
...
06771ecab6
Author | SHA1 | Date |
---|---|---|
Anna Schumaker | 06771ecab6 | |
Anna Schumaker | 17b2a82e20 | |
Anna Schumaker | c0c516fb70 | |
Anna Schumaker | 3cddde0986 | |
Anna Schumaker | 4f15bde850 | |
Anna Schumaker | 5ee86a9b5e | |
Anna Schumaker | 85c18fb5fe | |
Anna Schumaker | 67b508384c | |
Anna Schumaker | 929beb2a97 | |
Anna Schumaker | f400366210 |
|
@ -21,7 +21,7 @@ from gi.repository import Adw
|
|||
|
||||
MAJOR_VERSION = 3
|
||||
MINOR_VERSION = 0
|
||||
MICRO_VERSION = 3
|
||||
MICRO_VERSION = 4
|
||||
|
||||
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
|
||||
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}"
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import pathlib
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from .media import Medium
|
||||
from .. import format
|
||||
from . import playlist
|
||||
|
@ -23,10 +22,11 @@ class Album(playlist.Playlist):
|
|||
"""Initialize an Album object."""
|
||||
super().__init__(**kwargs)
|
||||
self.add_children(self.table.sql.media,
|
||||
Gtk.CustomFilter.new(self.__match_medium))
|
||||
self.table.get_mediumids(self))
|
||||
|
||||
def __match_medium(self, medium: Medium) -> bool:
|
||||
return medium.albumid == self.albumid and len(medium.name) > 0
|
||||
def add_medium(self, medium: Medium) -> None:
|
||||
"""Add a Medium to this Album."""
|
||||
self.add_child(medium)
|
||||
|
||||
def get_artists(self) -> list[playlist.Playlist]:
|
||||
"""Get a list of artists for this album."""
|
||||
|
@ -36,6 +36,14 @@ class Album(playlist.Playlist):
|
|||
"""Get a list of media for this album."""
|
||||
return self.table.get_media(self)
|
||||
|
||||
def has_medium(self, medium: Medium) -> bool:
|
||||
"""Check if a Medium is from this Album."""
|
||||
return self.has_child(medium)
|
||||
|
||||
def remove_medium(self, medium: Medium) -> None:
|
||||
"""Remove a Medium from this Album."""
|
||||
return self.remove_child(medium)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
"""Get the Album primary key."""
|
||||
|
@ -139,6 +147,11 @@ class Table(playlist.Table):
|
|||
|
||||
def get_media(self, album: Album) -> list[Medium]:
|
||||
"""Get the list of media for this album."""
|
||||
return [self.sql.media.rows.get(id)
|
||||
for id in self.get_mediumids(album)]
|
||||
|
||||
def get_mediumids(self, album: Album) -> set[int]:
|
||||
"""Get the set of mediumids for this album."""
|
||||
rows = self.sql("SELECT mediumid FROM media WHERE albumid=?",
|
||||
album.albumid)
|
||||
return [self.sql.media.rows.get(row["mediumid"]) for row in rows]
|
||||
return {row["mediumid"] for row in rows.fetchall()}
|
||||
|
|
|
@ -19,21 +19,21 @@ class Artist(playlist.Playlist):
|
|||
"""Initialize an Artist object."""
|
||||
super().__init__(**kwargs)
|
||||
self.add_children(self.table.sql.albums,
|
||||
table.Filter(self.table.get_albumids(self)))
|
||||
self.table.get_albumids(self))
|
||||
|
||||
def add_album(self, album: Album) -> None:
|
||||
"""Add an Album to this Artist."""
|
||||
if self.table.add_album(self, album):
|
||||
self.children.get_filter().add_row(album)
|
||||
self.add_child(album)
|
||||
|
||||
def has_album(self, album: Album) -> bool:
|
||||
"""Check if the Artist has this Album."""
|
||||
return self.children.get_filter().match(album)
|
||||
return self.has_child(album)
|
||||
|
||||
def remove_album(self, album: Album) -> None:
|
||||
"""Remove an album from this Artist."""
|
||||
self.children.get_filter().remove_row(album)
|
||||
self.table.remove_album(self, album)
|
||||
self.remove_child(album)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
|
@ -41,7 +41,7 @@ class Artist(playlist.Playlist):
|
|||
return self.artistid
|
||||
|
||||
|
||||
class Filter(table.Filter):
|
||||
class Filter(table.KeySet):
|
||||
"""Custom filter to hide artists without albums."""
|
||||
|
||||
show_all = GObject.Property(type=bool, default=False)
|
||||
|
@ -51,7 +51,7 @@ class Filter(table.Filter):
|
|||
super().__init__(show_all=show_all)
|
||||
self.connect("notify::show-all", self.__notify_show_all)
|
||||
|
||||
def __notify_show_all(self, filter: table.Filter, param) -> None:
|
||||
def __notify_show_all(self, filter: table.KeySet, param) -> None:
|
||||
self.changed(Gtk.FilterChange.LESS_STRICT if self.show_all else
|
||||
Gtk.FilterChange.MORE_STRICT)
|
||||
|
||||
|
@ -66,7 +66,7 @@ class Filter(table.Filter):
|
|||
"""Check if the artist matches the filter."""
|
||||
res = super().do_match(artist)
|
||||
if not self.show_all and res:
|
||||
return artist.children.get_filter().n_keys > 0
|
||||
return artist.child_set.keyset.n_keys > 0
|
||||
return res
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"""A custom Gio.ListModel for working with decades."""
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from .years import Year
|
||||
from . import playlist
|
||||
from . import tracks
|
||||
|
@ -17,15 +16,24 @@ class Decade(playlist.Playlist):
|
|||
"""Initialize a Decade object."""
|
||||
super().__init__(**kwargs)
|
||||
self.add_children(self.table.sql.years,
|
||||
Gtk.CustomFilter.new(self.__match_year))
|
||||
self.table.get_yearids(self))
|
||||
|
||||
def __match_year(self, year: Year) -> bool:
|
||||
return self.decade == year.year // 10 * 10
|
||||
def add_year(self, year: Year) -> None:
|
||||
"""Add a year to this decade."""
|
||||
self.add_child(year)
|
||||
|
||||
def get_years(self) -> list[Year]:
|
||||
"""Get a list of years for this decade."""
|
||||
return self.table.get_years(self)
|
||||
|
||||
def has_year(self, year: Year) -> bool:
|
||||
"""Check if the year is in this decade."""
|
||||
return self.has_child(year)
|
||||
|
||||
def remove_year(self, year: Year) -> None:
|
||||
"""Remove a year from this decade."""
|
||||
self.remove_child(year)
|
||||
|
||||
@property
|
||||
def primary_key(self) -> int:
|
||||
"""Get the primary key of this Decade."""
|
||||
|
@ -90,8 +98,12 @@ class Table(playlist.Table):
|
|||
return self.sql("""SELECT trackid FROM decade_tracks_view
|
||||
WHERE decade=?""", decade.decade)
|
||||
|
||||
def get_years(self, decade: Decade) -> list[Year]:
|
||||
"""Get the list of years for this decade."""
|
||||
def get_yearids(self, decade: Decade) -> set[int]:
|
||||
"""Get the set of years for this decade."""
|
||||
rows = self.sql("SELECT year FROM years WHERE (year / 10 * 10)=?",
|
||||
decade.decade)
|
||||
return [self.sql.years.rows.get(row["year"]) for row in rows]
|
||||
return {row["year"] for row in rows}
|
||||
|
||||
def get_years(self, decade: Decade) -> list[Year]:
|
||||
"""Get the list of years for this decade."""
|
||||
return [self.sql.years.rows.get(yr) for yr in self.get_yearids(decade)]
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
"""A custom Gio.ListModel for managing individual media in an album."""
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from .. import format
|
||||
from . import playlist
|
||||
from . import table
|
||||
from . import tracks
|
||||
|
||||
|
||||
|
@ -34,12 +36,26 @@ class Medium(playlist.Playlist):
|
|||
return self.get_album()
|
||||
|
||||
|
||||
class Filter(table.KeySet):
|
||||
"""Custom filter to hide media with empty names."""
|
||||
|
||||
def do_get_strictness(self) -> Gtk.FilterMatch:
|
||||
"""Get the strictness of the filter."""
|
||||
if (res := super().do_get_strictness()) == Gtk.FilterMatch.ALL:
|
||||
res = Gtk.FilterMatch.SOME
|
||||
return res
|
||||
|
||||
def do_match(self, medium: Medium) -> bool:
|
||||
"""Check if the Medium matches the filter."""
|
||||
return len(medium.name) > 0 if super().do_match(medium) else False
|
||||
|
||||
|
||||
class Table(playlist.Table):
|
||||
"""Our Media Table."""
|
||||
|
||||
def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs):
|
||||
"""Initialize the Media Table."""
|
||||
super().__init__(sql=sql, autodelete=True,
|
||||
super().__init__(sql=sql, filter=Filter(), autodelete=True,
|
||||
system_tracks=False, **kwargs)
|
||||
|
||||
def do_construct(self, **kwargs) -> Medium:
|
||||
|
@ -61,6 +77,7 @@ class Table(playlist.Table):
|
|||
|
||||
def do_sql_delete(self, medium: Medium) -> sqlite3.Cursor:
|
||||
"""Delete a medium."""
|
||||
medium.get_album().remove_medium(medium)
|
||||
return self.sql("DELETE FROM media WHERE mediumid=?",
|
||||
medium.mediumid)
|
||||
|
||||
|
@ -100,6 +117,13 @@ class Table(playlist.Table):
|
|||
return self.sql(f"UPDATE media SET {column}=? WHERE mediumid=?",
|
||||
newval, medium.mediumid)
|
||||
|
||||
def create(self, album: playlist.Playlist,
|
||||
*args, **kwargs) -> Medium | None:
|
||||
"""Create a new Medium playlist."""
|
||||
if (medium := super().create(album, *args, **kwargs)) is not None:
|
||||
album.add_medium(medium)
|
||||
return medium
|
||||
|
||||
def rename(self, medium: Medium, new_name: str) -> bool:
|
||||
"""Rename a medium."""
|
||||
if (new_name := new_name.strip()) != medium.name:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright 2022 (c) Anna Schumaker
|
||||
"""A customized Gio.ListStore for tracking Playlist GObjects."""
|
||||
import sqlite3
|
||||
import typing
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
from gi.repository import Gtk
|
||||
|
@ -28,6 +29,7 @@ class Playlist(table.Row):
|
|||
tracks_movable = GObject.Property(type=bool, default=False)
|
||||
current_trackid = GObject.Property(type=int)
|
||||
|
||||
child_set = GObject.Property(type=table.TableSubset)
|
||||
children = GObject.Property(type=Gtk.FilterListModel)
|
||||
|
||||
def __init__(self, table: Gio.ListModel, propertyid: int,
|
||||
|
@ -48,20 +50,26 @@ class Playlist(table.Row):
|
|||
self.table.remove_track(self, track)
|
||||
return True
|
||||
|
||||
def add_children(self, child_table: table.Table,
|
||||
child_filter: Gtk.Filter) -> None:
|
||||
def add_children(self, child_table: table.Table, child_keys: set) -> None:
|
||||
"""Create a FilterListModel for this playlist's children."""
|
||||
self.children = Gtk.FilterListModel.new(child_table, child_filter)
|
||||
self.child_set = table.TableSubset(child_table, keys=child_keys)
|
||||
self.children = Gtk.FilterListModel.new(self.child_set,
|
||||
child_table.get_filter())
|
||||
self.children.set_incremental(True)
|
||||
|
||||
def do_update(self, column: str) -> bool:
|
||||
"""Update a Playlist object."""
|
||||
match column:
|
||||
case "propertyid" | "name" | "n-tracks" | "children" | \
|
||||
"user-tracks" | "tracks-loaded" | "tracks-movable": pass
|
||||
case "propertyid" | "name" | "n-tracks" | "child-set" | \
|
||||
"children" | "user-tracks" | "tracks-loaded" | \
|
||||
"tracks-movable": pass
|
||||
case _: return super().do_update(column)
|
||||
return True
|
||||
|
||||
def add_child(self, child: typing.Self) -> None:
|
||||
"""Add a child Playlist to this Playlist."""
|
||||
self.child_set.add_row(child)
|
||||
|
||||
def add_track(self, track: Track, *, idle: bool = False) -> None:
|
||||
"""Add a Track to this Playlist."""
|
||||
if self.table.add_track(self, track):
|
||||
|
@ -71,6 +79,10 @@ class Playlist(table.Row):
|
|||
"""Get a dictionary mapping for trackid -> sorted position."""
|
||||
return self.table.get_track_order(self)
|
||||
|
||||
def has_child(self, child: typing.Self) -> bool:
|
||||
"""Check if this Playlist has a specific child Playlist."""
|
||||
return child in self.child_set
|
||||
|
||||
def has_track(self, track: Track) -> bool:
|
||||
"""Check if a Track is on this Playlist."""
|
||||
return track in self.tracks
|
||||
|
@ -95,6 +107,10 @@ class Playlist(table.Row):
|
|||
self.tracks_loaded = False
|
||||
self.table.queue.push(self.load_tracks, now=not idle)
|
||||
|
||||
def remove_child(self, child: typing.Self) -> None:
|
||||
"""Remove a child Playlist from this Playlist."""
|
||||
self.child_set.remove_row(child)
|
||||
|
||||
def remove_track(self, track: table.Row, *, idle: bool = False) -> None:
|
||||
"""Remove a Track from this Playlist."""
|
||||
self.table.queue.push(self.__remove_track, track, now=not idle)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright 2022 (c) Anna Schumaker
|
||||
"""Base classes for database objects."""
|
||||
import bisect
|
||||
import sqlite3
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
|
@ -37,44 +38,52 @@ class Row(GObject.GObject):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class Filter(Gtk.Filter):
|
||||
"""A Filter that can be used to search playlists."""
|
||||
class KeySet(Gtk.Filter):
|
||||
"""A Gtk.Filter that also acts as a Python Set."""
|
||||
|
||||
n_keys = GObject.Property(type=int)
|
||||
|
||||
def __init__(self, keys: set | None = None, **kwargs):
|
||||
"""Set up our Filter."""
|
||||
"""Set up our KeySet."""
|
||||
super().__init__(**kwargs)
|
||||
self._keys = keys
|
||||
self.n_keys = len(keys) if keys is not None else -1
|
||||
|
||||
def __contains__(self, row: Row) -> bool:
|
||||
"""Check if a Row is in the KeySet."""
|
||||
return self._keys is None or row.primary_key in self._keys
|
||||
|
||||
def __sub__(self, rhs: Gtk.Filter) -> set[int]:
|
||||
"""Subtract two Filters and return the result."""
|
||||
"""Subtract two KeySets 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 __find_difference(self, new: set[any] | None) \
|
||||
-> tuple[set, set, Gtk.FilterChange | None]:
|
||||
if self._keys is None:
|
||||
if new is None:
|
||||
return (set(), set(), None)
|
||||
return (set(), new, Gtk.FilterChange.MORE_STRICT)
|
||||
elif new is None:
|
||||
return (self._keys, set(), Gtk.FilterChange.LESS_STRICT)
|
||||
|
||||
removed = self._keys - new
|
||||
added = new - self._keys
|
||||
match len(removed), len(added):
|
||||
case 0, 0: return (removed, added, None)
|
||||
case _, 0: return (removed, added, Gtk.FilterChange.MORE_STRICT)
|
||||
case 0, _: return (removed, added, Gtk.FilterChange.LESS_STRICT)
|
||||
case _, _: return (removed, added, Gtk.FilterChange.DIFFERENT)
|
||||
|
||||
def changed(self, how: Gtk.FilterChange) -> None:
|
||||
"""Notify that the filter has changed."""
|
||||
"""Notify that the KeySet 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."""
|
||||
"""Get the strictness of the Gtk.Filter."""
|
||||
if self._keys is None:
|
||||
return Gtk.FilterMatch.ALL
|
||||
if len(self._keys) == 0:
|
||||
|
@ -82,19 +91,21 @@ class Filter(Gtk.Filter):
|
|||
return Gtk.FilterMatch.SOME
|
||||
|
||||
def do_match(self, row: Row) -> bool:
|
||||
"""Check if the Row matches the filter."""
|
||||
"""Check if the Row is in the KeySet."""
|
||||
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:
|
||||
"""Add a Row to the KeySet."""
|
||||
if row not in self:
|
||||
self._keys.add(row.primary_key)
|
||||
self.emit("key-added", 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:
|
||||
"""Remove a Row from the KeySet."""
|
||||
if self._keys is not None and row in self:
|
||||
self._keys.discard(row.primary_key)
|
||||
self.emit("key-removed", row.primary_key)
|
||||
self.changed(Gtk.FilterChange.MORE_STRICT)
|
||||
|
||||
@property
|
||||
|
@ -105,9 +116,23 @@ class Filter(Gtk.Filter):
|
|||
@keys.setter
|
||||
def keys(self, keys: set[any] | None) -> None:
|
||||
"""Set the matching primary keys."""
|
||||
if (how := self.__find_change(keys)) is not None:
|
||||
(removed, added, change) = self.__find_difference(keys)
|
||||
if change is not None:
|
||||
self._keys = keys
|
||||
self.changed(how)
|
||||
self.emit("keys-changed", removed, added)
|
||||
self.changed(change)
|
||||
|
||||
@GObject.Signal(arg_types=(int,))
|
||||
def key_added(self, key: int) -> None:
|
||||
"""Signal that a Row has been added to the KeySet."""
|
||||
|
||||
@GObject.Signal(arg_types=(int,))
|
||||
def key_removed(self, key: int) -> None:
|
||||
"""Signal that a Row has been removed from the KeySet."""
|
||||
|
||||
@GObject.Signal(arg_types=(GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT))
|
||||
def keys_changed(self, removed: set | None, added: set | None) -> None:
|
||||
"""Signal that the KeySet has been directly modified."""
|
||||
|
||||
|
||||
class Table(Gtk.FilterListModel):
|
||||
|
@ -121,12 +146,12 @@ class Table(Gtk.FilterListModel):
|
|||
loaded = GObject.Property(type=bool, default=False)
|
||||
|
||||
def __init__(self, sql: GObject.TYPE_PYOBJECT,
|
||||
filter: Filter | None = None,
|
||||
filter: KeySet | None = None,
|
||||
queue: Queue | None = None, **kwargs):
|
||||
"""Set up our Table object."""
|
||||
super().__init__(sql=sql, rows=dict(), incremental=True,
|
||||
store=store.SortedList(self.get_sort_key),
|
||||
filter=(filter if filter else Filter()),
|
||||
filter=(filter if filter else KeySet()),
|
||||
queue=(queue if queue else Queue()), **kwargs)
|
||||
self.set_model(self.store)
|
||||
|
||||
|
@ -247,3 +272,75 @@ class Table(Gtk.FilterListModel):
|
|||
def update(self, row: Row, column: str, newval) -> bool:
|
||||
"""Update a Row."""
|
||||
return self.do_sql_update(row, column, newval) is not None
|
||||
|
||||
|
||||
class TableSubset(GObject.GObject, Gio.ListModel):
|
||||
"""A list model containing a subset of the rows in the source Table."""
|
||||
|
||||
keyset = GObject.Property(type=KeySet)
|
||||
table = GObject.Property(type=Table)
|
||||
n_rows = GObject.Property(type=int)
|
||||
|
||||
def __init__(self, table: Table, *, keys: set[any] | None = None):
|
||||
"""Initialize a KeySetModel."""
|
||||
super().__init__(keyset=KeySet(set() if keys is None else keys),
|
||||
table=table)
|
||||
self._items = []
|
||||
|
||||
self.keyset.connect("key-added", self.__on_key_added)
|
||||
self.keyset.connect("key-removed", self.__on_key_removed)
|
||||
self.table.connect("notify::loaded", self.__notify_table_loaded)
|
||||
|
||||
def __contains__(self, row: Row) -> bool:
|
||||
"""Check if the Row is in the internal KeySet."""
|
||||
return row in self.keyset
|
||||
|
||||
def __bisect(self, key: any) -> int | None:
|
||||
if self.table.loaded:
|
||||
sort_key = self.table.get_sort_key(self.table.rows[key])
|
||||
return bisect.bisect_left(self._items, sort_key,
|
||||
key=self.table.get_sort_key)
|
||||
return None
|
||||
|
||||
def __items_changed(self, position: int, removed: int, added: int) -> None:
|
||||
self.n_rows = len(self._items)
|
||||
self.items_changed(position, removed, added)
|
||||
|
||||
def __notify_table_loaded(self, table: Table, param) -> None:
|
||||
if table.loaded and self.keyset.n_keys > 0:
|
||||
self._items = sorted([table.rows[k] for k in self.keyset.keys],
|
||||
key=self.table.get_sort_key)
|
||||
self.__items_changed(0, 0, self.keyset.n_keys)
|
||||
elif not table.loaded and self.n_rows > 0:
|
||||
self._items = []
|
||||
self.__items_changed(0, self.n_rows, 0)
|
||||
|
||||
def __on_key_added(self, keyset: KeySet, key: any) -> None:
|
||||
if (pos := self.__bisect(key)) is not None:
|
||||
self._items.insert(pos, self.table.rows[key])
|
||||
self.__items_changed(pos, 0, 1)
|
||||
|
||||
def __on_key_removed(self, keyset: KeySet, key: any) -> None:
|
||||
if (pos := self.__bisect(key)) is not None:
|
||||
del self._items[pos]
|
||||
self.__items_changed(pos, 1, 0)
|
||||
|
||||
def do_get_item_type(self) -> GObject.GType:
|
||||
"""Get the Gio.ListModel item type."""
|
||||
return Row.__gtype__
|
||||
|
||||
def do_get_n_items(self) -> int:
|
||||
"""Get the number of Rows in the TableSubset."""
|
||||
return self.n_rows
|
||||
|
||||
def do_get_item(self, n: int) -> int:
|
||||
"""Get the nth item in the TableSubset."""
|
||||
return self._items[n] if n < len(self._items) else None
|
||||
|
||||
def add_row(self, row: Row) -> None:
|
||||
"""Add a row to the TableSubset."""
|
||||
self.keyset.add_row(row)
|
||||
|
||||
def remove_row(self, row: Row) -> None:
|
||||
"""Remove a row from the TableSubset."""
|
||||
self.keyset.remove_row(row)
|
||||
|
|
|
@ -90,7 +90,7 @@ class Track(table.Row):
|
|||
return self.trackid
|
||||
|
||||
|
||||
class Filter(table.Filter):
|
||||
class Filter(table.KeySet):
|
||||
"""A customized Filter that never sets strictness to FilterMatch.All."""
|
||||
|
||||
def do_get_strictness(self) -> Gtk.FilterMatch:
|
||||
|
|
|
@ -48,6 +48,8 @@ class Table(playlist.Table):
|
|||
|
||||
def do_sql_delete(self, year: Year) -> sqlite3.Cursor:
|
||||
"""Delete a year."""
|
||||
if year.parent is not None:
|
||||
year.parent.remove_year(year)
|
||||
return self.sql("DELETE FROM years WHERE year=?", year.year)
|
||||
|
||||
def do_sql_glob(self, glob: str) -> sqlite3.Cursor:
|
||||
|
@ -71,3 +73,10 @@ class Table(playlist.Table):
|
|||
"""Load a Year's Tracks from the database."""
|
||||
return self.sql("""SELECT trackid FROM year_tracks_view
|
||||
WHERE year=?""", year.year)
|
||||
|
||||
def create(self, *args, **kwargs) -> Year | None:
|
||||
"""Create a new Year playlist."""
|
||||
if (year := super().create(*args, **kwargs)) is not None:
|
||||
if year.parent is not None:
|
||||
year.parent.add_year(year)
|
||||
return year
|
||||
|
|
|
@ -41,6 +41,22 @@ class TestAlbumObject(tests.util.TestCase):
|
|||
self.assertEqual(album2.mbid, "ab-cd-ef")
|
||||
self.assertEqual(album2.cover, cover)
|
||||
|
||||
def test_add_remove_medium(self):
|
||||
"""Test adding and removing a medium from the Album."""
|
||||
album = self.table.create("Test Album", "Album Artist", "2023-03")
|
||||
medium = self.sql.media.create(album, "Test Medium", number=1)
|
||||
|
||||
self.assertFalse(medium in self.album.child_set)
|
||||
self.assertFalse(self.album.has_medium(medium))
|
||||
|
||||
self.album.add_medium(medium)
|
||||
self.assertTrue(medium in self.album.child_set)
|
||||
self.assertTrue(self.album.has_medium(medium))
|
||||
|
||||
self.album.remove_medium(medium)
|
||||
self.assertFalse(medium in self.album.child_set)
|
||||
self.assertFalse(self.album.has_medium(medium))
|
||||
|
||||
def test_get_artists(self):
|
||||
"""Test getting the list of artists for this album."""
|
||||
with unittest.mock.patch.object(self.table, "get_artists",
|
||||
|
@ -56,22 +72,14 @@ class TestAlbumObject(tests.util.TestCase):
|
|||
self.assertListEqual(self.album.get_media(), [1, 2, 3])
|
||||
mock.assert_called_with(self.album)
|
||||
|
||||
def test_media_model(self):
|
||||
"""Test getting a Gio.ListModel representing this Album's media."""
|
||||
def test_children(self):
|
||||
"""Test the Album's 'children' model is set up properly."""
|
||||
self.assertIsInstance(self.album.child_set,
|
||||
emmental.db.table.TableSubset)
|
||||
self.assertIsInstance(self.album.children, Gtk.FilterListModel)
|
||||
self.assertIsInstance(self.album.children.get_filter(),
|
||||
Gtk.CustomFilter)
|
||||
self.assertEqual(self.album.children.get_model(), self.sql.media)
|
||||
|
||||
album = self.table.create("Test Album", "Album Artist", "2023-03")
|
||||
medium = self.sql.media.create(album, "Test Medium", number=1)
|
||||
self.assertTrue(album.children.get_filter().match(medium))
|
||||
|
||||
medium.albumid = album.albumid + 1
|
||||
self.assertFalse(album.children.get_filter().match(medium))
|
||||
|
||||
medium = self.sql.media.create(album, "", number=2)
|
||||
self.assertFalse(album.children.get_filter().match(medium))
|
||||
self.assertEqual(self.album.children.get_filter(),
|
||||
self.sql.media.get_filter())
|
||||
self.assertEqual(self.album.child_set.table, self.sql.media)
|
||||
|
||||
|
||||
class TestAlbumTable(tests.util.TestCase):
|
||||
|
@ -228,9 +236,11 @@ class TestAlbumTable(tests.util.TestCase):
|
|||
|
||||
def test_load(self):
|
||||
"""Test loading the album table."""
|
||||
self.table.create("Album 1", "Album Artist", "2023-03")
|
||||
album = self.table.create("Album 1", "Album Artist", "2023-03")
|
||||
self.table.create("Album 2", "Album Artist", "2023-03",
|
||||
mbid="ab-cd-ef", cover=tests.util.COVER_JPG)
|
||||
medium = self.sql.media.create(album, "Test Medium", number=1)
|
||||
album.add_medium(medium)
|
||||
|
||||
albums2 = emmental.db.albums.Table(self.sql)
|
||||
self.assertEqual(len(albums2), 0)
|
||||
|
@ -243,6 +253,7 @@ class TestAlbumTable(tests.util.TestCase):
|
|||
self.assertEqual(albums2.get_item(0).release, "2023-03")
|
||||
self.assertEqual(albums2.get_item(0).mbid, "")
|
||||
self.assertIsNone(albums2.get_item(0).cover)
|
||||
self.assertSetEqual(albums2.get_item(0).child_set.keyset.keys, {1})
|
||||
|
||||
self.assertEqual(albums2.get_item(1).name, "Album 2")
|
||||
self.assertEqual(albums2.get_item(1).artist, "Album Artist")
|
||||
|
@ -250,6 +261,7 @@ class TestAlbumTable(tests.util.TestCase):
|
|||
self.assertEqual(albums2.get_item(1).mbid, "ab-cd-ef")
|
||||
self.assertEqual(albums2.get_item(1).cover,
|
||||
tests.util.COVER_JPG)
|
||||
self.assertSetEqual(albums2.get_item(1).child_set.keyset.keys, set())
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looking up album playlists."""
|
||||
|
@ -320,4 +332,6 @@ class TestAlbumTable(tests.util.TestCase):
|
|||
medium1 = self.sql.media.create(album, "", number=1)
|
||||
medium2 = self.sql.media.create(album, "", number=2)
|
||||
|
||||
self.assertSetEqual(self.table.get_mediumids(album),
|
||||
{medium1.mediumid, medium2.mediumid})
|
||||
self.assertListEqual(self.table.get_media(album), [medium1, medium2])
|
||||
|
|
|
@ -20,7 +20,6 @@ class TestArtistObject(tests.util.TestCase):
|
|||
def test_init(self):
|
||||
"""Test that the Artist is set up properly."""
|
||||
self.assertIsInstance(self.artist, emmental.db.playlist.Playlist)
|
||||
self.assertSetEqual(self.artist.children.get_filter().keys, set())
|
||||
self.assertEqual(self.artist.table, self.table)
|
||||
self.assertEqual(self.artist.propertyid, 456)
|
||||
self.assertEqual(self.artist.artistid, 123)
|
||||
|
@ -37,8 +36,7 @@ class TestArtistObject(tests.util.TestCase):
|
|||
self.artist.add_album(album)
|
||||
|
||||
mock_add.assert_called_with(self.artist, album)
|
||||
self.assertSetEqual(self.artist.children.get_filter().keys,
|
||||
{album.albumid})
|
||||
self.assertIn(album, self.artist.child_set)
|
||||
self.assertTrue(self.artist.has_album(album))
|
||||
|
||||
with unittest.mock.patch.object(self.table, "remove_album",
|
||||
|
@ -46,15 +44,17 @@ class TestArtistObject(tests.util.TestCase):
|
|||
self.artist.remove_album(album)
|
||||
|
||||
mock_remove.assert_called_with(self.artist, album)
|
||||
self.assertSetEqual(self.artist.children.get_filter().keys, set())
|
||||
self.assertNotIn(album, self.artist.child_set)
|
||||
self.assertFalse(self.artist.has_album(album))
|
||||
|
||||
def test_children(self):
|
||||
"""Test that Albums have been added as Artist playlist children."""
|
||||
self.assertIsInstance(self.artist.child_set,
|
||||
emmental.db.table.TableSubset)
|
||||
self.assertIsInstance(self.artist.children, Gtk.FilterListModel)
|
||||
self.assertIsInstance(self.artist.children.get_filter(),
|
||||
emmental.db.table.Filter)
|
||||
self.assertEqual(self.artist.children.get_model(), self.sql.albums)
|
||||
self.assertEqual(self.artist.children.get_filter(),
|
||||
self.sql.albums.get_filter())
|
||||
self.assertEqual(self.artist.child_set.table, self.sql.albums)
|
||||
|
||||
|
||||
class TestFilter(tests.util.TestCase):
|
||||
|
@ -68,7 +68,7 @@ class TestFilter(tests.util.TestCase):
|
|||
|
||||
def test_init(self):
|
||||
"""Test that the filter is initialized properly."""
|
||||
self.assertIsInstance(self.filter, emmental.db.table.Filter)
|
||||
self.assertIsInstance(self.filter, emmental.db.table.KeySet)
|
||||
self.assertFalse(self.filter.show_all)
|
||||
|
||||
filter2 = emmental.db.artists.Filter(show_all=True)
|
||||
|
@ -219,13 +219,11 @@ class TestArtistTable(tests.util.TestCase):
|
|||
|
||||
self.assertEqual(artists2.get_item(0).name, "Artist 1")
|
||||
self.assertEqual(artists2.get_item(0).mbid, "")
|
||||
self.assertSetEqual(artists2.get_item(0).children.get_filter().keys,
|
||||
{1})
|
||||
self.assertSetEqual(artists2.get_item(0).child_set.keyset.keys, {1})
|
||||
|
||||
self.assertEqual(artists2.get_item(1).name, "Artist 2")
|
||||
self.assertEqual(artists2.get_item(1).mbid, "ab-cd-ef")
|
||||
self.assertSetEqual(artists2.get_item(1).children.get_filter().keys,
|
||||
set())
|
||||
self.assertSetEqual(artists2.get_item(1).child_set.keyset.keys, set())
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looking up artist playlists."""
|
||||
|
|
|
@ -28,6 +28,21 @@ class TestDecadeObject(tests.util.TestCase):
|
|||
self.assertEqual(self.decade.name, "The 2020s")
|
||||
self.assertIsNone(self.decade.parent)
|
||||
|
||||
def test_add_remove_year(self):
|
||||
"""Test adding and removing a year from the decade."""
|
||||
year = self.sql.years.create(1988)
|
||||
|
||||
self.assertFalse(year in self.decade.child_set)
|
||||
self.assertFalse(self.decade.has_year(year))
|
||||
|
||||
self.decade.add_year(year)
|
||||
self.assertTrue(year in self.decade.child_set)
|
||||
self.assertTrue(self.decade.has_year(year))
|
||||
|
||||
self.decade.remove_year(year)
|
||||
self.assertFalse(year in self.decade.child_set)
|
||||
self.assertFalse(self.decade.has_year(year))
|
||||
|
||||
def test_get_years(self):
|
||||
"""Test getting the list of years for this decade."""
|
||||
with unittest.mock.patch.object(self.table, "get_years",
|
||||
|
@ -37,16 +52,12 @@ class TestDecadeObject(tests.util.TestCase):
|
|||
|
||||
def test_years_model(self):
|
||||
"""Test getting a Gio.ListModel representing a Decade's years."""
|
||||
self.assertIsInstance(self.decade.child_set,
|
||||
emmental.db.table.TableSubset)
|
||||
self.assertIsInstance(self.decade.children, Gtk.FilterListModel)
|
||||
self.assertIsInstance(self.decade.children.get_filter(),
|
||||
Gtk.CustomFilter)
|
||||
self.assertEqual(self.decade.children.get_model(), self.sql.years)
|
||||
|
||||
year = self.sql.years.create(2023)
|
||||
self.assertTrue(self.decade.children.get_filter().match(year))
|
||||
|
||||
year = self.sql.years.create(1988)
|
||||
self.assertFalse(self.decade.children.get_filter().match(year))
|
||||
self.assertEqual(self.decade.children.get_filter(),
|
||||
self.sql.years.get_filter())
|
||||
self.assertEqual(self.decade.child_set.table, self.sql.years)
|
||||
|
||||
|
||||
class TestDecadeTable(tests.util.TestCase):
|
||||
|
@ -164,8 +175,10 @@ class TestDecadeTable(tests.util.TestCase):
|
|||
|
||||
def test_load(self):
|
||||
"""Load the decade table from the database."""
|
||||
self.table.create(1980)
|
||||
decade = self.table.create(1980)
|
||||
self.table.create(1990)
|
||||
year = self.sql.years.create(1988)
|
||||
decade.add_year(year)
|
||||
|
||||
decades2 = emmental.db.decades.Table(self.sql)
|
||||
self.assertEqual(len(decades2), 0)
|
||||
|
@ -175,9 +188,11 @@ class TestDecadeTable(tests.util.TestCase):
|
|||
|
||||
self.assertEqual(decades2.get_item(0).decade, 1980)
|
||||
self.assertEqual(decades2.get_item(0).name, "The 1980s")
|
||||
self.assertSetEqual(decades2.get_item(0).child_set.keyset.keys, {1988})
|
||||
|
||||
self.assertEqual(decades2.get_item(1).decade, 1990)
|
||||
self.assertEqual(decades2.get_item(1).name, "The 1990s")
|
||||
self.assertSetEqual(decades2.get_item(1).child_set.keyset.keys, set())
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looking up decade playlists."""
|
||||
|
@ -214,4 +229,5 @@ class TestDecadeTable(tests.util.TestCase):
|
|||
y1985 = self.sql.years.create(1985)
|
||||
y1988 = self.sql.years.create(1988)
|
||||
|
||||
self.assertSetEqual(self.table.get_yearids(decade), {1985, 1988})
|
||||
self.assertListEqual(self.table.get_years(decade), [y1985, y1988])
|
||||
|
|
|
@ -4,6 +4,7 @@ import pathlib
|
|||
import unittest.mock
|
||||
import emmental.db
|
||||
import tests.util
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class TestMediumObject(tests.util.TestCase):
|
||||
|
@ -46,6 +47,36 @@ class TestMediumObject(tests.util.TestCase):
|
|||
mock_rename.assert_called_with(self.medium, "New Name")
|
||||
|
||||
|
||||
class TestFilter(tests.util.TestCase):
|
||||
"""Test the medium filter."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
super().setUp()
|
||||
self.filter = emmental.db.media.Filter()
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the filter is initialized properly."""
|
||||
self.assertIsInstance(self.filter, emmental.db.table.KeySet)
|
||||
|
||||
def test_strictness(self):
|
||||
"""Test checking strictness."""
|
||||
self.filter.keys = None
|
||||
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
|
||||
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_match(self):
|
||||
"""Test matching a Medium."""
|
||||
album = self.sql.albums.create("Test Album", "Test Artist", "123")
|
||||
medium = self.sql.media.create(album, "", number=1)
|
||||
self.assertFalse(self.filter.match(medium))
|
||||
medium.name = "abcde"
|
||||
self.assertTrue(self.filter.match(medium))
|
||||
|
||||
|
||||
class TestMediumsTable(tests.util.TestCase):
|
||||
"""Tests our mediums table."""
|
||||
|
||||
|
@ -61,6 +92,8 @@ class TestMediumsTable(tests.util.TestCase):
|
|||
def test_init(self):
|
||||
"""Test that the medium model is configured corretly."""
|
||||
self.assertIsInstance(self.table, emmental.db.playlist.Table)
|
||||
self.assertIsInstance(self.table.get_filter(),
|
||||
emmental.db.media.Filter)
|
||||
self.assertEqual(len(self.table), 0)
|
||||
self.assertTrue(self.table.autodelete)
|
||||
self.assertFalse(self.table.system_tracks)
|
||||
|
@ -99,6 +132,7 @@ class TestMediumsTable(tests.util.TestCase):
|
|||
self.assertEqual(medium1.number, 1)
|
||||
self.assertEqual(medium1.type, "")
|
||||
self.assertEqual(medium1.sort_order, "mediumno, number")
|
||||
self.assertTrue(self.album.has_medium(medium1))
|
||||
|
||||
cur = self.sql("SELECT COUNT(name) FROM media")
|
||||
self.assertEqual(cur.fetchone()["COUNT(name)"], 1)
|
||||
|
@ -123,6 +157,7 @@ class TestMediumsTable(tests.util.TestCase):
|
|||
medium = self.table.create(self.album, "Medium 1", number=1)
|
||||
self.assertTrue(medium.delete())
|
||||
self.assertIsNone(self.table.index(medium))
|
||||
self.assertFalse(self.album.has_medium(medium))
|
||||
|
||||
cur = self.sql("SELECT COUNT(name) FROM media")
|
||||
self.assertEqual(cur.fetchone()["COUNT(name)"], 0)
|
||||
|
@ -172,17 +207,19 @@ class TestMediumsTable(tests.util.TestCase):
|
|||
self.assertEqual(len(mediums2), 0)
|
||||
|
||||
mediums2.load(now=True)
|
||||
self.assertEqual(len(mediums2), 2)
|
||||
self.assertEqual(len(mediums2.store), 2)
|
||||
|
||||
self.assertEqual(mediums2.get_item(0).albumid, self.album.albumid)
|
||||
self.assertEqual(mediums2.get_item(0).name, "")
|
||||
self.assertEqual(mediums2.get_item(0).number, 1)
|
||||
self.assertEqual(mediums2.get_item(0).type, "")
|
||||
self.assertEqual(mediums2.store.get_item(0).albumid,
|
||||
self.album.albumid)
|
||||
self.assertEqual(mediums2.store.get_item(0).name, "")
|
||||
self.assertEqual(mediums2.store.get_item(0).number, 1)
|
||||
self.assertEqual(mediums2.store.get_item(0).type, "")
|
||||
|
||||
self.assertEqual(mediums2.get_item(1).albumid, self.album.albumid)
|
||||
self.assertEqual(mediums2.get_item(1).name, "Medium 2")
|
||||
self.assertEqual(mediums2.get_item(1).number, 2)
|
||||
self.assertEqual(mediums2.get_item(1).type, "Digital Media")
|
||||
self.assertEqual(mediums2.store.get_item(1).albumid,
|
||||
self.album.albumid)
|
||||
self.assertEqual(mediums2.store.get_item(1).name, "Medium 2")
|
||||
self.assertEqual(mediums2.store.get_item(1).number, 2)
|
||||
self.assertEqual(mediums2.store.get_item(1).type, "Digital Media")
|
||||
|
||||
def test_lookup(self):
|
||||
"""Test looking up medium playlists."""
|
||||
|
|
|
@ -65,15 +65,29 @@ class TestPlaylistRow(unittest.TestCase):
|
|||
|
||||
def test_children(self):
|
||||
"""Test the child playlist properties."""
|
||||
self.assertIsNone(self.playlist.child_set)
|
||||
self.assertIsNone(self.playlist.children)
|
||||
|
||||
filter = Gtk.Filter()
|
||||
self.playlist.add_children(self.table, filter)
|
||||
table = emmental.db.table.Table(None)
|
||||
self.playlist.add_children(table, set())
|
||||
|
||||
self.assertIsInstance(self.playlist.child_set,
|
||||
emmental.db.table.TableSubset)
|
||||
self.assertEqual(self.playlist.child_set.table, table)
|
||||
self.assertSetEqual(self.playlist.child_set.keyset.keys, set())
|
||||
|
||||
self.assertIsInstance(self.playlist.children, Gtk.FilterListModel)
|
||||
self.assertEqual(self.playlist.children.get_filter(), filter)
|
||||
self.assertEqual(self.playlist.children.get_model(), self.table)
|
||||
self.assertEqual(self.playlist.children.get_filter(),
|
||||
table.get_filter())
|
||||
self.assertEqual(self.playlist.children.get_model(),
|
||||
self.playlist.child_set)
|
||||
self.assertTrue(self.playlist.children.get_incremental())
|
||||
|
||||
playlist2 = emmental.db.playlist.Playlist(table=self.table,
|
||||
propertyid=2, name="Plist2")
|
||||
playlist2.add_children(table, {1, 2, 3})
|
||||
self.assertSetEqual(playlist2.child_set.keyset.keys, {1, 2, 3})
|
||||
|
||||
def test_parent(self):
|
||||
"""Test the parent playlist property."""
|
||||
self.assertIsNone(self.playlist.parent)
|
||||
|
@ -97,6 +111,15 @@ class TestPlaylistRow(unittest.TestCase):
|
|||
self.table.update.assert_called_with(self.playlist,
|
||||
prop, value)
|
||||
|
||||
def test_add_child(self):
|
||||
"""Test adding a child playlist to the playlist."""
|
||||
table = emmental.db.table.Table(None)
|
||||
child = tests.util.table.MockRow(table=table, number=1)
|
||||
self.playlist.add_children(table, set())
|
||||
|
||||
self.playlist.add_child(child)
|
||||
self.assertIn(child, self.playlist.child_set)
|
||||
|
||||
def test_add_track(self):
|
||||
"""Test adding a track to the playlist."""
|
||||
self.playlist.add_track(self.track, idle=True)
|
||||
|
@ -122,6 +145,18 @@ class TestPlaylistRow(unittest.TestCase):
|
|||
{1: 3, 2: 2, 3: 1})
|
||||
self.table.get_track_order.assert_called_with(self.playlist)
|
||||
|
||||
def test_has_child(self):
|
||||
"""Test the playlist has_child() function."""
|
||||
table = emmental.db.table.Table(None)
|
||||
child = tests.util.table.MockRow(table=table, number=1)
|
||||
self.playlist.add_children(table, set())
|
||||
|
||||
self.assertFalse(self.playlist.has_child(child))
|
||||
self.playlist.add_child(child)
|
||||
self.assertTrue(self.playlist.has_child(child))
|
||||
self.playlist.remove_child(child)
|
||||
self.assertFalse(self.playlist.has_child(child))
|
||||
|
||||
def test_has_track(self):
|
||||
"""Test the playlist has_track() function."""
|
||||
self.assertFalse(self.playlist.has_track(self.track))
|
||||
|
@ -141,6 +176,16 @@ class TestPlaylistRow(unittest.TestCase):
|
|||
self.assertTrue(self.playlist.move_track_up(self.track))
|
||||
self.table.move_track_up.assert_called_with(self.playlist, self.track)
|
||||
|
||||
def test_remove_child(self):
|
||||
"""Test removing a child playlist from the playlist."""
|
||||
table = emmental.db.table.Table(None)
|
||||
child = tests.util.table.MockRow(table=table, number=1)
|
||||
self.playlist.add_children(table, set())
|
||||
|
||||
self.playlist.add_child(child)
|
||||
self.playlist.remove_child(child)
|
||||
self.assertFalse(child in self.playlist.child_set)
|
||||
|
||||
def test_remove_track(self):
|
||||
"""Test removing a track from the playlist."""
|
||||
self.playlist.tracks.trackids.add(self.track.trackid)
|
||||
|
|
|
@ -50,119 +50,166 @@ class TestRow(unittest.TestCase):
|
|||
|
||||
|
||||
@unittest.mock.patch("gi.repository.Gtk.Filter.changed")
|
||||
class TestFilter(unittest.TestCase):
|
||||
"""Tests our database row Filter."""
|
||||
class TestKeySet(unittest.TestCase):
|
||||
"""Tests our KeySet for holding database Rows."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
self.filter = emmental.db.table.Filter()
|
||||
self.keyset = emmental.db.table.KeySet()
|
||||
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)
|
||||
"""Test that the KeySet is created correctly."""
|
||||
self.assertIsInstance(self.keyset, Gtk.Filter)
|
||||
self.assertIsNone(self.keyset._keys, None)
|
||||
self.assertEqual(self.keyset.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)
|
||||
keyset2 = emmental.db.table.KeySet(keys={1, 2, 3})
|
||||
self.assertSetEqual(keyset2._keys, {1, 2, 3})
|
||||
self.assertEqual(keyset2.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})
|
||||
"""Test subtracting two KeySets."""
|
||||
keyset2 = emmental.db.table.KeySet(keys={2, 3})
|
||||
self.assertIsNone(self.keyset - self.keyset)
|
||||
self.assertIsNone(self.keyset - keyset2)
|
||||
self.assertSetEqual(keyset2 - self.keyset, {2, 3})
|
||||
|
||||
self.filter.keys = {1, 2, 3, 4, 5}
|
||||
self.assertSetEqual(self.filter - filter2, {1, 4, 5})
|
||||
self.assertSetEqual(filter2 - self.filter, set())
|
||||
self.keyset.keys = {1, 2, 3, 4, 5}
|
||||
self.assertSetEqual(self.keyset - keyset2, {1, 4, 5})
|
||||
self.assertSetEqual(keyset2 - self.keyset, 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)
|
||||
self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.ALL)
|
||||
self.keyset._keys = set()
|
||||
self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.NONE)
|
||||
self.keyset._keys = {1, 2, 3}
|
||||
self.assertEqual(self.keyset.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)
|
||||
"""Test adding Rows to the KeySet."""
|
||||
mock_added = unittest.mock.Mock()
|
||||
self.keyset.connect("key-added", mock_added)
|
||||
|
||||
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.keyset.add_row(self.row1)
|
||||
self.assertIsNone(self.keyset.keys)
|
||||
mock_added.assert_not_called()
|
||||
|
||||
self.filter.add_row(self.row2)
|
||||
self.assertSetEqual(self.filter.keys, {1, 2})
|
||||
self.keyset.keys = set()
|
||||
self.keyset.add_row(self.row1)
|
||||
self.assertSetEqual(self.keyset.keys, {1})
|
||||
self.assertEqual(self.keyset.n_keys, 1)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
|
||||
self.assertEqual(self.filter.n_keys, 2)
|
||||
mock_added.assert_called_with(self.keyset, 1)
|
||||
|
||||
self.keyset.add_row(self.row2)
|
||||
self.assertSetEqual(self.keyset.keys, {1, 2})
|
||||
self.assertEqual(self.keyset.n_keys, 2)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
|
||||
mock_added.assert_called_with(self.keyset, 2)
|
||||
|
||||
mock_changed.reset_mock()
|
||||
mock_added.reset_mock()
|
||||
self.keyset.add_row(self.row2)
|
||||
self.assertSetEqual(self.keyset.keys, {1, 2})
|
||||
mock_changed.assert_not_called()
|
||||
mock_added.assert_not_called()
|
||||
|
||||
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()
|
||||
"""Test removing Rows from the KeySet."""
|
||||
mock_removed = unittest.mock.Mock()
|
||||
self.keyset.connect("key-removed", mock_removed)
|
||||
|
||||
self.filter.keys = {1, 2}
|
||||
self.filter.remove_row(self.row1)
|
||||
self.assertSetEqual(self.filter._keys, {2})
|
||||
self.keyset.remove_row(self.row1)
|
||||
mock_changed.assert_not_called()
|
||||
mock_removed.assert_not_called()
|
||||
|
||||
self.keyset.keys = {1, 2}
|
||||
self.keyset.remove_row(self.row1)
|
||||
self.assertSetEqual(self.keyset._keys, {2})
|
||||
self.assertEqual(self.keyset.n_keys, 1)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
|
||||
self.assertEqual(self.filter.n_keys, 1)
|
||||
mock_removed.assert_called_with(self.keyset, 1)
|
||||
|
||||
mock_changed.reset_mock()
|
||||
self.filter.remove_row(self.row2)
|
||||
self.assertSetEqual(self.filter._keys, set())
|
||||
mock_removed.reset_mock()
|
||||
self.keyset.remove_row(self.row1)
|
||||
self.assertSetEqual(self.keyset.keys, {2})
|
||||
self.assertEqual(self.keyset.n_keys, 1)
|
||||
mock_changed.assert_not_called()
|
||||
mock_removed.assert_not_called()
|
||||
|
||||
self.keyset.remove_row(self.row2)
|
||||
self.assertSetEqual(self.keyset._keys, set())
|
||||
self.assertEqual(self.keyset.n_keys, 0)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
|
||||
self.assertEqual(self.filter.n_keys, 0)
|
||||
mock_removed.assert_called_with(self.keyset, 2)
|
||||
|
||||
def test_keys(self, mock_changed: unittest.mock.Mock):
|
||||
"""Test setting and getting the filter keys property."""
|
||||
self.assertIsNone(self.filter.keys)
|
||||
"""Test getting and setting the KeySet.keys property."""
|
||||
mock_keys_changed = unittest.mock.Mock()
|
||||
self.keyset.connect("keys-changed", mock_keys_changed)
|
||||
|
||||
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}
|
||||
self.assertIsNone(self.keyset.keys)
|
||||
self.keyset.keys = None
|
||||
self.assertIsNone(self.keyset.keys)
|
||||
mock_changed.assert_not_called()
|
||||
mock_keys_changed.assert_not_called()
|
||||
|
||||
self.filter.keys = {1, 2, 3}
|
||||
self.assertSetEqual(self.filter.keys, {1, 2, 3})
|
||||
self.keyset.keys = {1, 2, 3}
|
||||
self.assertSetEqual(self.keyset._keys, {1, 2, 3})
|
||||
self.assertEqual(self.keyset.n_keys, 3)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
|
||||
mock_keys_changed.assert_called_with(self.keyset, set(), {1, 2, 3})
|
||||
|
||||
mock_changed.reset_mock()
|
||||
self.keyset.keys = {1, 2}
|
||||
self.assertSetEqual(self.keyset.keys, {1, 2})
|
||||
self.assertEqual(self.keyset.n_keys, 2)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
|
||||
mock_keys_changed.assert_called_with(self.keyset, {3}, set())
|
||||
|
||||
mock_changed.reset_mock()
|
||||
mock_keys_changed.reset_mock()
|
||||
self.keyset.keys = {1, 2}
|
||||
mock_changed.assert_not_called()
|
||||
mock_keys_changed.assert_not_called()
|
||||
|
||||
self.keyset.keys = {1, 2, 3}
|
||||
self.assertSetEqual(self.keyset.keys, {1, 2, 3})
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
|
||||
mock_keys_changed.assert_called_with(self.keyset, set(), {3})
|
||||
|
||||
self.filter.keys = {4, 5, 6}
|
||||
self.assertSetEqual(self.filter._keys, {4, 5, 6})
|
||||
self.keyset.keys = {4, 5, 6}
|
||||
self.assertSetEqual(self.keyset._keys, {4, 5, 6})
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.DIFFERENT)
|
||||
mock_keys_changed.assert_called_with(self.keyset, {1, 2, 3}, {4, 5, 6})
|
||||
|
||||
self.filter.keys = None
|
||||
self.assertIsNone(self.filter._keys)
|
||||
self.keyset.keys = None
|
||||
self.assertIsNone(self.keyset._keys)
|
||||
self.assertEqual(self.keyset.n_keys, -1)
|
||||
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
|
||||
self.assertEqual(self.filter.n_keys, -1)
|
||||
mock_keys_changed.assert_called_with(self.keyset, {4, 5, 6}, set())
|
||||
|
||||
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))
|
||||
def test_match_contains(self, mock_changed: unittest.mock.Mock):
|
||||
"""Test matching Rows and the __contains__() magic method."""
|
||||
self.assertTrue(self.keyset.match(self.row1))
|
||||
self.assertTrue(self.row1 in self.keyset)
|
||||
|
||||
self.keyset.keys = {1, 2, 3}
|
||||
self.assertTrue(self.keyset.match(self.row1))
|
||||
self.assertTrue(self.row1 in self.keyset)
|
||||
|
||||
self.keyset.keys = {4, 5, 6}
|
||||
self.assertFalse(self.keyset.match(self.row1))
|
||||
self.assertFalse(self.row1 in self.keyset)
|
||||
|
||||
self.keyset.keys = set()
|
||||
self.assertFalse(self.keyset.match(self.row1))
|
||||
self.assertFalse(self.row1 in self.keyset)
|
||||
|
||||
|
||||
class TestTable(tests.util.TestCase):
|
||||
|
@ -178,7 +225,7 @@ class TestTable(tests.util.TestCase):
|
|||
self.assertIsInstance(self.table, Gtk.FilterListModel)
|
||||
self.assertIsInstance(self.table.queue, emmental.db.idle.Queue)
|
||||
self.assertIsInstance(self.table.get_filter(),
|
||||
emmental.db.table.Filter)
|
||||
emmental.db.table.KeySet)
|
||||
self.assertIsInstance(self.table.store, emmental.store.SortedList)
|
||||
self.assertIsInstance(self.table.rows, dict)
|
||||
|
||||
|
@ -188,7 +235,7 @@ class TestTable(tests.util.TestCase):
|
|||
self.assertDictEqual(self.table.rows, {})
|
||||
self.assertTrue(self.table.get_incremental())
|
||||
|
||||
filter2 = emmental.db.table.Filter()
|
||||
filter2 = emmental.db.table.KeySet()
|
||||
queue2 = emmental.db.idle.Queue()
|
||||
table2 = emmental.db.table.Table(self.sql, filter=filter2,
|
||||
queue=queue2)
|
||||
|
@ -393,3 +440,147 @@ class TestTableFunctions(tests.util.TestCase):
|
|||
|
||||
self.table.create(number=3)
|
||||
self.assertFalse(self.table.update(row, "number", 3))
|
||||
|
||||
|
||||
class TestTableSubset(tests.util.TestCase):
|
||||
"""Tests the TableSubset."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up common variables."""
|
||||
super().setUp()
|
||||
self.table = tests.util.table.MockTable(self.sql)
|
||||
self.subset = emmental.db.table.TableSubset(self.table)
|
||||
self.rows = [self.table.create(number=i) for i in range(5)]
|
||||
|
||||
def test_init(self):
|
||||
"""Test that the TableSubset was set up properly."""
|
||||
self.assertIsInstance(self.subset, Gio.ListModel)
|
||||
self.assertIsInstance(self.subset, GObject.GObject)
|
||||
self.assertIsInstance(self.subset.keyset, emmental.db.table.KeySet)
|
||||
self.assertSetEqual(self.subset.keyset.keys, set())
|
||||
self.assertEqual(self.subset.table, self.table)
|
||||
|
||||
subset2 = emmental.db.table.TableSubset(self.table, keys={1, 2, 3})
|
||||
self.assertSetEqual(subset2.keyset.keys, {1, 2, 3})
|
||||
|
||||
def test_get_item_type(self):
|
||||
"""Test the Gio.ListModel.get_item_type() function."""
|
||||
self.assertEqual(self.subset.get_item_type(),
|
||||
emmental.db.table.Row.__gtype__)
|
||||
|
||||
def test_get_n_items(self):
|
||||
"""Test the Gio.ListModel.get_n_items() function."""
|
||||
self.assertEqual(self.subset.get_n_items(), 0)
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
|
||||
self.subset.add_row(self.rows[0])
|
||||
self.assertEqual(self.subset.get_n_items(), 0)
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
|
||||
self.table.loaded = True
|
||||
self.assertEqual(self.subset.get_n_items(), 1)
|
||||
self.assertEqual(self.subset.n_rows, 1)
|
||||
|
||||
self.table.loaded = False
|
||||
self.assertEqual(self.subset.get_n_items(), 0)
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
|
||||
def test_get_item(self):
|
||||
"""Test the Gio.ListModel.get_item() function."""
|
||||
for row in self.rows:
|
||||
self.subset.add_row(row)
|
||||
|
||||
self.assertListEqual(self.subset._items, [])
|
||||
|
||||
for i, row in enumerate(self.rows):
|
||||
with self.subTest(i=i, row=row.number):
|
||||
self.assertIsNone(self.subset.get_item(i))
|
||||
|
||||
self.table.loaded = True
|
||||
self.assertEqual(self.subset.get_item(i), row)
|
||||
self.assertEqual(self.subset._items[i], row)
|
||||
|
||||
self.table.loaded = False
|
||||
self.assertIsNone(self.subset.get_item(i))
|
||||
|
||||
def test_add_row(self):
|
||||
"""Test adding a row to the TableSubset."""
|
||||
expected = set()
|
||||
self.table.loaded = True
|
||||
self.assertListEqual(self.subset._items, [])
|
||||
|
||||
changed = unittest.mock.Mock()
|
||||
self.subset.connect("items-changed", changed)
|
||||
|
||||
for n, i in enumerate([2, 0, 4, 1, 3], start=1):
|
||||
row = self.rows[i]
|
||||
with self.subTest(i=i, row=row.number):
|
||||
expected.add(i)
|
||||
self.subset.add_row(row)
|
||||
self.assertSetEqual(self.subset.keyset.keys, expected)
|
||||
self.assertEqual(self.subset.n_rows, n)
|
||||
changed.assert_called_with(self.subset,
|
||||
sorted(expected).index(i), 0, 1)
|
||||
|
||||
self.assertListEqual(self.subset._items, self.rows)
|
||||
self.assertListEqual(list(self.subset), self.rows)
|
||||
|
||||
def test_remove_row(self):
|
||||
"""Test removing a row from the TableSubset."""
|
||||
self.table.loaded = True
|
||||
[self.subset.add_row(row) for row in self.rows]
|
||||
expected = {row.number for row in self.rows}
|
||||
|
||||
changed = unittest.mock.Mock()
|
||||
self.subset.connect("items-changed", changed)
|
||||
|
||||
for n, i in enumerate([2, 0, 4, 1, 3], start=1):
|
||||
row = self.rows[i]
|
||||
rm = sorted(expected).index(i)
|
||||
with self.subTest(i=i, row=row.number):
|
||||
expected.discard(i)
|
||||
self.subset.remove_row(row)
|
||||
self.assertSetEqual(self.subset.keyset.keys, expected)
|
||||
self.assertEqual(self.subset.n_rows, 5 - n)
|
||||
changed.assert_called_with(self.subset, rm, 1, 0)
|
||||
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
|
||||
def test_contains(self):
|
||||
"""Test the __contains__() magic method."""
|
||||
self.table.loaded = True
|
||||
self.assertFalse(self.rows[0] in self.subset)
|
||||
self.subset.add_row(self.rows[0])
|
||||
self.assertTrue(self.rows[0] in self.subset)
|
||||
|
||||
def test_table_not_loaded(self):
|
||||
"""Test operations when the table hasn't been loaded."""
|
||||
self.subset.add_row(self.rows[0])
|
||||
self.assertListEqual(self.subset._items, [])
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
self.assertIsNone(self.subset.get_item(0))
|
||||
|
||||
self.subset.remove_row(self.rows[0])
|
||||
self.assertListEqual(self.subset._items, [])
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
|
||||
def test_table_loaded(self):
|
||||
"""Test changing the value of Table.loaded."""
|
||||
changed = unittest.mock.Mock()
|
||||
self.subset.connect("items-changed", changed)
|
||||
|
||||
self.table.loaded = True
|
||||
changed.assert_not_called()
|
||||
self.table.loaded = False
|
||||
changed.assert_not_called()
|
||||
|
||||
self.subset.add_row(self.rows[0])
|
||||
self.subset.add_row(self.rows[1])
|
||||
|
||||
self.table.loaded = True
|
||||
self.assertEqual(self.subset.n_rows, 2)
|
||||
changed.assert_called_with(self.subset, 0, 0, 2)
|
||||
|
||||
self.table.loaded = False
|
||||
self.assertEqual(self.subset.n_rows, 0)
|
||||
changed.assert_called_with(self.subset, 0, 2, 0)
|
||||
|
|
|
@ -75,12 +75,14 @@ class TestYearTable(tests.util.TestCase):
|
|||
|
||||
def test_create(self):
|
||||
"""Test creating a year playlist."""
|
||||
decade = self.sql.decades.create(1980)
|
||||
year = self.table.create(1988)
|
||||
self.assertIsInstance(year, emmental.db.years.Year)
|
||||
self.assertEqual(year.year, 1988)
|
||||
self.assertEqual(year.name, "1988")
|
||||
self.assertEqual(year.sort_order,
|
||||
"release, albumartist, album, mediumno, number")
|
||||
self.assertTrue(year in decade.child_set)
|
||||
|
||||
cur = self.sql("SELECT COUNT(year) FROM years")
|
||||
self.assertEqual(cur.fetchone()["COUNT(year)"], 1)
|
||||
|
@ -93,8 +95,10 @@ class TestYearTable(tests.util.TestCase):
|
|||
|
||||
def test_delete(self):
|
||||
"""Test deleting a year playlist."""
|
||||
decade = self.sql.decades.create(1980)
|
||||
year = self.table.create(1988)
|
||||
self.assertTrue(year.delete())
|
||||
self.assertFalse(year in decade.child_set)
|
||||
|
||||
cur = self.sql("SELECT COUNT(year) FROM years")
|
||||
self.assertEqual(cur.fetchone()["COUNT(year)"], 0)
|
||||
|
|
|
@ -22,9 +22,9 @@ class TestEmmental(unittest.TestCase):
|
|||
"""Check that version constants have been set properly."""
|
||||
self.assertEqual(emmental.MAJOR_VERSION, 3)
|
||||
self.assertEqual(emmental.MINOR_VERSION, 0)
|
||||
self.assertEqual(emmental.MICRO_VERSION, 3)
|
||||
self.assertEqual(emmental.VERSION_NUMBER, "3.0.3")
|
||||
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.3-debug")
|
||||
self.assertEqual(emmental.MICRO_VERSION, 4)
|
||||
self.assertEqual(emmental.VERSION_NUMBER, "3.0.4")
|
||||
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.4-debug")
|
||||
|
||||
def test_application(self):
|
||||
"""Check that the application instance is initialized properly."""
|
||||
|
@ -63,7 +63,7 @@ class TestEmmental(unittest.TestCase):
|
|||
mock_startup.assert_called()
|
||||
mock_load.assert_called()
|
||||
mock_add_window.assert_called_with(self.application.win)
|
||||
mock_set_useragent.assert_called_with("emmental-debug", "3.0.3")
|
||||
mock_set_useragent.assert_called_with("emmental-debug", "3.0.4")
|
||||
|
||||
@unittest.mock.patch("sys.stdout")
|
||||
@unittest.mock.patch("gi.repository.Adw.Application.add_window")
|
||||
|
|
Loading…
Reference in New Issue