#!/usr/bin/env python3 # # TODO: # # Add book import dialog # Reading scroll with speed control # Ability to edit text with updates to word-section links # import os import re import sys from typing import Optional from PyQt6.QtCore import (QCoreApplication, QEvent, QModelIndex, QResource, pyqtSignal, pyqtSlot) from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow from lib.preferences import Preferences from lib import PersonDialog, Book, SessionDialog, ReadDialog, query_error from ui.MainWindow import Ui_MainWindow translate = QCoreApplication.translate class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self) -> None: super(MainWindow, self).__init__() self.setupUi(self) model = QSqlQueryModel() query = QSqlQuery("SELECT * FROM people ORDER BY name") model.setQuery(query) self.peopleView.setModel(model) self.peopleView.setModelColumn(1) self.actionEditPerson.setEnabled(False) # # Connections # # Action Connections # self.actionQuit.triggered.connect(self.close) self.actionAddBook.triggered.connect(self.addBook) self.actionEditBook.triggered.connect(self.editBook) self.actionRead.triggered.connect(self.readBook) self.actionRead.enabledChanged.connect(self.readBtn.setEnabled) self.actionAddPerson.triggered.connect(self.addPerson) self.actionEditPerson.triggered.connect(self.editPerson) self.actionEditPerson.enabledChanged.connect(self.editBtn.setEnabled) self.actionPreferences.triggered.connect(self.editPreferences) self.peopleView.doubleClicked.connect(self.editPerson) self.peopleView.clicked.connect(self.selectedPerson) self.show() return @pyqtSlot() def editPreferences(self) -> None: dlg = Preferences() dlg.exec() return @pyqtSlot(QModelIndex) def selectedPerson(self, index: QModelIndex) -> None: self.actionEditPerson.setEnabled(True) book_id = index.siblingAtColumn(3).data() if not book_id or book_id < 0: self.actionRead.setEnabled(False) else: self.actionRead.setEnabled(True) return @pyqtSlot(bool) def enablePerson(self, flag: bool) -> None: if flag: self.editBtn.setEnabled(False) self.readBtn.setEnabled(False) else: self.editBtn.setEnabled(True) self.readBtn.setEnabled(True) return @pyqtSlot() def addPerson(self) -> None: dlg = PersonDialog() dlg.exec() model = self.peopleView.model() model.setQuery(model.query()) return @pyqtSlot() @pyqtSlot(QModelIndex) def editPerson(self, index: Optional[QModelIndex] = None) -> None: if not index: indexes = self.peopleView.selectedIndexes() if len(indexes) < 1: return index = indexes[0] assert index is not None dlg = PersonDialog(person_id=index.siblingAtColumn(0).data()) dlg.exec() return @pyqtSlot() def addBook(self) -> None: directory = QFileDialog.getExistingDirectory() self.book = Book(directory) return @pyqtSlot() def editBook(self) -> None: print(self.tr("Edit Book")) return session = None setPerson = pyqtSignal(int) @pyqtSlot() def readBook(self) -> None: indexes = self.peopleView.selectedIndexes() if len(indexes) < 1: return person_id = indexes[0].siblingAtColumn(0).data() if not self.session: self.session = SessionDialog() self.setPerson.connect(self.session.setPerson) self.session.show() self.session.raise_() self.setPerson.emit(person_id) self.dlg = ReadDialog(self, self.session, person_id) self.dlg.show() self.dlg.raise_() return # # Events # def changeEvent(self, event: Optional[QEvent]) -> None: assert event is not None if event.type() == QEvent.Type.LanguageChange: self.retranslateUi(self) return SQL_CMDS = [ "PRAGMA foreign_keys=ON", # "CREATE TABLE IF NOT EXISTS words " "(word_id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT, definition TEXT)", # "CREATE TABLE IF NOT EXISTS books " "(book_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author TEXT, " "uuid TEXT, level INTEGER)", # "CREATE TABLE IF NOT EXISTS sections " "(section_id INTEGER PRIMARY KEY AUTOINCREMENT, " "sequence INTEGER, content TEXT, " "book_id INTEGER REFERENCES books ON DELETE CASCADE)", # "CREATE TABLE IF NOT EXISTS people " "(person_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, " "organization TEXT, book_id INTEGER REFERENCES books ON DELETE CASCADE, " "email TEXT)", # "CREATE TABLE IF NOT EXISTS person_book " "(person_id INTEGER REFERENCES people ON DELETE CASCADE, " "book_id INTEGER REFERENCES books ON DELETE CASCADE, " "section_id INTEGER REFERENCES sections, " "block INTEGER)", # "CREATE TABLE IF NOT EXISTS word_block " "(word_id INTEGER REFERENCES words ON DELETE CASCADE, " "section_id INTEGER REFERENCES sections ON DELETE CASCADE, " "block INTEGER NOT NULL, start INTEGER NOT NULL, end INTEGER NOT NULL)", # "CREATE TABLE IF NOT EXISTS sessions " "(session_id INTEGER PRIMARY KEY AUTOINCREMENT, " "person_id INTEGER REFERENCES people ON DELETE CASCADE, " "start TEXT DEFAULT '', " "stop TEXT DEFAULT '', " "total TEXT DEFAULT '', " "notes TEXT DEFAULT '')", # "CREATE TABLE IF NOT EXISTS session_word " "(session_id INTEGER REFERENCES sessions ON DELETE CASCADE, " "word_id INTEGER REFERENCES words ON DELETE CASCADE, " "important INTEGER DEFAULT 0)", # "CREATE TABLE IF NOT EXISTS session_block " "(session_id INTEGER REFERENCES sessions ON DELETE CASCADE, " "section_id INTEGER REFERENCES sections ON DELETE CASCADE, " "block INTEGER)", ] def schema_update(db: QSqlDatabase) -> None: query = QSqlQuery() for sql in SQL_CMDS: inlower = sql.lower().strip() if not inlower.startswith("create table "): if not query.exec(sql): query_error(query) continue create_cmd = re.sub(r"IF NOT EXISTS ", "", sql.strip()) create_cmd = re.sub(r"\s\s*", " ", create_cmd) matches = re.search(r"^(CREATE TABLE )([^ ]+)( \(.+)$", create_cmd) if matches: table_name = matches.group(2) create_cmd = ( matches.group(1) + '"' + matches.group(2) + '"' + matches.group(3) ) else: raise AttributeError(f"No match found: {create_cmd}") query.prepare("SELECT sql FROM sqlite_schema WHERE tbl_name = :tbl") query.bindValue(":tbl", table_name) if not query.exec(): query_error(query) query.next() old = query.value(0) if not old: if not query.exec(sql): query_error(query) continue if old.lower() == create_cmd.lower(): continue print(old.lower()) print(create_cmd.lower()) print(translate("MainWindow", "Updating: ") + f"{table_name}") # Step 1 turn off foreign key constraints if not query.exec("PRAGMA foreign_keys=OFF"): query_error(query) # Step 2 start a transaction db.transaction() # Step 3 remember old indexes, triggers, and views # Step 4 create new table new_table_name = table_name + "_new" if not query.exec(matches.group(1) + new_table_name + matches.group(3)): query_error(query) # step 5 transfer content coldefs = re.search(r"\((.+)\)", old).group(1).split(", ") # type: ignore[union-attr] cols = [x.split(" ")[0] for x in coldefs] cols_str = ", ".join(cols) if not query.exec( f"INSERT INTO {new_table_name} ({cols_str}) SELECT {cols_str} FROM {table_name}" ): query_error(query) # step 6 Drop old table if not query.exec("DROP TABLE " + table_name): query_error(query) # step 6 rename new table to old table if not query.exec( "ALTER TABLE " + new_table_name + " RENAME TO " + table_name ): query_error(query) # step 8 create indexes, triggers, and views # step 9 rebuild affected views # step 10 turn foreign key constrants back on if not query.exec("PRAGMA foreign_keys=ON"): query_error(query) # step 11 commit the changes db.commit() return def main() -> int: db = QSqlDatabase() db = db.addDatabase("QSQLITE") db.setDatabaseName("twel.db") db.open() app = QApplication(sys.argv) # # Setup resources # if not QResource.registerResource( os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/" ): raise Exception( translate("MainWindow", "Unable to register resources.rcc") ) schema_update(db) Preferences() window = MainWindow() # noqa: F841 return app.exec() if __name__ == "__main__": # # XXX - Update the Makefile so that we can run a `make -q` will # report back if make needs to be run. # sys.exit(main())