From 86a1678f41dbc048034074f2ae3a8781969fe60c Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Tue, 24 Oct 2023 20:48:28 +0100 Subject: [PATCH] WIP V3: move row initial tests working More tests to write --- app/dbconfig.py | 13 ----- app/models.py | 18 +++---- app/playlistmodel.py | 111 ++++++++++++++++++++++++++++++++++++++++-- app/playlists.py | 6 +-- conftest.py | 21 ++++---- pyproject.toml | 1 + test_playlistmodel.py | 105 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 238 insertions(+), 37 deletions(-) create mode 100644 test_playlistmodel.py diff --git a/app/dbconfig.py b/app/dbconfig.py index be78662..ee6949a 100644 --- a/app/dbconfig.py +++ b/app/dbconfig.py @@ -15,19 +15,6 @@ else: dbname = MYSQL_CONNECT.split("/")[-1] log.debug(f"Database: {dbname}") -# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION') -# testing = False -# if MM_ENV == 'TESTING': -# dbname = os.environ.get('MM_TESTING_DBNAME', 'musicmuster_testing') -# dbuser = os.environ.get('MM_TESTING_DBUSER', 'musicmuster_testing') -# dbpw = os.environ.get('MM_TESTING_DBPW', 'musicmuster_testing') -# dbhost = os.environ.get('MM_TESTING_DBHOST', 'localhost') -# testing = True -# else: -# raise ValueError(f"Unknown MusicMuster environment: {MM_ENV=}") -# -# MYSQL_CONNECT = f"mysql+mysqldb://{dbuser}:{dbpw}@{dbhost}/{dbname}" - engine = create_engine( MYSQL_CONNECT, echo=Config.DISPLAY_SQL, diff --git a/app/models.py b/app/models.py index a261b83..b9df2c5 100644 --- a/app/models.py +++ b/app/models.py @@ -123,13 +123,7 @@ class NoteColours(Base): Return all records """ - return ( - session.execute( - select(cls) - ) - .scalars() - .all() - ) + return session.execute(select(cls)).scalars().all() @staticmethod def get_colour(session: scoped_session, text: str) -> Optional[str]: @@ -419,9 +413,9 @@ class PlaylistRows(Base): self, session: scoped_session, playlist_id: int, - track_id: Optional[int], row_number: int, note: str = "", + track_id: Optional[int] = None, ) -> None: """Create PlaylistRows object""" @@ -454,7 +448,13 @@ class PlaylistRows(Base): ) for plr in src_rows: - PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note) + PlaylistRows( + session=session, + playlist_id=dst_id, + row_number=plr.plr_rownum, + note=plr.note, + track_id=plr.track_id, + ) @staticmethod def delete_higher_rows( diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 4b5bbf8..be43a23 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -1,6 +1,8 @@ from datetime import datetime from enum import auto, Enum -from typing import Optional, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING + +from dbconfig import scoped_session, Session from PyQt6.QtCore import ( QAbstractTableModel, @@ -17,8 +19,6 @@ from PyQt6.QtGui import ( from config import Config -from dbconfig import Session - from helpers import ( file_is_unreadable, ) @@ -282,6 +282,110 @@ class PlaylistModel(QAbstractTableModel): return QVariant() + def insert_row(self, session: scoped_session, row_number: int) -> int: + """ + Make space for a row at row_number. If row_number is greater + than length of list plus 1, or if row number is -1, put row at + end of list. + + Return new row number that has an empty, valid entry in self.playlist_rows. + """ + + if row_number > len(self.playlist_rows) or row_number == -1: + new_row_number = len(self.playlist_rows) + 1 + elif row_number < 0: + raise ValueError( + f"playlistmodel.insert_row, invalid row number ({row_number})" + ) + else: + new_row_number = row_number + + # Move rows below new row down + modified_rows: List[int] = [] + for i in reversed(range(new_row_number, len(self.playlist_rows))): + self.playlist_rows[i + 1] = self.playlist_rows[i] + self.playlist_rows[i + 1].plr_rownum += 1 + modified_rows.append(i + 1) + + # Replace old row + self.playlist_rows[new_row_number] = PlaylistRowData( + PlaylistRows( + session=session, playlist_id=self.playlist_id, row_number=new_row_number + ) + ) + + # Refresh rows + self.invalidate_rows(modified_rows) + + return new_row_number + + def invalidate_rows(self, modified_rows: List[int]) -> None: + """ + Signal to view to refresh invlidated rows + """ + + for modified_row in modified_rows: + self.dataChanged.emit( + self.index(modified_row, 0), + self.index(modified_row, self.columnCount()), + ) + + def move_rows(self, from_rows: List[int], to_row: int) -> None: + """ + Move the playlist rows given to to_row and below. + """ + + # New thinking: + # Move relocated rows to correct place in mirror array + # Copy souurce to mirror around them + # Move mirror to original + # Fixup plr rownumbers and update db and display + # Signal rows have changed + + # Prep + # modified_rows: List[int] = [] + # moving_rows: dict[int, PlaylistRowData] = {} + new_playlist_rows: dict[int, PlaylistRowData] = {} + + # Move the from_row records from the playlist_rows dict to the + # new_playlist_rows dict + next_to_row = to_row + for from_row in from_rows: + new_playlist_rows[next_to_row] = self.playlist_rows[from_row] + del self.playlist_rows[from_row] + next_to_row += 1 + + # Move the remaining rows to the gaps in new_playlist_rows + new_row = 0 + for old_row in self.playlist_rows.keys(): + # Find next gap + while new_row in new_playlist_rows: + new_row += 1 + new_playlist_rows[new_row] = self.playlist_rows[old_row] + new_row += 1 + + # Make copy of rows live + self.playlist_rows = new_playlist_rows + + # Update PlaylistRows table and notify display of rows that + # moved + with Session() as session: + for idx in range(len(self.playlist_rows)): + if self.playlist_rows[idx].plr_rownum == idx: + continue + # Row number in this row is incorred. Fix it in + # database: + plr = session.get(PlaylistRows, self.playlist_rows[idx].plrid) + if not plr: + print(f"\nCan't find plr in playlistmodel:move_rows {idx=}") + continue + plr.plr_rownum = idx + # Fix in self.playlist_rows + self.playlist_rows[idx].plr_rownum = idx + # Update display + self.invalidate_rows([idx]) + print(f"Fixup {idx=}") + def refresh_data(self): """Populate dicts for data calls""" @@ -294,6 +398,7 @@ class PlaylistModel(QAbstractTableModel): """Populate dict for one row for data calls""" p = PlaylistRows.deep_row(session, self.playlist_id, row_number) + self.playlist_rows.clear() self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) def rowCount(self, index: QModelIndex = QModelIndex()) -> int: diff --git a/app/playlists.py b/app/playlists.py index b38dfe2..72ec598 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -227,7 +227,7 @@ class PlaylistTab(QTableView): and 0 <= max(from_rows) <= self.model().rowCount() and 0 <= to_row <= self.model().rowCount() ): - print(f"move_rows({from_rows=}, {to_row=})") + self.model().move_rows(from_rows, to_row) event.accept() super().dropEvent(event) @@ -605,7 +605,7 @@ class PlaylistTab(QTableView): # """ # row_number = self.get_new_row_number() - # plr = PlaylistRows(session, self.playlist_id, None, row_number, note) + # TODO: check arg order plr = PlaylistRows(session, self.playlist_id, None, row_number, note) # self.insert_row(session, plr) # self._set_row_header_text(session, row_number, note) # self.save_playlist(session) @@ -694,7 +694,7 @@ class PlaylistTab(QTableView): # return self._move_row(session, existing_plr, row_number) # # Build playlist_row object - # plr = PlaylistRows(session, self.playlist_id, track.id, row_number, note) + # plr = TODO: check arg order PlaylistRows(session, self.playlist_id, track.id, row_number, note) # self.insert_row(session, plr) # self.save_playlist(session) # self._update_start_end_times(session) diff --git a/conftest.py b/conftest.py index 92b1374..2f435d5 100644 --- a/conftest.py +++ b/conftest.py @@ -8,30 +8,33 @@ from sqlalchemy.orm import scoped_session, sessionmaker from app.models import Base, Tracks +DB_CONNECTION = \ + "mysql+mysqldb://musicmuster_testing:musicmuster_testing@localhost/dev_musicmuster_testing" + @pytest.fixture(scope="session") def db_engine(): - engine = create_engine( - "mysql+mysqldb://musicmuster_testing:musicmuster_testing@localhost/dev_musicmuster_testing" - ) + engine = create_engine(DB_CONNECTION, isolation_level="READ COMMITTED") Base.metadata.create_all(engine) yield engine engine.dispose() -@pytest.fixture(scope='function') -def session(db_engine): +@pytest.fixture(scope="function") +def Session(db_engine): connection = db_engine.connect() transaction = connection.begin() - Session = sessionmaker(bind=connection) - session = scoped_session(Session) + sm = sessionmaker(bind=connection) + session = scoped_session(sm) + # print(f"PyTest SqlA: session acquired [{hex(id(session))}]") yield session + # print(f" PyTest SqlA: session released and cleaned up [{hex(id(session))}]") session.remove() transaction.rollback() connection.close() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def track1(session): track_path = "testdata/isa.mp3" metadata = helpers.get_file_metadata(track_path) @@ -39,7 +42,7 @@ def track1(session): return track -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def track2(session): track_path = "testdata/mom.mp3" metadata = helpers.get_file_metadata(track_path) diff --git a/pyproject.toml b/pyproject.toml index 0927b3b..943cfc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ mypy_path = "/home/kae/git/musicmuster/app" [tool.pytest.ini_options] addopts = "--exitfirst --showlocals --capture=no" pythonpath = [".", "app"] +filterwarnings = "ignore:'audioop' is deprecated" [tool.vulture] exclude = ["migrations", "app/ui", "archive"] diff --git a/test_playlistmodel.py b/test_playlistmodel.py new file mode 100644 index 0000000..80e8573 --- /dev/null +++ b/test_playlistmodel.py @@ -0,0 +1,105 @@ +from app.models import ( + Playlists, +) +from app import playlistmodel +from dbconfig import scoped_session + + +def create_model_with_playlist_rows( + session: scoped_session, rows: int +) -> "playlistmodel.PlaylistModel": + playlist = Playlists(session, "test playlist") + # Create a model + model = playlistmodel.PlaylistModel(playlist.id, None) + for row in range(rows): + newrow = model.insert_row(session, row) + model.playlist_rows[newrow].note = str(newrow) + + session.commit() + return model + + +def test_insert_row(monkeypatch, Session): + monkeypatch.setattr(playlistmodel, "Session", Session) + # Create a playlist + with Session() as session: + playlist = Playlists(Session, "test playlist") + # Create a model + model = playlistmodel.PlaylistModel(playlist.id, None) + assert model.rowCount() == 0 + model.insert_row(session, 0) + assert model.rowCount() == 1 + + +def test_insert_high_row(monkeypatch, Session): + monkeypatch.setattr(playlistmodel, "Session", Session) + # Create a playlist + with Session() as session: + playlist = Playlists(Session, "test playlist") + # Create a model + model = playlistmodel.PlaylistModel(playlist.id, None) + assert model.rowCount() == 0 + model.insert_row(session, 5) + assert model.rowCount() == 1 + + +def test_11_row_playlist(monkeypatch, Session): + # Create multirow playlist + monkeypatch.setattr(playlistmodel, "Session", Session) + with Session() as session: + model = create_model_with_playlist_rows(session, 11) + assert model.rowCount() == 11 + assert max(model.playlist_rows.keys()) == 10 + for row in range(model.rowCount()): + assert row in model.playlist_rows + assert model.playlist_rows[row].plr_rownum == row + + +def test_move_rows_test2(monkeypatch, Session): + # move row 3 to row 5 + monkeypatch.setattr(playlistmodel, "Session", Session) + with Session() as session: + model = create_model_with_playlist_rows(session, 11) + model.move_rows([3], 5) + # Check we have all rows and plr_rownums are correct + for row in range(model.rowCount()): + assert row in model.playlist_rows + assert model.playlist_rows[row].plr_rownum == row + if row not in [3, 4, 5]: + assert model.playlist_rows[row].note == str(row) + elif row == 3: + assert model.playlist_rows[row].note == str(4) + elif row == 4: + assert model.playlist_rows[row].note == str(5) + elif row == 5: + assert model.playlist_rows[row].note == str(3) + + +# def test_move_rows_test3(Session): +# # move row 4 to row 3 +# pass + + +# def test_move_rows_test4(Session): +# # move row 4 to row 2 +# pass + + +# def test_move_rows_test5(Session): +# # move rows [1, 4, 5, 10] → 8 +# pass + + +# def test_move_rows_test6(Session): +# # move rows [3, 6] → 5 +# pass + + +# def test_move_rows_test7(Session): +# # move rows [3, 5, 6] → 8 +# pass + + +# def test_move_rows_test8(Session): +# # move rows [7, 8, 10] → 5 +# pass