diff options
author | Louie Shprung <lshprung@scu.edu> | 2023-12-21 13:17:39 -0800 |
---|---|---|
committer | Louie Shprung <lshprung@scu.edu> | 2023-12-21 13:17:39 -0800 |
commit | 65d8cbafe34b946d450ce46703e6449eb9962741 (patch) | |
tree | 9cb2bfa4857507207e8740478e5d23d4421ad784 /assignment_list_pyqt | |
parent | 11373742d701166f2580cfe67015eac012cda1a9 (diff) |
Rename src directory to unique name to avoid install issues
Diffstat (limited to 'assignment_list_pyqt')
-rw-r--r-- | assignment_list_pyqt/__init__.py | 0 | ||||
-rw-r--r-- | assignment_list_pyqt/add_entry_form.py | 51 | ||||
-rw-r--r-- | assignment_list_pyqt/add_entry_form.ui | 185 | ||||
-rw-r--r-- | assignment_list_pyqt/add_group_form.py | 47 | ||||
-rw-r--r-- | assignment_list_pyqt/add_group_form.ui | 131 | ||||
-rw-r--r-- | assignment_list_pyqt/config.py | 95 | ||||
-rw-r--r-- | assignment_list_pyqt/db_sqlite.py | 493 | ||||
-rw-r--r-- | assignment_list_pyqt/edit_entry_form.py | 83 | ||||
-rw-r--r-- | assignment_list_pyqt/edit_group_form.py | 63 | ||||
-rw-r--r-- | assignment_list_pyqt/entry.py | 100 | ||||
-rw-r--r-- | assignment_list_pyqt/globals.py | 4 | ||||
-rw-r--r-- | assignment_list_pyqt/group.py | 25 | ||||
-rw-r--r-- | assignment_list_pyqt/main.py | 273 | ||||
-rw-r--r-- | assignment_list_pyqt/main.ui | 207 | ||||
-rw-r--r-- | assignment_list_pyqt/preferences_dialog.py | 71 | ||||
-rw-r--r-- | assignment_list_pyqt/preferences_dialog.ui | 128 | ||||
-rw-r--r-- | assignment_list_pyqt/rule.py | 44 | ||||
-rw-r--r-- | assignment_list_pyqt/rules_dialog.py | 128 |
18 files changed, 2128 insertions, 0 deletions
diff --git a/assignment_list_pyqt/__init__.py b/assignment_list_pyqt/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/assignment_list_pyqt/__init__.py 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Dialog</class> + <widget class="QDialog" name="Dialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>266</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Entry</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="1" column="0"> + <widget class="QLabel" name="descriptionLabel"> + <property name="text"> + <string>Description: </string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="new_entry_desc"/> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <family>Arial</family> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>Add Entry</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="dueDateLabel"> + <property name="text"> + <string>Due Date: </string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QWidget" name="due_hbox" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QDateTimeEdit" name="new_entry_due"> + <property name="displayFormat"> + <string>MM/dd/yyyy</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="new_entry_due_checkbox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="dueDateAltLabel"> + <property name="text"> + <string>Due Date (Alt):</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="new_entry_due_alt"/> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="linkLabel"> + <property name="text"> + <string>Link:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="new_entry_link"/> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="colorLabel"> + <property name="text"> + <string>Color:</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLineEdit" name="new_entry_color"/> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="highlightLabel"> + <property name="text"> + <string>Highlight:</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLineEdit" name="new_entry_highlight"/> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="layoutDirection"> + <enum>Qt::RightToLeft</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + <property name="centerButtons"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>Dialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Dialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Dialog</class> + <widget class="QDialog" name="Dialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>172</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add Entry</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <family>Arial</family> + <pointsize>18</pointsize> + </font> + </property> + <property name="text"> + <string>Add Group</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="descriptionLabel"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="new_group_name"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="dueDateLabel"> + <property name="text"> + <string>Column:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="new_group_column"> + <item> + <property name="text"> + <string>Left</string> + </property> + </item> + <item> + <property name="text"> + <string>Right</string> + </property> + </item> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="linkLabel"> + <property name="text"> + <string>Link:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="new_group_link"/> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="layoutDirection"> + <enum>Qt::RightToLeft</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + <property name="centerButtons"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>Dialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Dialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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() + "<a href=\"{0}\" style=\"color: {1};\">".format( + self.link, + self.color if self.color else "default" + )) + body.setText(body.text() + self.desc) + if self.link: + body.setText(body.text() + "</a>") + 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>640</width> + <height>480</height> + </rect> + </property> + <property name="windowTitle"> + <string>Assignment List</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QScrollArea" name="scrollArea"> + <property name="widgetResizable"> + <bool>true</bool> + </property> + <widget class="QWidget" name="scrollAreaWidgetContents_3"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>620</width> + <height>421</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="v_box"> + <item> + <layout class="QHBoxLayout" name="title_h_box"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="title"> + <property name="font"> + <font> + <family>Arial</family> + <pointsize>17</pointsize> + </font> + </property> + <property name="text"> + <string>[DATE]</string> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <layout class="QGridLayout" name="groups_layout"> + <property name="leftMargin"> + <number>20</number> + </property> + <property name="topMargin"> + <number>5</number> + </property> + <property name="rightMargin"> + <number>20</number> + </property> + <property name="bottomMargin"> + <number>5</number> + </property> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>640</width> + <height>22</height> + </rect> + </property> + <widget class="QMenu" name="menuFile"> + <property name="title"> + <string>File</string> + </property> + <addaction name="actionPreferences"/> + <addaction name="actionReload"/> + <addaction name="separator"/> + <addaction name="actionExit"/> + </widget> + <widget class="QMenu" name="menuEdit"> + <property name="title"> + <string>Edit</string> + </property> + <addaction name="actionAdd_Group"/> + <addaction name="separator"/> + <addaction name="actionClean_Hidden"/> + </widget> + <widget class="QMenu" name="menuHelp"> + <property name="title"> + <string>Help</string> + </property> + <addaction name="actionAbout"/> + </widget> + <addaction name="menuFile"/> + <addaction name="menuEdit"/> + <addaction name="menuHelp"/> + </widget> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + </widget> + <action name="actionPreferences"> + <property name="text"> + <string>Preferences</string> + </property> + <property name="shortcut"> + <string>Alt+Return</string> + </property> + </action> + <action name="actionReload"> + <property name="text"> + <string>Reload</string> + </property> + <property name="shortcut"> + <string>F5</string> + </property> + </action> + <action name="actionExit"> + <property name="text"> + <string>Exit</string> + </property> + <property name="shortcut"> + <string>Ctrl+Q</string> + </property> + </action> + <action name="actionAdd_Group"> + <property name="text"> + <string>Add Group</string> + </property> + </action> + <action name="actionClean_Hidden"> + <property name="text"> + <string>Permanently Delete Removed Groups and Entries</string> + </property> + </action> + <action name="actionAbout"> + <property name="text"> + <string>About</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Dialog</class> + <widget class="QDialog" name="Dialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>320</height> + </rect> + </property> + <property name="windowTitle"> + <string>Preferences</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QVBoxLayout" name="main_layout"> + <item> + <widget class="QTabWidget" name="tab_bar"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="tab"> + <attribute name="title"> + <string>Paths</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QWidget" name="widget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QFormLayout" name="paths_tab_layout"> + <item row="0" column="0"> + <widget class="QLabel" name="databaseFileLabel"> + <property name="text"> + <string>Database File:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QWidget" name="db_path_hbox" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="db_path_edit"/> + </item> + <item> + <widget class="QPushButton" name="db_path_button"> + <property name="maximumSize"> + <size> + <width>25</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="buttons_hbox"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="close_button"> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="apply_button"> + <property name="text"> + <string>Apply</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="reload_button"> + <property name="text"> + <string>Reload</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> 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_()) |