Files
esl-reader/main.py
Christopher T. Johnson 11c4801b7f checkpoint prior to class
2023-12-15 10:25:52 -05:00

327 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
#
# TODO:
# Record all words examined
#
# Add definition to definition
# Follow definition links
# Print subset of words, limit to words from this session's paragraphs
# plus defined during session
# Add Note per session
# Add book import dialog
# Add person create/edit dialog
# Reading scroll with speed control
# Move controls out of reading window.
# Ability to edit text with updates to word-section links
# Need to be able to place a visible cursor in text.
# Scroll to cursor
# XXX:
# Scrolling is messed up. Need a way of marking current line.
#
import os
import re
import sys
from datetime import datetime, timedelta
from typing import cast
from PyQt6.QtCore import QModelIndex, Qt, QTimer, pyqtSignal, pyqtSlot
from PyQt6.QtGui import (
QAction,
QFont,
QTextCharFormat,
QTextCursor,
QTextDocument,
QTextListFormat,
)
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
from PyQt6.QtWidgets import (
QApplication,
QFileDialog,
QMainWindow,
QMessageBox,
QPushButton,
)
from lib import *
from ui.MainWindow import Ui_MainWindow
def query_error(query: QSqlQuery) -> None:
print(
"SQL Error:\n{}\n{}\n{}:{}".format(
query.executedQuery(),
query.boundValues(),
query.lastError().type(),
query.lastError().text(),
)
)
raise Exception("SQL Error")
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self) -> None:
super(MainWindow, self).__init__()
self.setupUi(self)
# model = ModelOverride()
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) # Y
self.actionAddBook.triggered.connect(self.addBook) # Y
self.actionEditBook.triggered.connect(self.editBook) # Y
self.actionRead.triggered.connect(self.readBook) # Y
self.actionRead.enabledChanged.connect(self.readBtn.setEnabled)
self.actionAddPerson.triggered.connect(self.addPerson) # Y
self.actionEditPerson.triggered.connect(self.editPerson) # Y
self.actionEditPerson.enabledChanged.connect(
self.editBtn.setEnabled
) # Y
self.peopleView.doubleClicked.connect(self.editPerson) # Y
self.peopleView.clicked.connect(self.selectedPerson) # Y
self.show()
return
@pyqtSlot(QModelIndex)
def selectedPerson(self, index: QModelIndex) -> None:
person_id = index.siblingAtColumn(0).data()
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=None) -> None:
if not index:
indexes = self.peopleView.selectedIndexes()
if len(indexes) < 1:
return
index = indexes[0]
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("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 = EditDialog(self.session, person_id)
self.dlg.show()
self.dlg.raise_()
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(f"Updating: {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)
schema_update(db)
window: QMainWindow = MainWindow()
return app.exec()
if __name__ == "__main__":
outOfDate = []
for fileName in os.listdir("ui"):
if not fileName.endswith(".py"):
continue
uiName = "ui/" + fileName[:-3] + ".ui"
rccName = "ui/" + fileName[:-3] + ".qrc"
if not os.path.isfile(uiName) and not os.path.isfile(rccName):
outOfDate.append(filenName)
continue
if os.path.isfile(uiName) and os.path.getmtime(
uiName
) > os.path.getmtime("ui/" + fileName):
outOfDate.append(fileName)
if os.path.isfile(rccName) and os.path.getmtime(
rccName
) > os.path.getmtime("ui/" + fileName):
outOfDate.append(fileName)
if len(outOfDate) > 0:
print(f"UI out of date: {', '.join(outOfDate)}")
sys.exit(1)
sys.exit(main())