Files
esl-reader/main.py
Christopher T. Johnson 7f01c8d040 Setup LSP and clear all lint
2024-03-11 17:06:25 -04:00

302 lines
9.5 KiB
Python
Executable File

#!/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())