Rename main.py to esl_reader.py
This commit is contained in:
321
esl_reader.py
Executable file
321
esl_reader.py
Executable file
@@ -0,0 +1,321 @@
|
||||
#!/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.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 = 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())
|
||||
Reference in New Issue
Block a user