From 374a3127974e4eca9fa98a49cfddbdae7cd26a39 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Mon, 4 Jul 2022 21:32:23 +0100 Subject: [PATCH] SQLA2.0 schema updates, column width saves --- alembic.ini | 1 + app/models.py | 235 ++++++++++-------- app/playlists.py | 76 +++--- ...rename_playlist_tracks_to_playlist_rows.py | 26 ++ ...56f_increase_settings_name_len_and_add_.py | 36 +++ 5 files changed, 243 insertions(+), 131 deletions(-) create mode 100644 migrations/versions/3f55ac7d80ad_rename_playlist_tracks_to_playlist_rows.py create mode 100644 migrations/versions/51f61433256f_increase_settings_name_len_and_add_.py diff --git a/alembic.ini b/alembic.ini index 214a93f..e19c527 100644 --- a/alembic.ini +++ b/alembic.ini @@ -43,6 +43,7 @@ sqlalchemy.url = SET # sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod # sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev # sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2 +# sqlalchemy.url = mysql+mysqldb://musicmusterv3:musicmusterv3@localhost/musicmuster_dev_v3 [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run diff --git a/app/models.py b/app/models.py index ba9a5a6..c5dbd37 100644 --- a/app/models.py +++ b/app/models.py @@ -15,8 +15,8 @@ from sqlalchemy import ( Boolean, Column, DateTime, - # Float, - # ForeignKey, + Float, + ForeignKey, # func, Integer, String, @@ -25,13 +25,16 @@ from sqlalchemy import ( ) # from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import ( - # backref, + backref, declarative_base, - # relationship, - # RelationshipProperty + relationship, + RelationshipProperty +) +from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.exc import ( + # MultipleResultsFound, + NoResultFound ) -# from sqlalchemy.orm.collections import attribute_mapped_collection -# from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # # from config import Config # from helpers import ( @@ -45,17 +48,17 @@ from sqlalchemy.orm import ( Base = declarative_base() # # -# # Database classes -# class NoteColours(Base): -# __tablename__ = 'notecolours' -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# substring: str = Column(String(256), index=False) -# colour: str = Column(String(21), index=False) -# enabled: bool = Column(Boolean, default=True, index=True) -# is_regex: bool = Column(Boolean, default=False, index=False) -# is_casesensitive: bool = Column(Boolean, default=False, index=False) -# order: int = Column(Integer, index=True) +# Database classes +class NoteColours(Base): + __tablename__ = 'notecolours' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + substring: str = Column(String(256), index=False) + colour: str = Column(String(21), index=False) + enabled: bool = Column(Boolean, default=True, index=True) + is_regex: bool = Column(Boolean, default=False, index=False) + is_casesensitive: bool = Column(Boolean, default=False, index=False) + order: int = Column(Integer, index=True) # # def __init__( # self, session: Session, substring: str, colour: str, @@ -119,17 +122,17 @@ Base = declarative_base() # return rec.colour # # return None -# -# -# class Notes(Base): -# __tablename__ = 'notes' -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# playlist_id: int = Column(Integer, ForeignKey('playlists.id')) -# playlist: RelationshipProperty = relationship( -# "Playlists", back_populates="notes", lazy="joined") -# row: int = Column(Integer, nullable=False) -# note: str = Column(String(256), index=False) + + +class Notes(Base): + __tablename__ = 'notes' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + playlist_id: int = Column(Integer, ForeignKey('playlists.id')) + playlist: RelationshipProperty = relationship( + "Playlists", back_populates="notes", lazy="joined") + row: int = Column(Integer, nullable=False) + note: str = Column(String(256), index=False) # # def __init__(self, session: Session, playlist_id: int, # row: int, text: str) -> None: @@ -204,16 +207,16 @@ Base = declarative_base() # if text: # self.note = text # session.flush() -# -# -# class Playdates(Base): -# __tablename__ = 'playdates' -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# lastplayed: datetime = Column(DateTime, index=True, default=None) -# track_id: int = Column(Integer, ForeignKey('tracks.id')) -# track: RelationshipProperty = relationship( -# "Tracks", back_populates="playdates", lazy="joined") + + +class Playdates(Base): + __tablename__ = 'playdates' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + lastplayed: datetime = Column(DateTime, index=True, default=None) + track_id: int = Column(Integer, ForeignKey('tracks.id')) + track: RelationshipProperty = relationship( + "Tracks", back_populates="playdates", lazy="joined") # # def __init__(self, session: Session, track_id: int) -> None: # """Record that track was played""" @@ -294,7 +297,7 @@ class Playlists(Base): # if row is None: # row = self.next_free_row(session, self.id) # -# PlaylistTracks(session, self.id, track_id, row) +# xPlaylistTracks(session, self.id, track_id, row) # # def close(self, session: Session) -> None: # """Record playlist as no longer loaded""" @@ -353,7 +356,7 @@ class Playlists(Base): # """Return next free row for this playlist""" # # max_notes_row = Notes.max_used_row(session, playlist_id) -# max_tracks_row = PlaylistTracks.max_used_row(session, playlist_id) +# max_tracks_row = xPlaylistTracks.max_used_row(session, playlist_id) # # if max_notes_row is not None and max_tracks_row is not None: # return max(max_notes_row, max_tracks_row) + 1 @@ -390,7 +393,7 @@ class Playlists(Base): # # class PlaylistTracks(Base): # __tablename__ = 'playlist_tracks' -# +# # id: int = Column(Integer, primary_key=True, autoincrement=True) # playlist_id: int = Column(Integer, ForeignKey('playlists.id'), # primary_key=True) @@ -406,6 +409,25 @@ class Playlists(Base): # cascade="all, delete-orphan" # ) # ) +class PlaylistRows(Base): + __tablename__ = 'playlist_rows' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + playlist_id: int = Column(Integer, ForeignKey('playlists.id'), + primary_key=True) + row: int = Column(Integer, nullable=False) + text: str = Column(String(2048), index=False) + track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True) + tracks: RelationshipProperty = relationship("Tracks") + playlist: RelationshipProperty = relationship( + Playlists, + backref=backref( + "playlist_tracks", + collection_class=attribute_mapped_collection("row"), + lazy="joined", + cascade="all, delete-orphan" + ) + ) # # Ensure row numbers are unique within each playlist # __table_args__ = (UniqueConstraint # ('row', 'playlist_id', name="uniquerow"), @@ -414,7 +436,7 @@ class Playlists(Base): # def __init__( # self, session: Session, playlist_id: int, track_id: int, # row: int) -> None: -# log.debug(f"PlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})") +# log.debug(f"xPlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})") # # self.playlist_id = playlist_id # self.track_id = track_id @@ -430,7 +452,7 @@ class Playlists(Base): # """ # # last_row = session.query( -# func.max(PlaylistTracks.row) +# func.max(xPlaylistTracks.row) # ).filter_by(playlist_id=playlist_id).first() # # if there are no rows, the above returns (None, ) which is True # if last_row and last_row[0] is not None: @@ -443,65 +465,74 @@ class Playlists(Base): # to_row: int, to_playlist_id: int) -> None: # """Move row to another playlist""" # -# session.query(PlaylistTracks).filter( -# PlaylistTracks.playlist_id == from_playlist_id, -# PlaylistTracks.row == from_row).update( +# session.query(xPlaylistTracks).filter( +# xPlaylistTracks.playlist_id == from_playlist_id, +# xPlaylistTracks.row == from_row).update( # {'playlist_id': to_playlist_id, 'row': to_row}, False) -# -# -# class Settings(Base): -# __tablename__ = 'settings' -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# name: str = Column(String(32), nullable=False, unique=True) -# f_datetime: datetime = Column(DateTime, default=None, nullable=True) -# f_int: int = Column(Integer, default=None, nullable=True) -# f_string: str = Column(String(128), default=None, nullable=True) -# -# @classmethod -# def get_int_settings(cls, session: Session, name: str) -> "Settings": -# """Get setting for an integer or return new setting record""" -# -# int_setting: Settings -# -# try: -# int_setting = session.query(cls).filter( -# cls.name == name).one() -# except NoResultFound: -# int_setting = Settings() -# int_setting.name = name -# int_setting.f_int = None -# session.add(int_setting) -# session.flush() -# return int_setting -# -# def update(self, session: Session, data): -# for key, value in data.items(): -# assert hasattr(self, key) -# setattr(self, key, value) -# session.flush() -# -# -# class Tracks(Base): -# __tablename__ = 'tracks' -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# title: str = Column(String(256), index=True) -# artist: str = Column(String(256), index=True) -# duration: int = Column(Integer, index=True) -# start_gap: int = Column(Integer, index=False) -# fade_at: int = Column(Integer, index=False) -# silence_at: int = Column(Integer, index=False) -# path: str = Column(String(2048), index=False, nullable=False) -# mtime: float = Column(Float, index=True) -# lastplayed: datetime = Column(DateTime, index=True, default=None) -# playlists: RelationshipProperty = relationship("PlaylistTracks", -# back_populates="tracks", -# lazy="joined") -# playdates: RelationshipProperty = relationship("Playdates", -# back_populates="track" -# "", -# lazy="joined") + + +class Settings(Base): + """Manage settings""" + + __tablename__ = 'settings' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(64), nullable=False, unique=True) + f_datetime: datetime = Column(DateTime, default=None, nullable=True) + f_int: int = Column(Integer, default=None, nullable=True) + f_string: str = Column(String(128), default=None, nullable=True) + + def __repr__(self) -> str: + value = self.f_datetime or self.f_int or self.f_string + return f"" + + @classmethod + def get_int_settings(cls, session: Session, name: str) -> "Settings": + """Get setting for an integer or return new setting record""" + + int_setting: Settings + + try: + int_setting = session.execute( + select(cls) + .where(cls.name == name) + ).scalar_one() + + except NoResultFound: + int_setting = Settings() + int_setting.name = name + int_setting.f_int = None + session.add(int_setting) + + return int_setting + + def update(self, session: Session, data): + for key, value in data.items(): + assert hasattr(self, key) + setattr(self, key, value) + session.flush() + + +class Tracks(Base): + __tablename__ = 'tracks' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + title: str = Column(String(256), index=True) + artist: str = Column(String(256), index=True) + duration: int = Column(Integer, index=True) + start_gap: int = Column(Integer, index=False) + fade_at: int = Column(Integer, index=False) + silence_at: int = Column(Integer, index=False) + path: str = Column(String(2048), index=False, nullable=False) + mtime: float = Column(Float, index=True) + lastplayed: datetime = Column(DateTime, index=True, default=None) + playlists: RelationshipProperty = relationship("PlaylistRows", + back_populates="tracks", + lazy="joined") + playdates: RelationshipProperty = relationship("Playdates", + back_populates="track" + "", + lazy="joined") # # def __init__( # self, diff --git a/app/playlists.py b/app/playlists.py index 582a0de..9f1a495 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -31,17 +31,16 @@ from config import Config # from datetime import datetime, timedelta # from helpers import get_relative_date, open_in_audacity # from log import log.debug, log.error -# from models import ( -# Notes, -# Playdates, -# Playlists, -# PlaylistTracks, -# Settings, -# Tracks, -# NoteColours -# ) +from models import ( + # Notes, + # Playdates, + # Playlists, + # PlaylistTracks, + Settings, + # Tracks, + # NoteColours +) from dbconfig import Session -#from collections import namedtuple # start_time_re = re.compile(r"@\d\d:\d\d:\d\d") # # @@ -94,9 +93,9 @@ class PlaylistTab(QTableWidget): def __init__(self, musicmuster: QMainWindow, session: Session, playlist_id: int, *args, **kwargs): super().__init__(*args, **kwargs) - self.musicmuster: QMainWindow = musicmuster self.playlist_id: int = playlist_id + # self.menu: Optional[QMenu] = None # self.current_track_start_time: Optional[datetime] = None # @@ -111,19 +110,19 @@ class PlaylistTab(QTableWidget): # self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.setRowCount(0) self.setColumnCount(len(columns)) - # Add header row + + # Header row for idx in [a for a in range(len(columns))]: item: QTableWidgetItem = QTableWidgetItem() self.setHorizontalHeaderItem(idx, item) - self.horizontalHeader().setMinimumSectionSize(0) - # self._set_column_widths(session) + self._set_column_widths(session) # Set column headings sorted by idx self.setHorizontalHeaderLabels( [a[0].heading for a in list(sorted(columns.values(), key=lambda item: item[0][0]))] ) -# + # self.setDragEnabled(True) # self.setAcceptDrops(True) # self.viewport().setAcceptDrops(True) @@ -146,15 +145,33 @@ class PlaylistTab(QTableWidget): # self.row_filter: Optional[str] = None # self.editing_cell: bool = False # self.selecting_in_progress = False + # Connect signals # self.cellChanged.connect(self._cell_changed) # self.cellClicked.connect(self._edit_note_cell) -# self.doubleClicked.connect(self._edit_cell) -# self.cellEditingStarted.connect(self._cell_edit_started) # self.cellEditingEnded.connect(self._cell_edit_ended) +# self.cellEditingStarted.connect(self._cell_edit_started) +# self.doubleClicked.connect(self._edit_cell) + self.horizontalHeader().sectionResized.connect(self._column_resize) # # # Now load our tracks and notes # self.populate(session, self.playlist_id) -# + + def _column_resize(self, idx, old, new): + """ + Called when column widths are changed. + + Save column sizes to database + """ + + with Session() as session: + for column_name, data in columns.items(): + idx = data[0].idx + width = self.columnWidth(idx) + attribute_name = f"playlist_{column_name}_col_width" + record = Settings.get_int_settings(session, attribute_name) + if record.f_int != self.columnWidth(idx): + record.update(session, {'f_int': width}) + # def __repr__(self) -> str: # return f" None: -# """Column widths from settings""" -# -# for column in range(self.columnCount()): -# name: str = f"playlist_col_{str(column)}_width" -# record: Settings = Settings.get_int_settings(session, name) -# if record and record.f_int is not None: -# self.setColumnWidth(column, record.f_int) -# else: -# self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH) + + def _set_column_widths(self, session: Session) -> None: + """Column widths from settings""" + + for column_name, data in columns.items(): + idx = data[0].idx + attr_name = f"playlist_{column_name}_col_width" + record: Settings = Settings.get_int_settings(session, attr_name) + if record and record.f_int is not None: + self.setColumnWidth(idx, record.f_int) + else: + self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) # # def _set_next(self, row: int, session: Session) -> None: # """ diff --git a/migrations/versions/3f55ac7d80ad_rename_playlist_tracks_to_playlist_rows.py b/migrations/versions/3f55ac7d80ad_rename_playlist_tracks_to_playlist_rows.py new file mode 100644 index 0000000..6a2f153 --- /dev/null +++ b/migrations/versions/3f55ac7d80ad_rename_playlist_tracks_to_playlist_rows.py @@ -0,0 +1,26 @@ +"""Rename playlist_tracks to playlist_rows + +Revision ID: 3f55ac7d80ad +Revises: 1c4048efee96 +Create Date: 2022-07-04 20:51:59.874004 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3f55ac7d80ad' +down_revision = '1c4048efee96' +branch_labels = None +depends_on = None + + +def upgrade(): + # Rename so as not to lose content + op.rename_table('playlist_tracks', 'playlist_rows') + + +def downgrade(): + # Rename so as not to lose content + op.rename_table('playlist_rows', 'playlist_tracks') diff --git a/migrations/versions/51f61433256f_increase_settings_name_len_and_add_.py b/migrations/versions/51f61433256f_increase_settings_name_len_and_add_.py new file mode 100644 index 0000000..3ccb342 --- /dev/null +++ b/migrations/versions/51f61433256f_increase_settings_name_len_and_add_.py @@ -0,0 +1,36 @@ +"""Increase settings.name len and add playlist_rows.notes + +Revision ID: 51f61433256f +Revises: 3f55ac7d80ad +Create Date: 2022-07-04 21:21:39.830406 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '51f61433256f' +down_revision = '3f55ac7d80ad' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('playlist_rows', sa.Column('text', sa.String(length=2048), nullable=True)) + # op.drop_index('uniquerow', table_name='playlist_rows') + op.alter_column('playlists', 'loaded', + existing_type=mysql.TINYINT(display_width=1), + nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('playlists', 'loaded', + existing_type=mysql.TINYINT(display_width=1), + nullable=True) + # op.create_index('uniquerow', 'playlist_rows', ['row', 'playlist_id'], unique=False) + op.drop_column('playlist_rows', 'text') + # ### end Alembic commands ###