summaryrefslogtreecommitdiff
path: root/assignment_list_pyqt
diff options
context:
space:
mode:
authorLouie Shprung <lshprung@scu.edu>2023-12-21 13:17:39 -0800
committerLouie Shprung <lshprung@scu.edu>2023-12-21 13:17:39 -0800
commit65d8cbafe34b946d450ce46703e6449eb9962741 (patch)
tree9cb2bfa4857507207e8740478e5d23d4421ad784 /assignment_list_pyqt
parent11373742d701166f2580cfe67015eac012cda1a9 (diff)
Rename src directory to unique name to avoid install issues
Diffstat (limited to 'assignment_list_pyqt')
-rw-r--r--assignment_list_pyqt/__init__.py0
-rw-r--r--assignment_list_pyqt/add_entry_form.py51
-rw-r--r--assignment_list_pyqt/add_entry_form.ui185
-rw-r--r--assignment_list_pyqt/add_group_form.py47
-rw-r--r--assignment_list_pyqt/add_group_form.ui131
-rw-r--r--assignment_list_pyqt/config.py95
-rw-r--r--assignment_list_pyqt/db_sqlite.py493
-rw-r--r--assignment_list_pyqt/edit_entry_form.py83
-rw-r--r--assignment_list_pyqt/edit_group_form.py63
-rw-r--r--assignment_list_pyqt/entry.py100
-rw-r--r--assignment_list_pyqt/globals.py4
-rw-r--r--assignment_list_pyqt/group.py25
-rw-r--r--assignment_list_pyqt/main.py273
-rw-r--r--assignment_list_pyqt/main.ui207
-rw-r--r--assignment_list_pyqt/preferences_dialog.py71
-rw-r--r--assignment_list_pyqt/preferences_dialog.ui128
-rw-r--r--assignment_list_pyqt/rule.py44
-rw-r--r--assignment_list_pyqt/rules_dialog.py128
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_())