#!/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, cast from PyQt6.QtCore import ( QCoreApplication, QEvent, QModelIndex, QResource, pyqtSignal, pyqtSlot, ) from PyQt6.QtGui import QContextMenuEvent from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel from PyQt6.QtWidgets import QApplication, QFileDialog, QMainWindow, QMenu from lib import Book, PersonDialog, ReadDialog, SessionDialog, query_error from lib.preferences import Preferences 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 def contextMenuEvent(self, event: Optional[QContextMenuEvent]) -> None: assert event is not None localPos = self.peopleView.mapFromGlobal(event.globalPos()) if not self.peopleView.rect().contains(localPos): return index = self.peopleView.indexAt(localPos) if index.row() < 0: return menu = QMenu(self) menu.addAction(self.actionRead) self.actionEditPerson.setEnabled(True) menu.addAction(self.actionEditPerson) menu.exec(event.globalPos()) 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 = cast(QSqlQueryModel, self.peopleView.model()) assert model is not None 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, " "source 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())