From 02c0c9c8612e6364ea7ac254ef9868a0102997c6 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 29 Dec 2024 18:06:31 +0000 Subject: [PATCH] Implement template management Allow template edits and deletions. Deletions are now true deletes, not just flagged in database as deletes, and this applies to all playlists. Includes schema changes to cascade deletes. --- app/dbtables.py | 1 - app/models.py | 4 +- app/musicmuster.py | 105 +++++++++++++++++- app/ui/main_window_ui.py | 4 +- ..._remove_playlists_delete_and_implement_.py | 52 +++++++++ 5 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 migrations/versions/33c04e3c12c8_remove_playlists_delete_and_implement_.py diff --git a/app/dbtables.py b/app/dbtables.py index c2da638..e89a3b9 100644 --- a/app/dbtables.py +++ b/app/dbtables.py @@ -73,7 +73,6 @@ class PlaylistsTable(Model): tab: Mapped[Optional[int]] = mapped_column(default=None) open: Mapped[bool] = mapped_column(default=False) is_template: Mapped[bool] = mapped_column(default=False) - deleted: Mapped[bool] = mapped_column(default=False) rows: Mapped[List["PlaylistRowsTable"]] = relationship( "PlaylistRowsTable", back_populates="playlist", diff --git a/app/models.py b/app/models.py index 1d5f7ea..a5f0936 100644 --- a/app/models.py +++ b/app/models.py @@ -225,10 +225,10 @@ class Playlists(dbtables.PlaylistsTable): def delete(self, session: Session) -> None: """ - Mark as deleted + Delete playlist """ - self.deleted = True + session.execute(delete(Playlists).where(Playlists.id == self.id)) session.commit() @classmethod diff --git a/app/musicmuster.py b/app/musicmuster.py index 8cc75a1..a9bdc91 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -51,6 +51,7 @@ import stackprinter # type: ignore # App imports from classes import ( + ApplicationError, MusicMusterSignals, TrackInfo, ) @@ -96,6 +97,64 @@ class Current: ) +class EditDeleteDialog(QDialog): + def __init__(self, templates: list[tuple[str, int]]) -> None: + super().__init__() + self.templates = templates + self.selection: tuple[str, int] = ("", -1) + + self.init_ui() + + def init_ui(self) -> None: + # Create label + label = QLabel("Select template:") + + # Create combo box + self.combo_box = QComboBox() + for text, id_ in self.templates: + self.combo_box.addItem(text, id_) + + # Create buttons + edit_button = QPushButton("Edit") + delete_button = QPushButton("Delete") + cancel_button = QPushButton("Cancel") + + # Connect buttons + edit_button.clicked.connect(self.edit_clicked) + delete_button.clicked.connect(self.delete_clicked) + cancel_button.clicked.connect(self.cancel_clicked) + + # Layout setup + top_layout = QHBoxLayout() + top_layout.addWidget(label) + top_layout.addWidget(self.combo_box) + + bottom_layout = QHBoxLayout() + bottom_layout.addStretch() + bottom_layout.addWidget(edit_button) + bottom_layout.addWidget(delete_button) + bottom_layout.addWidget(cancel_button) + + main_layout = QVBoxLayout() + main_layout.addLayout(top_layout) + main_layout.addLayout(bottom_layout) + + self.setLayout(main_layout) + self.setWindowTitle("Edit or Delete Template") + + def edit_clicked(self) -> None: + self.selection = ("Edit", self.combo_box.currentData()) + self.accept() + + def delete_clicked(self) -> None: + self.selection = ("Delete", self.combo_box.currentData()) + self.accept() + + def cancel_clicked(self) -> None: + self.selection = ("Cancelled", -1) + self.reject() + + class PreviewManager: """ Manage track preview player @@ -803,8 +862,12 @@ class Window(QMainWindow, Ui_MainWindow): dlg.resize(500, 100) ok = dlg.exec() if ok: + if self.current.selected_rows: + new_row_number = self.current.selected_rows[0] + else: + new_row_number = self.current.base_model.rowCount() self.current.base_model.insert_row( - proposed_row_number=self.current.selected_rows[0], + proposed_row_number=new_row_number, note=dlg.textValue(), ) @@ -870,6 +933,46 @@ class Window(QMainWindow, Ui_MainWindow): self.signals.search_wikipedia_signal.emit(track_info.title) + def manage_templates(self) -> None: + """ + Delete / edit templates + """ + + # Build a list of (template-name, playlist-id) tuples + template_list: list[tuple[str, int]] = [] + + with db.Session() as session: + for template in Playlists.get_all_templates(session): + template_list.append((template.name, template.id)) + + # Get user's selection + dlg = EditDeleteDialog(template_list) + if not dlg.exec(): + return # User cancelled + + action, template_id = dlg.selection + + playlist = session.get(Playlists, template_id) + if not playlist: + log.error(f"Error opening {template_id=}") + + if action == "Edit": + # Simply load the template as a playlist. Any changes + # made will persist + idx = self.create_playlist_tab(playlist) + self.tabPlaylist.setCurrentIndex(idx) + + elif action == "Delete": + if helpers.ask_yes_no( + "Delete template", + f"Delete template '{playlist.name}': " "Are you sure?", + ): + if self.close_playlist_tab(): + playlist.delete(session) + session.commit() + else: + raise ApplicationError(f"Unrecognised action from EditDeleteDialog: {action=}") + def mark_rows_for_moving(self) -> None: """ Cut rows ready for pasting. diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index ddeace6..a763a63 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -677,5 +677,5 @@ class Ui_MainWindow(object): self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S")) self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows...")) self.actionReplace_files.setText(_translate("MainWindow", "Import files...")) -from infotabs import InfoTabs -from pyqtgraph import PlotWidget +from infotabs import InfoTabs # type: ignore +from pyqtgraph import PlotWidget # type: ignore diff --git a/migrations/versions/33c04e3c12c8_remove_playlists_delete_and_implement_.py b/migrations/versions/33c04e3c12c8_remove_playlists_delete_and_implement_.py new file mode 100644 index 0000000..d84c19f --- /dev/null +++ b/migrations/versions/33c04e3c12c8_remove_playlists_delete_and_implement_.py @@ -0,0 +1,52 @@ +"""Remove playlists.delete and implement Cascade deletes + +Revision ID: 33c04e3c12c8 +Revises: 164bd5ef3074 +Create Date: 2024-12-29 17:56:00.627198 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '33c04e3c12c8' +down_revision = '164bd5ef3074' +branch_labels = None +depends_on = None + + +def upgrade(engine_name: str) -> None: + globals()["upgrade_%s" % engine_name]() + + +def downgrade(engine_name: str) -> None: + globals()["downgrade_%s" % engine_name]() + + + + + +def upgrade_() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('playlist_rows', schema=None) as batch_op: + batch_op.drop_constraint('playlist_rows_ibfk_3', type_='foreignkey') + batch_op.create_foreign_key('playlist_rows_ibfk_3', 'playlists', ['playlist_id'], ['id'], ondelete='CASCADE') + + with op.batch_alter_table('playlists', schema=None) as batch_op: + batch_op.drop_column('deleted') + + # ### end Alembic commands ### + + +def downgrade_() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('playlists', schema=None) as batch_op: + batch_op.add_column(sa.Column('deleted', mysql.TINYINT(display_width=1), autoincrement=False, nullable=False)) + + with op.batch_alter_table('playlist_rows', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key(None, 'playlists', ['playlist_id'], ['id']) + + # ### end Alembic commands ### +