From 65d8cbafe34b946d450ce46703e6449eb9962741 Mon Sep 17 00:00:00 2001 From: Louie Shprung Date: Thu, 21 Dec 2023 13:17:39 -0800 Subject: Rename src directory to unique name to avoid install issues --- MANIFEST.in | 1 + assignment-list | 2 +- assignment_list_pyqt/__init__.py | 0 assignment_list_pyqt/add_entry_form.py | 51 +++ assignment_list_pyqt/add_entry_form.ui | 185 +++++++++++ assignment_list_pyqt/add_group_form.py | 47 +++ assignment_list_pyqt/add_group_form.ui | 131 ++++++++ assignment_list_pyqt/config.py | 95 ++++++ assignment_list_pyqt/db_sqlite.py | 493 +++++++++++++++++++++++++++++ assignment_list_pyqt/edit_entry_form.py | 83 +++++ assignment_list_pyqt/edit_group_form.py | 63 ++++ assignment_list_pyqt/entry.py | 100 ++++++ assignment_list_pyqt/globals.py | 4 + assignment_list_pyqt/group.py | 25 ++ assignment_list_pyqt/main.py | 273 ++++++++++++++++ assignment_list_pyqt/main.ui | 207 ++++++++++++ assignment_list_pyqt/preferences_dialog.py | 71 +++++ assignment_list_pyqt/preferences_dialog.ui | 128 ++++++++ assignment_list_pyqt/rule.py | 44 +++ assignment_list_pyqt/rules_dialog.py | 128 ++++++++ setup.cfg | 2 +- src/__init__.py | 0 src/add_entry_form.py | 51 --- src/add_entry_form.ui | 185 ----------- src/add_group_form.py | 47 --- src/add_group_form.ui | 131 -------- src/config.py | 95 ------ src/db_sqlite.py | 493 ----------------------------- src/edit_entry_form.py | 83 ----- src/edit_group_form.py | 63 ---- src/entry.py | 100 ------ src/globals.py | 4 - src/group.py | 25 -- src/main.py | 273 ---------------- src/main.ui | 207 ------------ src/preferences_dialog.py | 70 ---- src/preferences_dialog.ui | 128 -------- src/rule.py | 44 --- src/rules_dialog.py | 128 -------- 39 files changed, 2131 insertions(+), 2129 deletions(-) create mode 100644 assignment_list_pyqt/__init__.py create mode 100644 assignment_list_pyqt/add_entry_form.py create mode 100644 assignment_list_pyqt/add_entry_form.ui create mode 100644 assignment_list_pyqt/add_group_form.py create mode 100644 assignment_list_pyqt/add_group_form.ui create mode 100644 assignment_list_pyqt/config.py create mode 100644 assignment_list_pyqt/db_sqlite.py create mode 100644 assignment_list_pyqt/edit_entry_form.py create mode 100644 assignment_list_pyqt/edit_group_form.py create mode 100644 assignment_list_pyqt/entry.py create mode 100644 assignment_list_pyqt/globals.py create mode 100644 assignment_list_pyqt/group.py create mode 100644 assignment_list_pyqt/main.py create mode 100644 assignment_list_pyqt/main.ui create mode 100644 assignment_list_pyqt/preferences_dialog.py create mode 100644 assignment_list_pyqt/preferences_dialog.ui create mode 100644 assignment_list_pyqt/rule.py create mode 100644 assignment_list_pyqt/rules_dialog.py delete mode 100644 src/__init__.py delete mode 100644 src/add_entry_form.py delete mode 100644 src/add_entry_form.ui delete mode 100644 src/add_group_form.py delete mode 100644 src/add_group_form.ui delete mode 100644 src/config.py delete mode 100644 src/db_sqlite.py delete mode 100644 src/edit_entry_form.py delete mode 100644 src/edit_group_form.py delete mode 100644 src/entry.py delete mode 100644 src/globals.py delete mode 100644 src/group.py delete mode 100644 src/main.py delete mode 100644 src/main.ui delete mode 100644 src/preferences_dialog.py delete mode 100644 src/preferences_dialog.ui delete mode 100644 src/rule.py delete mode 100644 src/rules_dialog.py diff --git a/MANIFEST.in b/MANIFEST.in index fc1059f..31c06d2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +recursive-include assignment_list_pyqt *ui include data/assignment-list.desktop include data/assignment-list.ico include data/assignment-list.svg diff --git a/assignment-list b/assignment-list index cb0af8a..957b0d4 100755 --- a/assignment-list +++ b/assignment-list @@ -3,7 +3,7 @@ from __future__ import absolute_import import sys from PyQt5.QtWidgets import QApplication -from src.main import AssignmentList as main +from assignment_list_pyqt.main import AssignmentList as main app = QApplication(sys.argv) window = main() diff --git a/assignment_list_pyqt/__init__.py b/assignment_list_pyqt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/assignment_list_pyqt/add_entry_form.py b/assignment_list_pyqt/add_entry_form.py new file mode 100644 index 0000000..59ed08a --- /dev/null +++ b/assignment_list_pyqt/add_entry_form.py @@ -0,0 +1,51 @@ +import os +import sys +from PyQt5 import uic +from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt5.QtCore import QDate +from assignment_list_pyqt.entry import Entry +import assignment_list_pyqt.globals as Globals +import assignment_list_pyqt.db_sqlite as DB + +class addEntryForm(QDialog): + def __init__(self, parent): + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "add_entry_form.ui"), self) + self.initializeUI(parent) + + def initializeUI(self, parent): + self.displayWidgets(parent) + self.exec() + + def displayWidgets(self, parent): + self.new_entry_due.setDate(QDate.currentDate()) + self.buttonBox.rejected.connect(self.close) + self.buttonBox.accepted.connect(lambda: self.handleSubmit(parent)) + + def handleSubmit(self, parent): + # Check that the new entry is not blank + desc_text = self.new_entry_desc.text() + due_text = "" + if self.new_entry_due_checkbox.isChecked(): + due_text = self.new_entry_due.date() # due_text is a QDate + due_alt_text = self.new_entry_due_alt.text() + link_text = self.new_entry_link.text() + color_text = self.new_entry_color.text() + highlight_text = self.new_entry_highlight.text() + + if not desc_text: + QMessageBox.warning(self, "Error Message", + "Description cannot be blank", + QMessageBox.Close, + QMessageBox.Close) + return + + new_id = DB.insertEntry(Entry(0, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text)) + Globals.entries.append(Entry(new_id, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text)) + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = addEntryForm() + sys.exit(app.exec_()) diff --git a/assignment_list_pyqt/add_entry_form.ui b/assignment_list_pyqt/add_entry_form.ui new file mode 100644 index 0000000..3f8f9e7 --- /dev/null +++ b/assignment_list_pyqt/add_entry_form.ui @@ -0,0 +1,185 @@ + + + Dialog + + + + 0 + 0 + 400 + 266 + + + + Add Entry + + + + + + + + Description: + + + + + + + + + + + Arial + 18 + + + + Add Entry + + + Qt::AlignCenter + + + + + + + Due Date: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + MM/dd/yyyy + + + + + + + true + + + + + + true + + + + + + + + + + Due Date (Alt): + + + + + + + + + + Link: + + + + + + + + + + Color: + + + + + + + + + + Highlight: + + + + + + + + + + + + Qt::RightToLeft + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/assignment_list_pyqt/add_group_form.py b/assignment_list_pyqt/add_group_form.py new file mode 100644 index 0000000..b6cb804 --- /dev/null +++ b/assignment_list_pyqt/add_group_form.py @@ -0,0 +1,47 @@ +import os +import sys +from PyQt5 import uic +from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox + +import assignment_list_pyqt.globals as Globals +from assignment_list_pyqt.group import Group +import assignment_list_pyqt.db_sqlite as DB + +class addGroupForm(QDialog): + """ + Implemented so that it can be used for adding and editing groups + """ + def __init__(self): + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "add_group_form.ui"), self) + self.initializeUI() + + def initializeUI(self): + self.displayWidgets() + self.exec() + + def displayWidgets(self): + self.buttonBox.rejected.connect(self.close) + self.buttonBox.accepted.connect(self.handleSubmit) + + def handleSubmit(self): + name_text = self.new_group_name.text() + column_text = self.new_group_column.currentText() + link_text = self.new_group_link.text() + + if not name_text: + QMessageBox.warning(self, "Error Message", + "Name cannot be blank", + QMessageBox.Close, + QMessageBox.Close) + return + + new_id = DB.insertGroup(Group(0, name_text, column_text, link_text)) + Globals.groups.append(Group(new_id, name_text, column_text, link_text)) + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = addGroupForm() + sys.exit(app.exec_()) diff --git a/assignment_list_pyqt/add_group_form.ui b/assignment_list_pyqt/add_group_form.ui new file mode 100644 index 0000000..c3c5c80 --- /dev/null +++ b/assignment_list_pyqt/add_group_form.ui @@ -0,0 +1,131 @@ + + + Dialog + + + + 0 + 0 + 400 + 172 + + + + Add Entry + + + + + + + + + Arial + 18 + + + + Add Group + + + Qt::AlignCenter + + + + + + + Name: + + + + + + + + + + Column: + + + + + + + + Left + + + + + Right + + + + + + + + Link: + + + + + + + + + + + + Qt::RightToLeft + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/assignment_list_pyqt/config.py b/assignment_list_pyqt/config.py new file mode 100644 index 0000000..daf26e5 --- /dev/null +++ b/assignment_list_pyqt/config.py @@ -0,0 +1,95 @@ +""" +Handle reading of the config file, which on POSIX-compliant systems will be +created in ~/.config/assignment-list-pyqt5/config.py +""" + +import configparser +import os +import sys + +import assignment_list_pyqt.globals as Globals + +class Config(): + def __init__(self): + self.config_path = self.getConfigPath() + + if not os.path.exists(self.config_path): + self.createConfig() + + self.loadConfig() + + def getConfigPath(self): + # Windows config path is "$LOCALAPPDATA/assignment-list-pyqt5/config" + if sys.platform.startswith("win32"): + return os.path.join(os.path.expandvars("$LOCALAPPDATA"), + "assignment-list-pyqt5", + "config") + # Unix config path is "$HOME/.config/assignment-list-pyqt5/config" + else: + return os.path.join( + os.path.expanduser("~"), + ".config", + "assignment-list-pyqt5", + "config") + + def loadConfig(self): + self.config = configparser.ConfigParser() + + try: + self.config.read(self.config_path) + except: + print("Could not parse config file '{}'".format(self.config_path)) + + if "paths" in self.config: + if self.config["paths"]["db_path"]: + Globals.db_path = self.config["paths"]["db_path"] + + def createConfig(self): + self.config = configparser.ConfigParser() + if sys.platform.startswith("win32"): + self.config["paths"] = { + # Windows default DB path is "$APPDATA/assignment-list-pyqt5/data.db" + "db_path": os.path.join( + os.path.expandvars("$APPDATA"), + "assignment-list-pyqt5", + "data.db" + ) + } + else: + self.config["paths"] = { + # Unix default DB path is "$HOME/.local/share/assignment-list-pyqt5/data.db" + "db_path": os.path.join( + os.path.expanduser("~"), + ".local", + "share", + "assignment-list-pyqt5", + "data.db" + ) + } + + self.updateConfig() + + def updateConfig(self): + """ + Update the configuration file with values from self.config + """ + # Attempt to create directory if necessary + if not os.path.exists(os.path.dirname(self.config_path)): + try: + os.mkdir(os.path.dirname(self.config_path)) + except: + print("Error: Could not create config directory '{}'".format(os.path.dirname(self.config_path))) + sys.exit(1) + + # Attempt to write to file + try: + with open(self.config_path, 'w') as configfile: + self.config.write(configfile) + except: + print("Error: Could not open config file '{}'".format(self.config_path)) + sys.exit(1) + + print("Successfully created config at {}".format(self.config_path)) + +if __name__ == "__main__": + Config() diff --git a/assignment_list_pyqt/db_sqlite.py b/assignment_list_pyqt/db_sqlite.py new file mode 100644 index 0000000..b7bed41 --- /dev/null +++ b/assignment_list_pyqt/db_sqlite.py @@ -0,0 +1,493 @@ +import os +import sys +from time import strptime +from PyQt5.QtCore import QDate +from PyQt5.QtSql import QSqlDatabase, QSqlQuery +import assignment_list_pyqt.globals as Globals +from assignment_list_pyqt.group import Group +from assignment_list_pyqt.entry import Entry +from assignment_list_pyqt.rule import Rule + +def initDB(): + """ + Check for existing database. If it doesn't exist, build it + """ + if not os.path.exists(Globals.db_path) or not os.stat(Globals.db_path).st_size: + createTables() + + loadFromTables() + +def createTables(): + """ + Create database at a specified Globals.db_path + """ + print(Globals.db_path) + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + # Create database parent directory if necessary + if not os.path.exists(os.path.dirname(Globals.db_path)): + try: + os.mkdir(os.path.dirname(Globals.db_path)) + except: + print("Unable to open data source file.") + sys.exit(1) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + # Erase database contents so that we don't have duplicates + query.exec_("DROP TABLE groups") + query.exec_("DROP TABLE entries") + query.exec_("DROP TABLE rules") + + query.exec_(""" + CREATE TABLE groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + column TINYINT(1) DEFAULT FALSE, + link VARCHAR(255) NOT NULL, + hidden TINYINT(1) DEFAULT FALSE + ) + """) + + query.exec_(""" + CREATE TABLE entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + parent_id REFERENCES groups (id), + description VARCHAR(255) NOT NULL, + due_date TEXT DEFAULT NULL, + alt_due_date VARCHAR(255) DEFAULT NULL, + link VARCHAR(255) DEFAULT NULL, + color VARCHAR(255) DEFAULT NULL, + highlight VARCHAR(255) DEFAULT NULL, + done TINYINT(1) DEFAULT FALSE, + hidden TINYINT(1) DEFAULT FALSE + ) + """) + + query.exec_(""" + CREATE TABLE rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + entry_id REFERENCES entries (id), + before_after TINYINT(1) DEFAULT TRUE, + date TEXT NOT NULL, + color VARCHAR(255) DEFAULT NULL, + highlight VARCHAR(255) DEFAULT NULL + ) + """) + + print(database.lastError().text()) + + database.close() + +def loadFromTables(): + """ + Load groups and entries into global variables + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + # Load groups + Globals.groups = [] # Reset local groups array + query.exec_("SELECT * FROM groups") + while query.next(): + record = query.record() + Globals.groups.append( + Group( + record.field("id").value(), + record.field("name").value(), + record.field("column").value(), + record.field("link").value(), + record.field("hidden").value())) + + # Load entries + Globals.entries = [] # Reset local entries array + query.exec_("SELECT * FROM entries") + while query.next(): + record = query.record() + # create a QDate if the due date is set + if record.field("due_date").value(): + due_date_struct = strptime(record.field("due_date").value(), "%Y-%m-%d") + due_date = QDate(due_date_struct.tm_year, due_date_struct.tm_mon, due_date_struct.tm_mday) + else: + due_date = "" + Globals.entries.append( + Entry( + record.field("id").value(), + record.field("parent_id").value(), + record.field("description").value(), + due_date, + record.field("alt_due_date").value(), + record.field("link").value(), + record.field("color").value(), + record.field("highlight").value(), + record.field("done").value(), + record.field("hidden").value())) + + # Load rules + Globals.rules = [] # Reset local rules array + query.exec_("SELECT * FROM rules") + while query.next(): + record = query.record() + date_struct = strptime(record.field("date").value(), "%Y-%m-%d") + date = QDate(date_struct.tm_year, date_struct.tm_mon, date_struct.tm_mday) + Globals.rules.append( + Rule( + record.field("id").value(), + record.field("entry_id").value(), + "before" if record.field("before_after").value() == 0 else "after", + date, + record.field("color").value(), + record.field("highlight").value())) + + database.close() + +def insertGroup(new_group): + """ + Insert group to the database at Globals.db_path + """ + output = -1 + + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + INSERT INTO groups (name, column, link) VALUES (?, ?, ?) + """) + query.addBindValue(new_group.name) + query.addBindValue(new_group.column) + query.addBindValue(new_group.link) + query.exec_() + + output = query.lastInsertId() + + database.close() + + return output + +def insertEntry(new_entry): + """ + Insert entry to the database at Globals.db_path + """ + output = -1 + + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + INSERT INTO entries (parent_id, description, due_date, alt_due_date, link, color, highlight) VALUES (:p_id, :desc, :due, :alt_due, :link, :color, :highlight) + """) + query.bindValue(":p_id", new_entry.parent_id) + query.bindValue(":desc", new_entry.desc) + if new_entry.due: + query.bindValue(":due", "{0}-{1}-{2}".format( + new_entry.due.year(), + new_entry.due.month(), + new_entry.due.day())) + else: + query.bindValue(":due", "") + query.bindValue(":alt_due", new_entry.due_alt) + query.bindValue(":link", new_entry.link) + query.bindValue(":color", new_entry.color) + query.bindValue(":highlight", new_entry.highlight) + success = query.exec_() + # DEBUG + #print(query.lastError().text()) + #print(query.boundValues()) + #if success: + # print("Query succeeded") + #else: + # print("Query failed") + + output = query.lastInsertId() + + database.close() + + return output + +def insertRule(new_rule): + """ + Insert rule to the database at Globals.db_path + """ + output = -1 + + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + INSERT INTO rules (entry_id, before_after, date, color, highlight) VALUES (:e_id, :when, :date, :color, :highlight) + """) + query.bindValue(":e_id", new_rule.entry_id) + query.bindValue(":when", 0 if new_rule.when.lower() == "before" else 1) + query.bindValue(":date", "{0}-{1}-{2}".format( + new_rule.date.year(), + new_rule.date.month(), + new_rule.date.day())) + query.bindValue(":color", new_rule.color) + query.bindValue(":highlight", new_rule.highlight) + success = query.exec_() + # DEBUG + #print(query.lastError().text()) + #print(query.boundValues()) + #if success: + # print("Query succeeded") + #else: + # print("Query failed") + + output = query.lastInsertId() + + database.close() + + return output + +def updateGroup(group): + """ + Update group by its id + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + UPDATE groups SET name = ?, column = ?, link = ?, hidden = ? WHERE id = ? + """) + query.addBindValue(group.name) + query.addBindValue(group.column) + query.addBindValue(group.link) + query.addBindValue(group.hidden) + query.addBindValue(group.id) + query.exec_() + + database.close() + +def updateEntry(entry): + """ + Update entry by its id + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + UPDATE entries SET + description = :desc, + due_date = :due, + alt_due_date = :alt_due, + link = :link, + color = :color, + highlight = :highlight, + done = :done, + hidden = :hidden + WHERE id = :id + """) + query.bindValue(":desc", entry.desc) + if entry.due: + query.bindValue(":due", "{0}-{1}-{2}".format( + entry.due.year(), + entry.due.month(), + entry.due.day())) + else: + query.bindValue(":due", "") + query.bindValue(":alt_due", entry.due_alt) + query.bindValue(":link", entry.link) + query.bindValue(":color", entry.color) + query.bindValue(":highlight", entry.highlight) + query.bindValue(":done", entry.done) + query.bindValue(":hidden", entry.hidden) + query.bindValue(":id", entry.id) + query.exec_() + + database.close() + +def updateRule(rule): + """ + Update rule by its id + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + query.prepare(""" + UPDATE rules SET + before_after = :when, + date = :date, + color = :color, + highlight = :highlight + WHERE id = :id + """) + query.bindValue(":when", 0 if rule.when.lower() == "before" else 1) + query.bindValue(":date", "{0}-{1}-{2}".format( + rule.date.year(), + rule.date.month(), + rule.date.day())) + query.bindValue(":color", rule.color) + query.bindValue(":highlight", rule.highlight) + query.bindValue(":id", rule.id) + success = query.exec_() + # DEBUG + #print(query.lastError().text()) + #print(query.boundValues()) + #if success: + # print("Query succeeded") + #else: + # print("Query failed") + + database.close() + +def removeGroup(group_id): + """ + Remove a group by id from the database + (actually set hidden to true, don't permanently delete it) + """ + + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + # First, set entries to hidden + query.prepare(""" + UPDATE entries SET hidden = 1 WHERE parent_id = ? + """) + query.addBindValue(group_id) + query.exec_() + + # Now, set the group to hidden + query.prepare(""" + UPDATE groups SET hidden = 1 WHERE id = ? + """) + query.addBindValue(group_id) + query.exec_() + + output = query.numRowsAffected() + database.close() + return output + +def removeEntry(entry_id): + """ + Remove a group by id from the database + (actually set hidden to true, don't permanently delete it) + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + # Set entry to hidden + query.prepare(""" + UPDATE entries SET hidden = 1 WHERE id = ? + """) + query.addBindValue(entry_id) + query.exec_() + + output = query.numRowsAffected() + database.close() + return output + +def removeRule(rule_id): + """ + Remove a rule by id from the database + (we do not preserve rules, unlike groups and entries) + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + # Set entry to hidden + query.prepare(""" + DELETE FROM rules WHERE id = ? + """) + query.addBindValue(rule_id) + query.exec_() + + output = query.numRowsAffected() + database.close() + return output + +def cleanHidden(): + """ + Permanently delete removed/hidden groups and entries + """ + database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 + database.setDatabaseName(Globals.db_path) + + if not database.open(): + print("Unable to open data source file.") + sys.exit(1) # Error out. TODO consider throwing a dialog instead + + query = QSqlQuery() + + # Remove rules associated with hidden entries + query.exec_(""" + DELETE FROM rules WHERE id IN ( + SELECT rules.id FROM rules + INNER JOIN entries ON rules.entry_id = entries.id + WHERE entries.hidden = 1 + )""") + + # Remove hidden entries + query.exec_(""" + DELETE FROM entries WHERE hidden = 1 + """) + + # Remove hidden groups + query.exec_(""" + DELETE FROM groups WHERE hidden = 1 + """) + + database.close() diff --git a/assignment_list_pyqt/edit_entry_form.py b/assignment_list_pyqt/edit_entry_form.py new file mode 100644 index 0000000..20a8e23 --- /dev/null +++ b/assignment_list_pyqt/edit_entry_form.py @@ -0,0 +1,83 @@ +import os +import sys +from PyQt5 import uic +from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt5.QtCore import QDate + +import assignment_list_pyqt.globals as Globals +from assignment_list_pyqt.entry import Entry +import assignment_list_pyqt.db_sqlite as DB + +# Reuses the add_entry_form UI file +class editEntryForm(QDialog): + """ + Form to edit/update an entry + """ + def __init__(self, id): + self.id = id + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "add_entry_form.ui"), self) + self.initializeUI() + + def initializeUI(self): + self.setWindowTitle("Edit Entry") + self.displayWidgets() + self.exec() + + def displayWidgets(self): + entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0] + + self.title.setText("Edit Entry") + self.new_entry_desc.setText(entry.desc) + self.new_entry_due.setDate(QDate.currentDate()) + if entry.due: + self.new_entry_due.setDate(entry.due) + self.new_entry_due_checkbox.setChecked(True) + else: + self.new_entry_due_checkbox.setChecked(False) + self.new_entry_due_alt.setText(entry.due_alt) + self.new_entry_link.setText(entry.link) + self.new_entry_color.setText(entry.color) + self.new_entry_highlight.setText(entry.highlight) + self.buttonBox.rejected.connect(self.close) + self.buttonBox.accepted.connect(self.handleSubmit) + + def handleSubmit(self): + desc_text = self.new_entry_desc.text() + if self.new_entry_due_checkbox.isChecked(): + due_text = self.new_entry_due.date() # due_text is a QDate + else: + due_text = "" # due is unchecked + due_alt_text = self.new_entry_due_alt.text() + link_text = self.new_entry_link.text() + color_text = self.new_entry_color.text() + highlight_text = self.new_entry_highlight.text() + + if not desc_text: + QMessageBox.warning(self, "Error Message", + "Name cannot be blank", + QMessageBox.Close, + QMessageBox.Close) + return + + # Update DB + entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0] + entry.desc = desc_text + entry.due = due_text + entry.due_alt = due_alt_text + entry.link = link_text + entry.color = color_text + entry.highlight = highlight_text + DB.updateEntry(entry) + + # Update global variables + Globals.entries = list(filter(lambda e: e.id != self.id, Globals.entries)) + Globals.entries.append(Entry(self.id, entry.parent_id, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text, entry.done, entry.hidden)) + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = editEntryForm() + sys.exit(app.exec_()) + diff --git a/assignment_list_pyqt/edit_group_form.py b/assignment_list_pyqt/edit_group_form.py new file mode 100644 index 0000000..5a89035 --- /dev/null +++ b/assignment_list_pyqt/edit_group_form.py @@ -0,0 +1,63 @@ +import os +import sys +from PyQt5 import uic +from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox + +import assignment_list_pyqt.globals as Globals +from assignment_list_pyqt.group import Group +import assignment_list_pyqt.db_sqlite as DB + +class editGroupForm(QDialog): + """ + Form to edit/update a group + """ + def __init__(self, id): + self.id = id + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "add_group_form.ui"), self) + self.initializeUI() + + def initializeUI(self): + self.setWindowTitle("Edit Group") + self.displayWidgets() + self.exec() + + def displayWidgets(self): + group = list(filter(lambda g: g.id == self.id, Globals.groups))[0] + + self.title.setText("Edit Group") + self.new_group_name.setText(group.name) + self.new_group_column.setCurrentIndex(0 if group.column.lower() == "left" else 1) + self.new_group_link.setText(group.link) + self.buttonBox.rejected.connect(self.close) + self.buttonBox.accepted.connect(self.handleSubmit) + + def handleSubmit(self): + name_text = self.new_group_name.text() + column_text = self.new_group_column.currentText() + link_text = self.new_group_link.text() + + if not name_text: + QMessageBox.warning(self, "Error Message", + "Name cannot be blank", + QMessageBox.Close, + QMessageBox.Close) + return + + # Update DB + group = list(filter(lambda g: g.id == self.id, Globals.groups))[0] + group.name = name_text + group.column = column_text + group.link = link_text + DB.updateGroup(group) + + # Update global variables + Globals.groups = list(filter(lambda g: g.id != self.id, Globals.groups)) + Globals.groups.append(Group(self.id, name_text, column_text, link_text, group.hidden)) + self.close() + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = editGroupForm() + sys.exit(app.exec_()) diff --git a/assignment_list_pyqt/entry.py b/assignment_list_pyqt/entry.py new file mode 100644 index 0000000..48df534 --- /dev/null +++ b/assignment_list_pyqt/entry.py @@ -0,0 +1,100 @@ +from datetime import date +from PyQt5.QtCore import QDate, Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QHBoxLayout, QLabel +import assignment_list_pyqt.globals as Globals + +class Entry: + def __init__(self, id, parent_id, desc, due = "", due_alt = "", link = "", color = "", highlight = "", done = False, hidden = False): + self.id = id + self.parent_id = parent_id + self.desc = desc + self.due = due + self.due_alt = due_alt + self.link = link + self.color = color + self.highlight = highlight + self.done = done + self.hidden = hidden + + def buildLayout(self): + output = QHBoxLayout() + bullet = QLabel() + body = QLabel() + + output.setContentsMargins(2,2,2,2) + + bullet.setFont(QFont("Arial", 11)) + bullet.setMaximumWidth(15) + + body.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) + body.setFont(QFont("Arial", 11)) + body.setWordWrap(True) + body.setToolTip("Right-Click for actions") + + # Check rules + relevant_rules = list(filter(lambda r: r.entry_id == self.id, Globals.rules)) + for r in relevant_rules: + if (r.when.lower() == "before" and r.date > QDate.currentDate()) or (r.when.lower() == "after" and r.date <= QDate.currentDate()): + if r.color: + self.color = r.color + if r.highlight: + self.highlight = r.highlight + + if self.done: + bullet.setText("\u2713 ") + bullet.setStyleSheet(""" + QLabel{ + color: green; + } + """) + self.color = "green" + else: + bullet.setText("- ") + output.addWidget(bullet) + + if self.due: + body.setText("{0}/{1}/{2}: ".format( + self.due.month(), + self.due.day(), + self.due.year() + )) + elif self.due_alt: + body.setText("{0}: ".format( + self.due_alt + )) + if self.link: + body.setOpenExternalLinks(True) + body.setText(body.text() + "".format( + self.link, + self.color if self.color else "default" + )) + body.setText(body.text() + self.desc) + if self.link: + body.setText(body.text() + "") + body.setToolTip("{}".format(self.link)) + output.addWidget(body) + + if self.done: + font = body.font() + font.setStrikeOut(True) + body.setFont(font) + body.setStyleSheet(""" + QLabel{ + color: green; + } + """) + + else: + body.setStyleSheet(""" + QLabel{{ + color: {0}; + background-color: {1}; + font-weight: {2}; + }}""".format( + self.color if self.color else "default", + self.highlight if self.highlight else "none", + "bold" if self.due and self.due <= date.today() else "normal" + )) + + return output diff --git a/assignment_list_pyqt/globals.py b/assignment_list_pyqt/globals.py new file mode 100644 index 0000000..bb6f717 --- /dev/null +++ b/assignment_list_pyqt/globals.py @@ -0,0 +1,4 @@ +groups = [] +entries = [] +rules = [] +db_path = "" diff --git a/assignment_list_pyqt/group.py b/assignment_list_pyqt/group.py new file mode 100644 index 0000000..01f5427 --- /dev/null +++ b/assignment_list_pyqt/group.py @@ -0,0 +1,25 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QLabel, QVBoxLayout +import assignment_list_pyqt.globals as Globals + +class Group: + def __init__(self, id, name, column = "left", link = "", hidden = False): + self.id = id + self.name = name + self.column = column + self.link = link + self.hidden = hidden + + def buildLayout(self): + output = QVBoxLayout() + output.setContentsMargins(0, 10, 0, 10) + + name = QLabel(self.name) + name.setTextInteractionFlags(Qt.TextSelectableByMouse) + name_font = QFont("Arial", 13) + name_font.setUnderline(True) + name.setFont(name_font) + output.addWidget(name) + + return output diff --git a/assignment_list_pyqt/main.py b/assignment_list_pyqt/main.py new file mode 100644 index 0000000..3b19fd6 --- /dev/null +++ b/assignment_list_pyqt/main.py @@ -0,0 +1,273 @@ +#!/usr/bin/python3 +import os +import sys +import time +from PyQt5 import uic +from PyQt5.QtWidgets import QAction, QApplication, QMainWindow, QMenu, QMessageBox, QVBoxLayout +from PyQt5.QtGui import QCursor +from PyQt5.QtCore import QDate, Qt +from assignment_list_pyqt.config import Config +from assignment_list_pyqt.preferences_dialog import PreferencesDialog +from assignment_list_pyqt.add_group_form import addGroupForm +from assignment_list_pyqt.edit_group_form import editGroupForm +from assignment_list_pyqt.add_entry_form import addEntryForm +from assignment_list_pyqt.edit_entry_form import editEntryForm +import assignment_list_pyqt.globals as Globals +import assignment_list_pyqt.db_sqlite as DB +from assignment_list_pyqt.rules_dialog import RulesDialog + +class AssignmentList(QMainWindow): + def __init__(self): + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "main.ui"), self) + + self.initializeUI() + + def initializeUI(self): + self.createMenu() + self.createToolbar() + Config() + self.setupDB() + self.displayWidgets() + self.show() + + def createMenu(self): + self.actionPreferences.triggered.connect(self.preferences) + self.actionReload.triggered.connect(self.reload) + self.actionExit.triggered.connect(self.close) + + self.actionAdd_Group.triggered.connect(self.addGroup) + self.actionClean_Hidden.triggered.connect(self.cleanHidden) + + self.actionAbout.triggered.connect(self.aboutDialog) + + def createToolbar(self): + self.toolBar.addAction(self.actionAdd_Group) + + def setupDB(self): + DB.initDB() + + def displayWidgets(self): + self.title.setText(time.strftime("%A, %b %d %Y")) + self.drawGroups() + + def addGroup(self): + """ + Open the 'addGroup' form + """ + old_count = len(Globals.groups) + self.create_new_group_dialog = addGroupForm() + if old_count != len(Globals.groups): + self.drawGroups() + + def editGroup(self, id): + """ + Open the 'editGroup' form + """ + self.create_edit_group_dialog = editGroupForm(id) + self.drawGroups() + + def removeGroup(self, id): + """ + Delete a group with a given id + """ + # TODO might want to add a warning + # TODO might want to make part of the a destructor in the Group class + removed = DB.removeGroup(id) + if removed > 0: + Globals.entries = list(filter(lambda e: e.parent_id != id, Globals.entries)) + Globals.groups = list(filter(lambda g: g.id != id, Globals.groups)) + self.drawGroups() + + def groupContextMenu(self, group_id): + menu = QMenu() + + add_entry_act = QAction("Add Entry") + add_entry_act.triggered.connect((lambda id: lambda: self.addEntry(id))(group_id)) + menu.addAction(add_entry_act) + + edit_group_act = QAction("Edit Group") + edit_group_act.triggered.connect((lambda id: lambda: self.editGroup(id))(group_id)) + menu.addAction(edit_group_act) + + del_group_act = QAction("Remove Group") + del_group_act.triggered.connect((lambda id: lambda: self.removeGroup(id))(group_id)) + menu.addAction(del_group_act) + + menu.exec_(QCursor.pos()) + + def addEntry(self, parent): + """ + Open the 'addEntry' form + """ + old_count = len(Globals.entries) + self.create_new_entry_dialog = addEntryForm(parent) + if old_count != len(Globals.entries): + self.drawGroups() # TODO see if we can do this with only redrawing a single group + + def editEntry(self, id): + """ + Open the 'editEntry' form + """ + self.create_edit_entry_dialog = editEntryForm(id) + self.drawGroups() + + def toggleDoneEntry(self, id): + """ + Toggle the 'done' flag on the entry with the given id + """ + entry = list(filter(lambda e: e.id == id, Globals.entries))[0] + if entry.done: + entry.done = False + else: + entry.done = True + DB.updateEntry(entry) + Globals.entries = list(filter(lambda e: e.id != id, Globals.entries)) + Globals.entries.append(entry) + self.drawGroups() + + def removeEntry(self, id): + """ + Delete an entry with a given id + """ + # TODO might want to add a warning + # TODO might want to make part of the a destructor in the Entry class + removed = DB.removeEntry(id) + if removed > 0: + Globals.entries = list(filter(lambda e: e.id != id, Globals.entries)) + self.drawGroups() + + def editRules(self, id): + pass + need_reload = RulesDialog(id) + if need_reload: + self.reload() + + def entryContextMenu(self, entry_id): + menu = QMenu() + + edit_entry_act = QAction("Edit Entry") + edit_entry_act.triggered.connect((lambda id: lambda: self.editEntry(id))(entry_id)) + menu.addAction(edit_entry_act) + + rules_act = QAction("Rules") + rules_act.triggered.connect((lambda id: lambda: self.editRules(id))(entry_id)) + menu.addAction(rules_act) + + mark_done_act = QAction("Done", checkable=True) + if list(filter(lambda e: e.id == entry_id, Globals.entries))[0].done: + mark_done_act.setChecked(True) + mark_done_act.triggered.connect((lambda id: lambda: self.toggleDoneEntry(id))(entry_id)) + menu.addAction(mark_done_act) + + del_entry_act = QAction("Remove Entry") + del_entry_act.triggered.connect((lambda id: lambda: self.removeEntry(id))(entry_id)) + menu.addAction(del_entry_act) + + menu.exec_(QCursor.pos()) + + def preferences(self): + # TODO not sure if this is working exactly how I think it does, but it works + need_reload = PreferencesDialog() + if need_reload: + self.reload() + + def cleanHidden(self): + """ + Permanently delete removed groups and entries from db + """ + # TODO consider creating a warning dialogue for this + DB.cleanHidden() + + def drawGroups(self): + """ + Redraw the groups_layout + """ + # Remove all children from layout + def recursiveClear(layout): + while layout.count(): + child = layout.takeAt(0) + if child.widget(): + child.widget().deleteLater() + elif child.layout(): + recursiveClear(child) + + recursiveClear(self.groups_layout) + + # Sort the groups + Globals.groups = sorted(Globals.groups, key=lambda g: g.id) + + # Sort the entries + Globals.entries = sorted(Globals.entries, key=lambda e: (e.parent_id, (e.due if e.due else QDate.currentDate()), e.done, e.id)) + + # Sort the rules + Globals.rules = sorted(Globals.rules, key=lambda r: (r.id)) + + # Create columns as vertical boxes + column_left = QVBoxLayout() + column_right = QVBoxLayout() + + for g in Globals.groups: + # skip if this group is set to hidden + if g.hidden: + continue + + g_layout = g.buildLayout() + + # Create custom context menu + g_layout.itemAt(0).widget().setToolTip("Right-Click for actions") + g_layout.itemAt(0).widget().setContextMenuPolicy(Qt.CustomContextMenu) + g_layout.itemAt(0).widget().customContextMenuRequested.connect((lambda id: lambda: self.groupContextMenu(id))(g.id)) + + # Draw entries belonging to this group + g_layout.addLayout(self.drawEntries(g.id)) + + if g.column.lower() == "left": + column_left.addLayout(g_layout) + else: + column_right.addLayout(g_layout) + + column_left.addStretch() + column_right.addStretch() + + self.groups_layout.addLayout(column_left, 0, 0) + self.groups_layout.addLayout(column_right, 0, 1) + + def drawEntries(self, group_id): + """ + Redraw the entries of a specific group + """ + # TODO consider having code to remove existing widgets to make this function more modular + entries = list(filter(lambda e: e.parent_id == group_id, Globals.entries)) + entries_vbox = QVBoxLayout() + entries_vbox.setContentsMargins(5, 0, 0, 0) + + for e in entries: + # skip if this entry is set to hidden + if e.hidden: + continue + + e_layout = e.buildLayout() + entries_vbox.addLayout(e_layout) + + # Create custom context menu + e_layout.itemAt(1).widget().setContextMenuPolicy(Qt.CustomContextMenu) + e_layout.itemAt(1).widget().customContextMenuRequested.connect((lambda id: lambda: self.entryContextMenu(id))(e.id)) + + return entries_vbox + + def reload(self): + Config() + self.setupDB() + self.drawGroups() + + def aboutDialog(self): + QMessageBox.about(self, "About Assignment List", + "Created by Louie S. - 2023") + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = AssignmentList() + sys.exit(app.exec_()) + diff --git a/assignment_list_pyqt/main.ui b/assignment_list_pyqt/main.ui new file mode 100644 index 0000000..bc3c0fb --- /dev/null +++ b/assignment_list_pyqt/main.ui @@ -0,0 +1,207 @@ + + + MainWindow + + + + 0 + 0 + 640 + 480 + + + + Assignment List + + + + + + + true + + + + + 0 + 0 + 620 + 421 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Arial + 17 + + + + [DATE] + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 20 + + + 5 + + + 20 + + + 5 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + 0 + 640 + 22 + + + + + File + + + + + + + + + Edit + + + + + + + + Help + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + Preferences + + + Alt+Return + + + + + Reload + + + F5 + + + + + Exit + + + Ctrl+Q + + + + + Add Group + + + + + Permanently Delete Removed Groups and Entries + + + + + About + + + + + + diff --git a/assignment_list_pyqt/preferences_dialog.py b/assignment_list_pyqt/preferences_dialog.py new file mode 100644 index 0000000..276e59a --- /dev/null +++ b/assignment_list_pyqt/preferences_dialog.py @@ -0,0 +1,71 @@ +import os +import sys +from PyQt5 import uic +from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog +from assignment_list_pyqt.config import Config + +class PreferencesDialog(QDialog): + """ + Implemented to set configuration options in the program + """ + def __init__(self): + super().__init__() + uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "preferences_dialog.ui"), self) + + # class globals + self.config = Config() + + self.initializeUI() + + def initializeUI(self): + self.displayWidgets() + self.exec() + + def displayWidgets(self): + # TODO make this a scrollable window + # FIXME could use some work on sizing + self.pathsTabLayout() + + self.close_button.clicked.connect(self.close) + self.apply_button.clicked.connect(self.apply) + self.reload_button.clicked.connect(self.reload) + + def pathsTabLayout(self): + if "paths" in self.config.config: + self.db_path_edit.setText(self.config.config["paths"]["db_path"]) + self.db_path_button.clicked.connect(self.dbPathDialog) + + def dbPathDialog(self): + file_dialog = QFileDialog(self) + # TODO create filter to only allow selecting .db files + new_path = file_dialog.getOpenFileName(self, "Open File") + + if new_path[0]: + self.db_path_edit.setText(new_path[0]) + + def apply(self): + """ + Update the configuration in the config file + """ + # Save paths + if "paths" in self.config.config: + try: + with open(self.db_path_edit.text(), 'a'): + self.config.config["paths"]["db_path"] = self.db_path_edit.text() + except: + print("Warning: db_path '{}' does not exist; skipping".format(self.db_path_edit.text())) + + self.config.updateConfig() + + def reload(self): + """ + Update, reload, and close the window + """ + self.apply() + self.done(1) + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = PreferencesDialog() + sys.exit(app.exec_()) diff --git a/assignment_list_pyqt/preferences_dialog.ui b/assignment_list_pyqt/preferences_dialog.ui new file mode 100644 index 0000000..244eb26 --- /dev/null +++ b/assignment_list_pyqt/preferences_dialog.ui @@ -0,0 +1,128 @@ + + + Dialog + + + + 0 + 0 + 500 + 320 + + + + Preferences + + + + + + + + 0 + + + + Paths + + + + + + + + + + + Database File: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 25 + 16777215 + + + + ... + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + Apply + + + + + + + Reload + + + + + + + + + + + + diff --git a/assignment_list_pyqt/rule.py b/assignment_list_pyqt/rule.py new file mode 100644 index 0000000..414ccf0 --- /dev/null +++ b/assignment_list_pyqt/rule.py @@ -0,0 +1,44 @@ + +from PyQt5.QtCore import QDate +from PyQt5.QtWidgets import QComboBox, QDateTimeEdit, QHBoxLayout, QLineEdit + + +class Rule: + def __init__(self, id, entry_id, when, date, color = "", highlight = ""): + self.id = id + self.entry_id = entry_id + self.when = when + self.date = date + self.color = color + self.highlight = highlight + + def buildLayout(self): + output = QHBoxLayout() + + when_widget = QComboBox() + when_widget.addItems(["Before", "After"]) + when_widget.setCurrentIndex(0 if self.when.lower() == "before" else 1) + output.addWidget(when_widget) + + date_widget = QDateTimeEdit(QDate.currentDate()) + date_widget.setDisplayFormat("MM/dd/yyyy") + date_widget.setDate(self.date) + output.addWidget(date_widget) + + output.addStretch() + + # TODO Consider making this a color selector widget + color_widget = QLineEdit() + color_widget.setPlaceholderText("Color") + if self.color: + color_widget.setText(self.color) + output.addWidget(color_widget) + + # TODO Consider making this a color selector widget + highlight_widget = QLineEdit() + highlight_widget.setPlaceholderText("Highlight") + if self.highlight: + highlight_widget.setText(self.highlight) + output.addWidget(highlight_widget) + + return output diff --git a/assignment_list_pyqt/rules_dialog.py b/assignment_list_pyqt/rules_dialog.py new file mode 100644 index 0000000..971b95a --- /dev/null +++ b/assignment_list_pyqt/rules_dialog.py @@ -0,0 +1,128 @@ +import sys +from PyQt5.QtCore import QDate +from PyQt5.QtWidgets import QApplication, QDialog, QHBoxLayout, QPushButton, QScrollArea, QVBoxLayout +from assignment_list_pyqt.config import Config +from assignment_list_pyqt.rule import Rule +import assignment_list_pyqt.db_sqlite as DB +import assignment_list_pyqt.globals as Globals + +class RulesDialog(QDialog): + """ + Show the list of rules associated with an entry + """ + def __init__(self, entry_id): + super().__init__() + + self.entry_id = entry_id + # class globals + self.config = Config() + self.relevant_rules = self.getRelevantRules() + + self.initializeUI() + + def initializeUI(self): + self.resize(500, 320) + self.setWindowTitle("Rules") + self.displayWidgets() + self.exec() + + def displayWidgets(self): + main_layout = QVBoxLayout() + main_layout_scroll_area = QScrollArea() + main_layout_scroll_area.setWidgetResizable(True) + main_layout_scroll_area.setLayout(main_layout) + + self.rules_layout = QVBoxLayout() + self.drawRules() + main_layout.addLayout(self.rules_layout) + + main_layout.addStretch() + # Create Close and Save buttons + buttons_hbox = QHBoxLayout() + buttons_hbox.addStretch() + + close_button = QPushButton("Close", self) + close_button.clicked.connect(self.close) + buttons_hbox.addWidget(close_button) + + save_button = QPushButton("Save", self) + save_button.clicked.connect(self.save) + buttons_hbox.addWidget(save_button) + + main_layout.addLayout(buttons_hbox) + self.setLayout(main_layout) + + def drawRules(self): + # Remove all children from layout + def recursiveClear(layout): + while layout.count(): + child = layout.takeAt(0) + if child.widget(): + child.widget().deleteLater() + elif child.layout(): + recursiveClear(child) + + recursiveClear(self.rules_layout) + + # Draw each rule + self.r_layouts_dict = {} # Use to help update things in the save() function + for r in self.relevant_rules: + r_layout = r.buildLayout() + self.r_layouts_dict[r.id] = r_layout + del_button = QPushButton("Delete", self) + del_button.clicked.connect((lambda id: lambda: self.deleteRule(id))(r.id)) + r_layout.addWidget(del_button) + self.rules_layout.addLayout(r_layout) + + # Draw a button to add rules + rules_buttons_hbox = QHBoxLayout() + add_rule_button = QPushButton("Add Rule", self) + add_rule_button.clicked.connect(self.addRule) + rules_buttons_hbox.addWidget(add_rule_button) + rules_buttons_hbox.addStretch() + self.rules_layout.addLayout(rules_buttons_hbox) + + def addRule(self): + self.apply() + + new_rule = Rule(0, self.entry_id, "before", QDate.currentDate()) + new_rule_id = DB.insertRule(new_rule) + new_rule.id = new_rule_id + Globals.rules.append(new_rule) + self.relevant_rules = self.getRelevantRules() + self.drawRules() + + def deleteRule(self, rule_id): + DB.removeRule(rule_id) + Globals.rules = list(filter(lambda r: r.id != rule_id, Globals.rules)) + self.relevant_rules = self.getRelevantRules() + self.drawRules() + + def getRelevantRules(self): + return list(filter(lambda r: r.entry_id == self.entry_id, Globals.rules)) + + def apply(self): + for id, layout in self.r_layouts_dict.items(): + updated_rule = Rule( + id, + self.entry_id, + layout.itemAt(0).widget().currentText(), + layout.itemAt(1).widget().date(), + layout.itemAt(3).widget().text(), + layout.itemAt(4).widget().text()) + DB.updateRule(updated_rule) + Globals.rules = list(filter(lambda r: r.id != id, Globals.rules)) + Globals.rules.append(updated_rule) + + def save(self): + """ + Save any existing rules. Added rules are automatically saved, + but changing rules is not, hence the need for a manual save + """ + self.apply() + self.done(1) + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = RulesDialog() + sys.exit(app.exec_()) diff --git a/setup.cfg b/setup.cfg index 4984b64..1a39c91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ author = Louie S [options] packages = - src + assignment_list_pyqt scripts = assignment-list include_package_data = True diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/add_entry_form.py b/src/add_entry_form.py deleted file mode 100644 index 9a2d260..0000000 --- a/src/add_entry_form.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import sys -from PyQt5 import uic -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtCore import QDate -from src.entry import Entry -import src.globals as Globals -import src.db_sqlite as DB - -class addEntryForm(QDialog): - def __init__(self, parent): - super().__init__() - uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "add_entry_form.ui"), self) - self.initializeUI(parent) - - def initializeUI(self, parent): - self.displayWidgets(parent) - self.exec() - - def displayWidgets(self, parent): - self.new_entry_due.setDate(QDate.currentDate()) - self.buttonBox.rejected.connect(self.close) - self.buttonBox.accepted.connect(lambda: self.handleSubmit(parent)) - - def handleSubmit(self, parent): - # Check that the new entry is not blank - desc_text = self.new_entry_desc.text() - due_text = "" - if self.new_entry_due_checkbox.isChecked(): - due_text = self.new_entry_due.date() # due_text is a QDate - due_alt_text = self.new_entry_due_alt.text() - link_text = self.new_entry_link.text() - color_text = self.new_entry_color.text() - highlight_text = self.new_entry_highlight.text() - - if not desc_text: - QMessageBox.warning(self, "Error Message", - "Description cannot be blank", - QMessageBox.Close, - QMessageBox.Close) - return - - new_id = DB.insertEntry(Entry(0, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text)) - Globals.entries.append(Entry(new_id, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text)) - self.close() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = addEntryForm() - sys.exit(app.exec_()) diff --git a/src/add_entry_form.ui b/src/add_entry_form.ui deleted file mode 100644 index 3f8f9e7..0000000 --- a/src/add_entry_form.ui +++ /dev/null @@ -1,185 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 266 - - - - Add Entry - - - - - - - - Description: - - - - - - - - - - - Arial - 18 - - - - Add Entry - - - Qt::AlignCenter - - - - - - - Due Date: - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - MM/dd/yyyy - - - - - - - true - - - - - - true - - - - - - - - - - Due Date (Alt): - - - - - - - - - - Link: - - - - - - - - - - Color: - - - - - - - - - - Highlight: - - - - - - - - - - - - Qt::RightToLeft - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - true - - - - - - - - - buttonBox - accepted() - Dialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/src/add_group_form.py b/src/add_group_form.py deleted file mode 100644 index 4a543e4..0000000 --- a/src/add_group_form.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import sys -from PyQt5 import uic -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox - -import src.globals as Globals -from src.group import Group -import src.db_sqlite as DB - -class addGroupForm(QDialog): - """ - Implemented so that it can be used for adding and editing groups - """ - def __init__(self): - super().__init__() - uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "add_group_form.ui"), self) - self.initializeUI() - - def initializeUI(self): - self.displayWidgets() - self.exec() - - def displayWidgets(self): - self.buttonBox.rejected.connect(self.close) - self.buttonBox.accepted.connect(self.handleSubmit) - - def handleSubmit(self): - name_text = self.new_group_name.text() - column_text = self.new_group_column.currentText() - link_text = self.new_group_link.text() - - if not name_text: - QMessageBox.warning(self, "Error Message", - "Name cannot be blank", - QMessageBox.Close, - QMessageBox.Close) - return - - new_id = DB.insertGroup(Group(0, name_text, column_text, link_text)) - Globals.groups.append(Group(new_id, name_text, column_text, link_text)) - self.close() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = addGroupForm() - sys.exit(app.exec_()) diff --git a/src/add_group_form.ui b/src/add_group_form.ui deleted file mode 100644 index c3c5c80..0000000 --- a/src/add_group_form.ui +++ /dev/null @@ -1,131 +0,0 @@ - - - Dialog - - - - 0 - 0 - 400 - 172 - - - - Add Entry - - - - - - - - - Arial - 18 - - - - Add Group - - - Qt::AlignCenter - - - - - - - Name: - - - - - - - - - - Column: - - - - - - - - Left - - - - - Right - - - - - - - - Link: - - - - - - - - - - - - Qt::RightToLeft - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - true - - - - - - - - - buttonBox - accepted() - Dialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 0fa034e..0000000 --- a/src/config.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Handle reading of the config file, which on POSIX-compliant systems will be -created in ~/.config/assignment-list-pyqt5/config.py -""" - -import configparser -import os -import sys - -import src.globals as Globals - -class Config(): - def __init__(self): - self.config_path = self.getConfigPath() - - if not os.path.exists(self.config_path): - self.createConfig() - - self.loadConfig() - - def getConfigPath(self): - # Windows config path is "$LOCALAPPDATA/assignment-list-pyqt5/config" - if sys.platform.startswith("win32"): - return os.path.join(os.path.expandvars("$LOCALAPPDATA"), - "assignment-list-pyqt5", - "config") - # Unix config path is "$HOME/.config/assignment-list-pyqt5/config" - else: - return os.path.join( - os.path.expanduser("~"), - ".config", - "assignment-list-pyqt5", - "config") - - def loadConfig(self): - self.config = configparser.ConfigParser() - - try: - self.config.read(self.config_path) - except: - print("Could not parse config file '{}'".format(self.config_path)) - - if "paths" in self.config: - if self.config["paths"]["db_path"]: - Globals.db_path = self.config["paths"]["db_path"] - - def createConfig(self): - self.config = configparser.ConfigParser() - if sys.platform.startswith("win32"): - self.config["paths"] = { - # Windows default DB path is "$APPDATA/assignment-list-pyqt5/data.db" - "db_path": os.path.join( - os.path.expandvars("$APPDATA"), - "assignment-list-pyqt5", - "data.db" - ) - } - else: - self.config["paths"] = { - # Unix default DB path is "$HOME/.local/share/assignment-list-pyqt5/data.db" - "db_path": os.path.join( - os.path.expanduser("~"), - ".local", - "share", - "assignment-list-pyqt5", - "data.db" - ) - } - - self.updateConfig() - - def updateConfig(self): - """ - Update the configuration file with values from self.config - """ - # Attempt to create directory if necessary - if not os.path.exists(os.path.dirname(self.config_path)): - try: - os.mkdir(os.path.dirname(self.config_path)) - except: - print("Error: Could not create config directory '{}'".format(os.path.dirname(self.config_path))) - sys.exit(1) - - # Attempt to write to file - try: - with open(self.config_path, 'w') as configfile: - self.config.write(configfile) - except: - print("Error: Could not open config file '{}'".format(self.config_path)) - sys.exit(1) - - print("Successfully created config at {}".format(self.config_path)) - -if __name__ == "__main__": - Config() diff --git a/src/db_sqlite.py b/src/db_sqlite.py deleted file mode 100644 index 00f4edb..0000000 --- a/src/db_sqlite.py +++ /dev/null @@ -1,493 +0,0 @@ -import os -import sys -from time import strptime -from PyQt5.QtCore import QDate -from PyQt5.QtSql import QSqlDatabase, QSqlQuery -import src.globals as Globals -from src.group import Group -from src.entry import Entry -from src.rule import Rule - -def initDB(): - """ - Check for existing database. If it doesn't exist, build it - """ - if not os.path.exists(Globals.db_path) or not os.stat(Globals.db_path).st_size: - createTables() - - loadFromTables() - -def createTables(): - """ - Create database at a specified Globals.db_path - """ - print(Globals.db_path) - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - # Create database parent directory if necessary - if not os.path.exists(os.path.dirname(Globals.db_path)): - try: - os.mkdir(os.path.dirname(Globals.db_path)) - except: - print("Unable to open data source file.") - sys.exit(1) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - # Erase database contents so that we don't have duplicates - query.exec_("DROP TABLE groups") - query.exec_("DROP TABLE entries") - query.exec_("DROP TABLE rules") - - query.exec_(""" - CREATE TABLE groups ( - id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - name VARCHAR(255) NOT NULL, - column TINYINT(1) DEFAULT FALSE, - link VARCHAR(255) NOT NULL, - hidden TINYINT(1) DEFAULT FALSE - ) - """) - - query.exec_(""" - CREATE TABLE entries ( - id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - parent_id REFERENCES groups (id), - description VARCHAR(255) NOT NULL, - due_date TEXT DEFAULT NULL, - alt_due_date VARCHAR(255) DEFAULT NULL, - link VARCHAR(255) DEFAULT NULL, - color VARCHAR(255) DEFAULT NULL, - highlight VARCHAR(255) DEFAULT NULL, - done TINYINT(1) DEFAULT FALSE, - hidden TINYINT(1) DEFAULT FALSE - ) - """) - - query.exec_(""" - CREATE TABLE rules ( - id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - entry_id REFERENCES entries (id), - before_after TINYINT(1) DEFAULT TRUE, - date TEXT NOT NULL, - color VARCHAR(255) DEFAULT NULL, - highlight VARCHAR(255) DEFAULT NULL - ) - """) - - print(database.lastError().text()) - - database.close() - -def loadFromTables(): - """ - Load groups and entries into global variables - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - # Load groups - Globals.groups = [] # Reset local groups array - query.exec_("SELECT * FROM groups") - while query.next(): - record = query.record() - Globals.groups.append( - Group( - record.field("id").value(), - record.field("name").value(), - record.field("column").value(), - record.field("link").value(), - record.field("hidden").value())) - - # Load entries - Globals.entries = [] # Reset local entries array - query.exec_("SELECT * FROM entries") - while query.next(): - record = query.record() - # create a QDate if the due date is set - if record.field("due_date").value(): - due_date_struct = strptime(record.field("due_date").value(), "%Y-%m-%d") - due_date = QDate(due_date_struct.tm_year, due_date_struct.tm_mon, due_date_struct.tm_mday) - else: - due_date = "" - Globals.entries.append( - Entry( - record.field("id").value(), - record.field("parent_id").value(), - record.field("description").value(), - due_date, - record.field("alt_due_date").value(), - record.field("link").value(), - record.field("color").value(), - record.field("highlight").value(), - record.field("done").value(), - record.field("hidden").value())) - - # Load rules - Globals.rules = [] # Reset local rules array - query.exec_("SELECT * FROM rules") - while query.next(): - record = query.record() - date_struct = strptime(record.field("date").value(), "%Y-%m-%d") - date = QDate(date_struct.tm_year, date_struct.tm_mon, date_struct.tm_mday) - Globals.rules.append( - Rule( - record.field("id").value(), - record.field("entry_id").value(), - "before" if record.field("before_after").value() == 0 else "after", - date, - record.field("color").value(), - record.field("highlight").value())) - - database.close() - -def insertGroup(new_group): - """ - Insert group to the database at Globals.db_path - """ - output = -1 - - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - INSERT INTO groups (name, column, link) VALUES (?, ?, ?) - """) - query.addBindValue(new_group.name) - query.addBindValue(new_group.column) - query.addBindValue(new_group.link) - query.exec_() - - output = query.lastInsertId() - - database.close() - - return output - -def insertEntry(new_entry): - """ - Insert entry to the database at Globals.db_path - """ - output = -1 - - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - INSERT INTO entries (parent_id, description, due_date, alt_due_date, link, color, highlight) VALUES (:p_id, :desc, :due, :alt_due, :link, :color, :highlight) - """) - query.bindValue(":p_id", new_entry.parent_id) - query.bindValue(":desc", new_entry.desc) - if new_entry.due: - query.bindValue(":due", "{0}-{1}-{2}".format( - new_entry.due.year(), - new_entry.due.month(), - new_entry.due.day())) - else: - query.bindValue(":due", "") - query.bindValue(":alt_due", new_entry.due_alt) - query.bindValue(":link", new_entry.link) - query.bindValue(":color", new_entry.color) - query.bindValue(":highlight", new_entry.highlight) - success = query.exec_() - # DEBUG - #print(query.lastError().text()) - #print(query.boundValues()) - #if success: - # print("Query succeeded") - #else: - # print("Query failed") - - output = query.lastInsertId() - - database.close() - - return output - -def insertRule(new_rule): - """ - Insert rule to the database at Globals.db_path - """ - output = -1 - - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - INSERT INTO rules (entry_id, before_after, date, color, highlight) VALUES (:e_id, :when, :date, :color, :highlight) - """) - query.bindValue(":e_id", new_rule.entry_id) - query.bindValue(":when", 0 if new_rule.when.lower() == "before" else 1) - query.bindValue(":date", "{0}-{1}-{2}".format( - new_rule.date.year(), - new_rule.date.month(), - new_rule.date.day())) - query.bindValue(":color", new_rule.color) - query.bindValue(":highlight", new_rule.highlight) - success = query.exec_() - # DEBUG - #print(query.lastError().text()) - #print(query.boundValues()) - #if success: - # print("Query succeeded") - #else: - # print("Query failed") - - output = query.lastInsertId() - - database.close() - - return output - -def updateGroup(group): - """ - Update group by its id - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - UPDATE groups SET name = ?, column = ?, link = ?, hidden = ? WHERE id = ? - """) - query.addBindValue(group.name) - query.addBindValue(group.column) - query.addBindValue(group.link) - query.addBindValue(group.hidden) - query.addBindValue(group.id) - query.exec_() - - database.close() - -def updateEntry(entry): - """ - Update entry by its id - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - UPDATE entries SET - description = :desc, - due_date = :due, - alt_due_date = :alt_due, - link = :link, - color = :color, - highlight = :highlight, - done = :done, - hidden = :hidden - WHERE id = :id - """) - query.bindValue(":desc", entry.desc) - if entry.due: - query.bindValue(":due", "{0}-{1}-{2}".format( - entry.due.year(), - entry.due.month(), - entry.due.day())) - else: - query.bindValue(":due", "") - query.bindValue(":alt_due", entry.due_alt) - query.bindValue(":link", entry.link) - query.bindValue(":color", entry.color) - query.bindValue(":highlight", entry.highlight) - query.bindValue(":done", entry.done) - query.bindValue(":hidden", entry.hidden) - query.bindValue(":id", entry.id) - query.exec_() - - database.close() - -def updateRule(rule): - """ - Update rule by its id - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - query.prepare(""" - UPDATE rules SET - before_after = :when, - date = :date, - color = :color, - highlight = :highlight - WHERE id = :id - """) - query.bindValue(":when", 0 if rule.when.lower() == "before" else 1) - query.bindValue(":date", "{0}-{1}-{2}".format( - rule.date.year(), - rule.date.month(), - rule.date.day())) - query.bindValue(":color", rule.color) - query.bindValue(":highlight", rule.highlight) - query.bindValue(":id", rule.id) - success = query.exec_() - # DEBUG - #print(query.lastError().text()) - #print(query.boundValues()) - #if success: - # print("Query succeeded") - #else: - # print("Query failed") - - database.close() - -def removeGroup(group_id): - """ - Remove a group by id from the database - (actually set hidden to true, don't permanently delete it) - """ - - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - # First, set entries to hidden - query.prepare(""" - UPDATE entries SET hidden = 1 WHERE parent_id = ? - """) - query.addBindValue(group_id) - query.exec_() - - # Now, set the group to hidden - query.prepare(""" - UPDATE groups SET hidden = 1 WHERE id = ? - """) - query.addBindValue(group_id) - query.exec_() - - output = query.numRowsAffected() - database.close() - return output - -def removeEntry(entry_id): - """ - Remove a group by id from the database - (actually set hidden to true, don't permanently delete it) - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - # Set entry to hidden - query.prepare(""" - UPDATE entries SET hidden = 1 WHERE id = ? - """) - query.addBindValue(entry_id) - query.exec_() - - output = query.numRowsAffected() - database.close() - return output - -def removeRule(rule_id): - """ - Remove a rule by id from the database - (we do not preserve rules, unlike groups and entries) - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - # Set entry to hidden - query.prepare(""" - DELETE FROM rules WHERE id = ? - """) - query.addBindValue(rule_id) - query.exec_() - - output = query.numRowsAffected() - database.close() - return output - -def cleanHidden(): - """ - Permanently delete removed/hidden groups and entries - """ - database = QSqlDatabase.addDatabase("QSQLITE") # SQlite version 3 - database.setDatabaseName(Globals.db_path) - - if not database.open(): - print("Unable to open data source file.") - sys.exit(1) # Error out. TODO consider throwing a dialog instead - - query = QSqlQuery() - - # Remove rules associated with hidden entries - query.exec_(""" - DELETE FROM rules WHERE id IN ( - SELECT rules.id FROM rules - INNER JOIN entries ON rules.entry_id = entries.id - WHERE entries.hidden = 1 - )""") - - # Remove hidden entries - query.exec_(""" - DELETE FROM entries WHERE hidden = 1 - """) - - # Remove hidden groups - query.exec_(""" - DELETE FROM groups WHERE hidden = 1 - """) - - database.close() diff --git a/src/edit_entry_form.py b/src/edit_entry_form.py deleted file mode 100644 index 3c7f8c2..0000000 --- a/src/edit_entry_form.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import sys -from PyQt5 import uic -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtCore import QDate - -import src.globals as Globals -from src.entry import Entry -import src.db_sqlite as DB - -# Reuses the add_entry_form UI file -class editEntryForm(QDialog): - """ - Form to edit/update an entry - """ - def __init__(self, id): - self.id = id - super().__init__() - uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "add_entry_form.ui"), self) - self.initializeUI() - - def initializeUI(self): - self.setWindowTitle("Edit Entry") - self.displayWidgets() - self.exec() - - def displayWidgets(self): - entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0] - - self.title.setText("Edit Entry") - self.new_entry_desc.setText(entry.desc) - self.new_entry_due.setDate(QDate.currentDate()) - if entry.due: - self.new_entry_due.setDate(entry.due) - self.new_entry_due_checkbox.setChecked(True) - else: - self.new_entry_due_checkbox.setChecked(False) - self.new_entry_due_alt.setText(entry.due_alt) - self.new_entry_link.setText(entry.link) - self.new_entry_color.setText(entry.color) - self.new_entry_highlight.setText(entry.highlight) - self.buttonBox.rejected.connect(self.close) - self.buttonBox.accepted.connect(self.handleSubmit) - - def handleSubmit(self): - desc_text = self.new_entry_desc.text() - if self.new_entry_due_checkbox.isChecked(): - due_text = self.new_entry_due.date() # due_text is a QDate - else: - due_text = "" # due is unchecked - due_alt_text = self.new_entry_due_alt.text() - link_text = self.new_entry_link.text() - color_text = self.new_entry_color.text() - highlight_text = self.new_entry_highlight.text() - - if not desc_text: - QMessageBox.warning(self, "Error Message", - "Name cannot be blank", - QMessageBox.Close, - QMessageBox.Close) - return - - # Update DB - entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0] - entry.desc = desc_text - entry.due = due_text - entry.due_alt = due_alt_text - entry.link = link_text - entry.color = color_text - entry.highlight = highlight_text - DB.updateEntry(entry) - - # Update global variables - Globals.entries = list(filter(lambda e: e.id != self.id, Globals.entries)) - Globals.entries.append(Entry(self.id, entry.parent_id, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text, entry.done, entry.hidden)) - self.close() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = editEntryForm() - sys.exit(app.exec_()) - diff --git a/src/edit_group_form.py b/src/edit_group_form.py deleted file mode 100644 index cd01162..0000000 --- a/src/edit_group_form.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -import sys -from PyQt5 import uic -from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox - -import src.globals as Globals -from src.group import Group -import src.db_sqlite as DB - -class editGroupForm(QDialog): - """ - Form to edit/update a group - """ - def __init__(self, id): - self.id = id - super().__init__() - uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "add_group_form.ui"), self) - self.initializeUI() - - def initializeUI(self): - self.setWindowTitle("Edit Group") - self.displayWidgets() - self.exec() - - def displayWidgets(self): - group = list(filter(lambda g: g.id == self.id, Globals.groups))[0] - - self.title.setText("Edit Group") - self.new_group_name.setText(group.name) - self.new_group_column.setCurrentIndex(0 if group.column.lower() == "left" else 1) - self.new_group_link.setText(group.link) - self.buttonBox.rejected.connect(self.close) - self.buttonBox.accepted.connect(self.handleSubmit) - - def handleSubmit(self): - name_text = self.new_group_name.text() - column_text = self.new_group_column.currentText() - link_text = self.new_group_link.text() - - if not name_text: - QMessageBox.warning(self, "Error Message", - "Name cannot be blank", - QMessageBox.Close, - QMessageBox.Close) - return - - # Update DB - group = list(filter(lambda g: g.id == self.id, Globals.groups))[0] - group.name = name_text - group.column = column_text - group.link = link_text - DB.updateGroup(group) - - # Update global variables - Globals.groups = list(filter(lambda g: g.id != self.id, Globals.groups)) - Globals.groups.append(Group(self.id, name_text, column_text, link_text, group.hidden)) - self.close() - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = editGroupForm() - sys.exit(app.exec_()) diff --git a/src/entry.py b/src/entry.py deleted file mode 100644 index 3b639ab..0000000 --- a/src/entry.py +++ /dev/null @@ -1,100 +0,0 @@ -from datetime import date -from PyQt5.QtCore import QDate, Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QHBoxLayout, QLabel -import src.globals as Globals - -class Entry: - def __init__(self, id, parent_id, desc, due = "", due_alt = "", link = "", color = "", highlight = "", done = False, hidden = False): - self.id = id - self.parent_id = parent_id - self.desc = desc - self.due = due - self.due_alt = due_alt - self.link = link - self.color = color - self.highlight = highlight - self.done = done - self.hidden = hidden - - def buildLayout(self): - output = QHBoxLayout() - bullet = QLabel() - body = QLabel() - - output.setContentsMargins(2,2,2,2) - - bullet.setFont(QFont("Arial", 11)) - bullet.setMaximumWidth(15) - - body.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) - body.setFont(QFont("Arial", 11)) - body.setWordWrap(True) - body.setToolTip("Right-Click for actions") - - # Check rules - relevant_rules = list(filter(lambda r: r.entry_id == self.id, Globals.rules)) - for r in relevant_rules: - if (r.when.lower() == "before" and r.date > QDate.currentDate()) or (r.when.lower() == "after" and r.date <= QDate.currentDate()): - if r.color: - self.color = r.color - if r.highlight: - self.highlight = r.highlight - - if self.done: - bullet.setText("\u2713 ") - bullet.setStyleSheet(""" - QLabel{ - color: green; - } - """) - self.color = "green" - else: - bullet.setText("- ") - output.addWidget(bullet) - - if self.due: - body.setText("{0}/{1}/{2}: ".format( - self.due.month(), - self.due.day(), - self.due.year() - )) - elif self.due_alt: - body.setText("{0}: ".format( - self.due_alt - )) - if self.link: - body.setOpenExternalLinks(True) - body.setText(body.text() + "".format( - self.link, - self.color if self.color else "default" - )) - body.setText(body.text() + self.desc) - if self.link: - body.setText(body.text() + "") - body.setToolTip("{}".format(self.link)) - output.addWidget(body) - - if self.done: - font = body.font() - font.setStrikeOut(True) - body.setFont(font) - body.setStyleSheet(""" - QLabel{ - color: green; - } - """) - - else: - body.setStyleSheet(""" - QLabel{{ - color: {0}; - background-color: {1}; - font-weight: {2}; - }}""".format( - self.color if self.color else "default", - self.highlight if self.highlight else "none", - "bold" if self.due and self.due <= date.today() else "normal" - )) - - return output diff --git a/src/globals.py b/src/globals.py deleted file mode 100644 index bb6f717..0000000 --- a/src/globals.py +++ /dev/null @@ -1,4 +0,0 @@ -groups = [] -entries = [] -rules = [] -db_path = "" diff --git a/src/group.py b/src/group.py deleted file mode 100644 index 18b4836..0000000 --- a/src/group.py +++ /dev/null @@ -1,25 +0,0 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QLabel, QVBoxLayout -import src.globals as Globals - -class Group: - def __init__(self, id, name, column = "left", link = "", hidden = False): - self.id = id - self.name = name - self.column = column - self.link = link - self.hidden = hidden - - def buildLayout(self): - output = QVBoxLayout() - output.setContentsMargins(0, 10, 0, 10) - - name = QLabel(self.name) - name.setTextInteractionFlags(Qt.TextSelectableByMouse) - name_font = QFont("Arial", 13) - name_font.setUnderline(True) - name.setFont(name_font) - output.addWidget(name) - - return output diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 88e0cda..0000000 --- a/src/main.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/python3 -import os -import sys -import time -from PyQt5 import uic -from PyQt5.QtWidgets import QAction, QApplication, QMainWindow, QMenu, QMessageBox, QVBoxLayout -from PyQt5.QtGui import QCursor -from PyQt5.QtCore import QDate, Qt -from src.config import Config -from src.preferences_dialog import PreferencesDialog -from src.add_group_form import addGroupForm -from src.edit_group_form import editGroupForm -from src.add_entry_form import addEntryForm -from src.edit_entry_form import editEntryForm -import src.globals as Globals -import src.db_sqlite as DB -from src.rules_dialog import RulesDialog - -class AssignmentList(QMainWindow): - def __init__(self): - super().__init__() - uic.loadUi(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "main.ui"), self) - - self.initializeUI() - - def initializeUI(self): - self.createMenu() - self.createToolbar() - Config() - self.setupDB() - self.displayWidgets() - self.show() - - def createMenu(self): - self.actionPreferences.triggered.connect(self.preferences) - self.actionReload.triggered.connect(self.reload) - self.actionExit.triggered.connect(self.close) - - self.actionAdd_Group.triggered.connect(self.addGroup) - self.actionClean_Hidden.triggered.connect(self.cleanHidden) - - self.actionAbout.triggered.connect(self.aboutDialog) - - def createToolbar(self): - self.toolBar.addAction(self.actionAdd_Group) - - def setupDB(self): - DB.initDB() - - def displayWidgets(self): - self.title.setText(time.strftime("%A, %b %d %Y")) - self.drawGroups() - - def addGroup(self): - """ - Open the 'addGroup' form - """ - old_count = len(Globals.groups) - self.create_new_group_dialog = addGroupForm() - if old_count != len(Globals.groups): - self.drawGroups() - - def editGroup(self, id): - """ - Open the 'editGroup' form - """ - self.create_edit_group_dialog = editGroupForm(id) - self.drawGroups() - - def removeGroup(self, id): - """ - Delete a group with a given id - """ - # TODO might want to add a warning - # TODO might want to make part of the a destructor in the Group class - removed = DB.removeGroup(id) - if removed > 0: - Globals.entries = list(filter(lambda e: e.parent_id != id, Globals.entries)) - Globals.groups = list(filter(lambda g: g.id != id, Globals.groups)) - self.drawGroups() - - def groupContextMenu(self, group_id): - menu = QMenu() - - add_entry_act = QAction("Add Entry") - add_entry_act.triggered.connect((lambda id: lambda: self.addEntry(id))(group_id)) - menu.addAction(add_entry_act) - - edit_group_act = QAction("Edit Group") - edit_group_act.triggered.connect((lambda id: lambda: self.editGroup(id))(group_id)) - menu.addAction(edit_group_act) - - del_group_act = QAction("Remove Group") - del_group_act.triggered.connect((lambda id: lambda: self.removeGroup(id))(group_id)) - menu.addAction(del_group_act) - - menu.exec_(QCursor.pos()) - - def addEntry(self, parent): - """ - Open the 'addEntry' form - """ - old_count = len(Globals.entries) - self.create_new_entry_dialog = addEntryForm(parent) - if old_count != len(Globals.entries): - self.drawGroups() # TODO see if we can do this with only redrawing a single group - - def editEntry(self, id): - """ - Open the 'editEntry' form - """ - self.create_edit_entry_dialog = editEntryForm(id) - self.drawGroups() - - def toggleDoneEntry(self, id): - """ - Toggle the 'done' flag on the entry with the given id - """ - entry = list(filter(lambda e: e.id == id, Globals.entries))[0] - if entry.done: - entry.done = False - else: - entry.done = True - DB.updateEntry(entry) - Globals.entries = list(filter(lambda e: e.id != id, Globals.entries)) - Globals.entries.append(entry) - self.drawGroups() - - def removeEntry(self, id): - """ - Delete an entry with a given id - """ - # TODO might want to add a warning - # TODO might want to make part of the a destructor in the Entry class - removed = DB.removeEntry(id) - if removed > 0: - Globals.entries = list(filter(lambda e: e.id != id, Globals.entries)) - self.drawGroups() - - def editRules(self, id): - pass - need_reload = RulesDialog(id) - if need_reload: - self.reload() - - def entryContextMenu(self, entry_id): - menu = QMenu() - - edit_entry_act = QAction("Edit Entry") - edit_entry_act.triggered.connect((lambda id: lambda: self.editEntry(id))(entry_id)) - menu.addAction(edit_entry_act) - - rules_act = QAction("Rules") - rules_act.triggered.connect((lambda id: lambda: self.editRules(id))(entry_id)) - menu.addAction(rules_act) - - mark_done_act = QAction("Done", checkable=True) - if list(filter(lambda e: e.id == entry_id, Globals.entries))[0].done: - mark_done_act.setChecked(True) - mark_done_act.triggered.connect((lambda id: lambda: self.toggleDoneEntry(id))(entry_id)) - menu.addAction(mark_done_act) - - del_entry_act = QAction("Remove Entry") - del_entry_act.triggered.connect((lambda id: lambda: self.removeEntry(id))(entry_id)) - menu.addAction(del_entry_act) - - menu.exec_(QCursor.pos()) - - def preferences(self): - # TODO not sure if this is working exactly how I think it does, but it works - need_reload = PreferencesDialog() - if need_reload: - self.reload() - - def cleanHidden(self): - """ - Permanently delete removed groups and entries from db - """ - # TODO consider creating a warning dialogue for this - DB.cleanHidden() - - def drawGroups(self): - """ - Redraw the groups_layout - """ - # Remove all children from layout - def recursiveClear(layout): - while layout.count(): - child = layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() - elif child.layout(): - recursiveClear(child) - - recursiveClear(self.groups_layout) - - # Sort the groups - Globals.groups = sorted(Globals.groups, key=lambda g: g.id) - - # Sort the entries - Globals.entries = sorted(Globals.entries, key=lambda e: (e.parent_id, (e.due if e.due else QDate.currentDate()), e.done, e.id)) - - # Sort the rules - Globals.rules = sorted(Globals.rules, key=lambda r: (r.id)) - - # Create columns as vertical boxes - column_left = QVBoxLayout() - column_right = QVBoxLayout() - - for g in Globals.groups: - # skip if this group is set to hidden - if g.hidden: - continue - - g_layout = g.buildLayout() - - # Create custom context menu - g_layout.itemAt(0).widget().setToolTip("Right-Click for actions") - g_layout.itemAt(0).widget().setContextMenuPolicy(Qt.CustomContextMenu) - g_layout.itemAt(0).widget().customContextMenuRequested.connect((lambda id: lambda: self.groupContextMenu(id))(g.id)) - - # Draw entries belonging to this group - g_layout.addLayout(self.drawEntries(g.id)) - - if g.column.lower() == "left": - column_left.addLayout(g_layout) - else: - column_right.addLayout(g_layout) - - column_left.addStretch() - column_right.addStretch() - - self.groups_layout.addLayout(column_left, 0, 0) - self.groups_layout.addLayout(column_right, 0, 1) - - def drawEntries(self, group_id): - """ - Redraw the entries of a specific group - """ - # TODO consider having code to remove existing widgets to make this function more modular - entries = list(filter(lambda e: e.parent_id == group_id, Globals.entries)) - entries_vbox = QVBoxLayout() - entries_vbox.setContentsMargins(5, 0, 0, 0) - - for e in entries: - # skip if this entry is set to hidden - if e.hidden: - continue - - e_layout = e.buildLayout() - entries_vbox.addLayout(e_layout) - - # Create custom context menu - e_layout.itemAt(1).widget().setContextMenuPolicy(Qt.CustomContextMenu) - e_layout.itemAt(1).widget().customContextMenuRequested.connect((lambda id: lambda: self.entryContextMenu(id))(e.id)) - - return entries_vbox - - def reload(self): - Config() - self.setupDB() - self.drawGroups() - - def aboutDialog(self): - QMessageBox.about(self, "About Assignment List", - "Created by Louie S. - 2023") - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = AssignmentList() - sys.exit(app.exec_()) - diff --git a/src/main.ui b/src/main.ui deleted file mode 100644 index bc3c0fb..0000000 --- a/src/main.ui +++ /dev/null @@ -1,207 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 640 - 480 - - - - Assignment List - - - - - - - true - - - - - 0 - 0 - 620 - 421 - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - Arial - 17 - - - - [DATE] - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - 20 - - - 5 - - - 20 - - - 5 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - 0 - 0 - 640 - 22 - - - - - File - - - - - - - - - Edit - - - - - - - - Help - - - - - - - - - - toolBar - - - TopToolBarArea - - - false - - - - - Preferences - - - Alt+Return - - - - - Reload - - - F5 - - - - - Exit - - - Ctrl+Q - - - - - Add Group - - - - - Permanently Delete Removed Groups and Entries - - - - - About - - - - - - diff --git a/src/preferences_dialog.py b/src/preferences_dialog.py deleted file mode 100644 index e6d4187..0000000 --- a/src/preferences_dialog.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import sys -from PyQt5 import uic -from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog -from src.config import Config - -class PreferencesDialog(QDialog): - """ - Implemented to set configuration options in the program - """ - def __init__(self): - super().__init__() - uic.loadUi(os.path.join("src", "preferences_dialog.ui"), self) - - # class globals - self.config = Config() - - self.initializeUI() - - def initializeUI(self): - self.displayWidgets() - self.exec() - - def displayWidgets(self): - # TODO make this a scrollable window - # FIXME could use some work on sizing - self.pathsTabLayout() - - self.close_button.clicked.connect(self.close) - self.apply_button.clicked.connect(self.apply) - self.reload_button.clicked.connect(self.reload) - - def pathsTabLayout(self): - if "paths" in self.config.config: - self.db_path_edit.setText(self.config.config["paths"]["db_path"]) - self.db_path_button.clicked.connect(self.dbPathDialog) - - def dbPathDialog(self): - file_dialog = QFileDialog(self) - # TODO create filter to only allow selecting .db files - new_path = file_dialog.getOpenFileName(self, "Open File") - - if new_path[0]: - self.db_path_edit.setText(new_path[0]) - - def apply(self): - """ - Update the configuration in the config file - """ - # Save paths - if "paths" in self.config.config: - try: - with open(self.db_path_edit.text(), 'a'): - self.config.config["paths"]["db_path"] = self.db_path_edit.text() - except: - print("Warning: db_path '{}' does not exist; skipping".format(self.db_path_edit.text())) - - self.config.updateConfig() - - def reload(self): - """ - Update, reload, and close the window - """ - self.apply() - self.done(1) - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = PreferencesDialog() - sys.exit(app.exec_()) diff --git a/src/preferences_dialog.ui b/src/preferences_dialog.ui deleted file mode 100644 index 244eb26..0000000 --- a/src/preferences_dialog.ui +++ /dev/null @@ -1,128 +0,0 @@ - - - Dialog - - - - 0 - 0 - 500 - 320 - - - - Preferences - - - - - - - - 0 - - - - Paths - - - - - - - - - - - Database File: - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - 25 - 16777215 - - - - ... - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - - - - - Apply - - - - - - - Reload - - - - - - - - - - - - diff --git a/src/rule.py b/src/rule.py deleted file mode 100644 index 414ccf0..0000000 --- a/src/rule.py +++ /dev/null @@ -1,44 +0,0 @@ - -from PyQt5.QtCore import QDate -from PyQt5.QtWidgets import QComboBox, QDateTimeEdit, QHBoxLayout, QLineEdit - - -class Rule: - def __init__(self, id, entry_id, when, date, color = "", highlight = ""): - self.id = id - self.entry_id = entry_id - self.when = when - self.date = date - self.color = color - self.highlight = highlight - - def buildLayout(self): - output = QHBoxLayout() - - when_widget = QComboBox() - when_widget.addItems(["Before", "After"]) - when_widget.setCurrentIndex(0 if self.when.lower() == "before" else 1) - output.addWidget(when_widget) - - date_widget = QDateTimeEdit(QDate.currentDate()) - date_widget.setDisplayFormat("MM/dd/yyyy") - date_widget.setDate(self.date) - output.addWidget(date_widget) - - output.addStretch() - - # TODO Consider making this a color selector widget - color_widget = QLineEdit() - color_widget.setPlaceholderText("Color") - if self.color: - color_widget.setText(self.color) - output.addWidget(color_widget) - - # TODO Consider making this a color selector widget - highlight_widget = QLineEdit() - highlight_widget.setPlaceholderText("Highlight") - if self.highlight: - highlight_widget.setText(self.highlight) - output.addWidget(highlight_widget) - - return output diff --git a/src/rules_dialog.py b/src/rules_dialog.py deleted file mode 100644 index 0980fdd..0000000 --- a/src/rules_dialog.py +++ /dev/null @@ -1,128 +0,0 @@ -import sys -from PyQt5.QtCore import QDate -from PyQt5.QtWidgets import QApplication, QDialog, QHBoxLayout, QPushButton, QScrollArea, QVBoxLayout -from src.config import Config -from src.rule import Rule -import src.db_sqlite as DB -import src.globals as Globals - -class RulesDialog(QDialog): - """ - Show the list of rules associated with an entry - """ - def __init__(self, entry_id): - super().__init__() - - self.entry_id = entry_id - # class globals - self.config = Config() - self.relevant_rules = self.getRelevantRules() - - self.initializeUI() - - def initializeUI(self): - self.resize(500, 320) - self.setWindowTitle("Rules") - self.displayWidgets() - self.exec() - - def displayWidgets(self): - main_layout = QVBoxLayout() - main_layout_scroll_area = QScrollArea() - main_layout_scroll_area.setWidgetResizable(True) - main_layout_scroll_area.setLayout(main_layout) - - self.rules_layout = QVBoxLayout() - self.drawRules() - main_layout.addLayout(self.rules_layout) - - main_layout.addStretch() - # Create Close and Save buttons - buttons_hbox = QHBoxLayout() - buttons_hbox.addStretch() - - close_button = QPushButton("Close", self) - close_button.clicked.connect(self.close) - buttons_hbox.addWidget(close_button) - - save_button = QPushButton("Save", self) - save_button.clicked.connect(self.save) - buttons_hbox.addWidget(save_button) - - main_layout.addLayout(buttons_hbox) - self.setLayout(main_layout) - - def drawRules(self): - # Remove all children from layout - def recursiveClear(layout): - while layout.count(): - child = layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() - elif child.layout(): - recursiveClear(child) - - recursiveClear(self.rules_layout) - - # Draw each rule - self.r_layouts_dict = {} # Use to help update things in the save() function - for r in self.relevant_rules: - r_layout = r.buildLayout() - self.r_layouts_dict[r.id] = r_layout - del_button = QPushButton("Delete", self) - del_button.clicked.connect((lambda id: lambda: self.deleteRule(id))(r.id)) - r_layout.addWidget(del_button) - self.rules_layout.addLayout(r_layout) - - # Draw a button to add rules - rules_buttons_hbox = QHBoxLayout() - add_rule_button = QPushButton("Add Rule", self) - add_rule_button.clicked.connect(self.addRule) - rules_buttons_hbox.addWidget(add_rule_button) - rules_buttons_hbox.addStretch() - self.rules_layout.addLayout(rules_buttons_hbox) - - def addRule(self): - self.apply() - - new_rule = Rule(0, self.entry_id, "before", QDate.currentDate()) - new_rule_id = DB.insertRule(new_rule) - new_rule.id = new_rule_id - Globals.rules.append(new_rule) - self.relevant_rules = self.getRelevantRules() - self.drawRules() - - def deleteRule(self, rule_id): - DB.removeRule(rule_id) - Globals.rules = list(filter(lambda r: r.id != rule_id, Globals.rules)) - self.relevant_rules = self.getRelevantRules() - self.drawRules() - - def getRelevantRules(self): - return list(filter(lambda r: r.entry_id == self.entry_id, Globals.rules)) - - def apply(self): - for id, layout in self.r_layouts_dict.items(): - updated_rule = Rule( - id, - self.entry_id, - layout.itemAt(0).widget().currentText(), - layout.itemAt(1).widget().date(), - layout.itemAt(3).widget().text(), - layout.itemAt(4).widget().text()) - DB.updateRule(updated_rule) - Globals.rules = list(filter(lambda r: r.id != id, Globals.rules)) - Globals.rules.append(updated_rule) - - def save(self): - """ - Save any existing rules. Added rules are automatically saved, - but changing rules is not, hence the need for a manual save - """ - self.apply() - self.done(1) - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = RulesDialog() - sys.exit(app.exec_()) -- cgit