summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/__init__.py0
-rw-r--r--src/add_entry_form.py95
-rw-r--r--src/add_group_form.py76
-rw-r--r--src/config.py74
-rw-r--r--src/db_sqlite.py346
-rw-r--r--src/edit_entry_form.py120
-rw-r--r--src/edit_group_form.py89
-rw-r--r--src/entry.py84
-rw-r--r--src/globals.py3
-rw-r--r--src/group.py25
-rw-r--r--src/main.py295
-rw-r--r--src/preferences_dialog.py93
12 files changed, 1300 insertions, 0 deletions
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/__init__.py
diff --git a/src/add_entry_form.py b/src/add_entry_form.py
new file mode 100644
index 0000000..9550108
--- /dev/null
+++ b/src/add_entry_form.py
@@ -0,0 +1,95 @@
+import sys
+from PyQt5.QtWidgets import QApplication, QCheckBox, QDateTimeEdit, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton
+from PyQt5.QtGui import QFont
+from PyQt5.QtCore import QDate, Qt
+from src.entry import Entry
+import src.globals as Globals
+import src.db_sqlite as DB
+
+class addEntryForm(QDialog):
+ def __init__(self, parent):
+ super().__init__()
+ self.initializeUI(parent)
+
+ def initializeUI(self, parent):
+ self.resize(400, 1)
+ self.setWindowTitle("Add Entry")
+ self.displayWidgets(parent)
+ self.exec()
+
+ def displayWidgets(self, parent):
+ entry_form_layout = QFormLayout()
+
+ title = QLabel("Add Entry")
+ title.setFont(QFont("Arial", 18))
+ title.setAlignment(Qt.AlignCenter)
+ entry_form_layout.addRow(title)
+
+ self.new_entry_desc = QLineEdit()
+ entry_form_layout.addRow("Description:", self.new_entry_desc)
+
+ self.due_hbox = QHBoxLayout()
+ self.new_entry_due = QDateTimeEdit(QDate.currentDate())
+ self.new_entry_due.setDisplayFormat("MM/dd/yyyy")
+ self.due_hbox.addWidget(self.new_entry_due)
+ self.new_entry_due_checkbox = QCheckBox()
+ self.new_entry_due_checkbox.setChecked(True)
+ self.due_hbox.addWidget(self.new_entry_due_checkbox)
+ entry_form_layout.addRow("Due Date:", self.due_hbox)
+
+ self.new_entry_due_alt = QLineEdit()
+ entry_form_layout.addRow("Due Date (Alt):", self.new_entry_due_alt)
+
+ self.new_entry_link = QLineEdit() # TODO see if there is a widget specifically for URLs
+ entry_form_layout.addRow("Link:", self.new_entry_link)
+
+ # TODO:
+ # depends
+
+ self.new_entry_color = QLineEdit()
+ entry_form_layout.addRow("Color:", self.new_entry_color)
+
+ self.new_entry_highlight = QLineEdit()
+ entry_form_layout.addRow("Highlight:", self.new_entry_highlight)
+
+ # Submit and cancel buttons
+ buttons_h_box = QHBoxLayout()
+ buttons_h_box.addStretch()
+ close_button = QPushButton("Cancel")
+ close_button.clicked.connect(self.close)
+ buttons_h_box.addWidget(close_button)
+ submit_button = QPushButton("Submit")
+ submit_button.clicked.connect(lambda: self.handleSubmit(parent))
+ buttons_h_box.addWidget(submit_button)
+ buttons_h_box.addStretch()
+
+ entry_form_layout.addRow(buttons_h_box)
+
+ self.setLayout(entry_form_layout)
+
+ def handleSubmit(self, parent):
+ # Check that the new entry is not blank
+ desc_text = self.new_entry_desc.text()
+ due_text = ""
+ if self.new_entry_due_checkbox.isChecked():
+ due_text = self.new_entry_due.date() # due_text is a QDate
+ due_alt_text = self.new_entry_due_alt.text()
+ link_text = self.new_entry_link.text()
+ color_text = self.new_entry_color.text()
+ highlight_text = self.new_entry_highlight.text()
+
+ if not desc_text:
+ QMessageBox.warning(self, "Error Message",
+ "Description cannot be blank",
+ QMessageBox.Close,
+ QMessageBox.Close)
+ return
+
+ new_id = DB.insertEntry(Entry(0, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text))
+ Globals.entries.append(Entry(new_id, parent, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text))
+ self.close()
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = addEntryForm()
+ sys.exit(app.exec_())
diff --git a/src/add_group_form.py b/src/add_group_form.py
new file mode 100644
index 0000000..b7144d0
--- /dev/null
+++ b/src/add_group_form.py
@@ -0,0 +1,76 @@
+import sys
+from PyQt5.QtWidgets import QApplication, QComboBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton
+from PyQt5.QtGui import QFont
+from PyQt5.QtCore import Qt
+
+import src.globals as Globals
+from src.group import Group
+import src.db_sqlite as DB
+
+class addGroupForm(QDialog):
+ """
+ Implemented so that it can be used for adding and editing groups
+ """
+ def __init__(self):
+ super().__init__()
+ self.initializeUI()
+
+ def initializeUI(self):
+ self.resize(400, 1)
+ self.setWindowTitle("Add Group")
+ self.displayWidgets()
+ self.exec()
+
+ def displayWidgets(self):
+ group_form_layout = QFormLayout()
+
+ title = QLabel("Add Group")
+ title.setFont(QFont("Arial", 18))
+ title.setAlignment(Qt.AlignCenter)
+ group_form_layout.addRow(title)
+
+ self.new_group_name = QLineEdit()
+ group_form_layout.addRow("Name:", self.new_group_name)
+
+ self.new_group_column = QComboBox()
+ self.new_group_column.addItems(["Left", "Right"])
+ group_form_layout.addRow("Column:", self.new_group_column)
+
+ self.new_group_link = QLineEdit() # TODO see if there is a widget specifically for URLs
+ group_form_layout.addRow("Link:", self.new_group_link)
+
+ # Submit and cancel buttons
+ buttons_h_box = QHBoxLayout()
+ buttons_h_box.addStretch()
+ close_button = QPushButton("Cancel")
+ close_button.clicked.connect(self.close)
+ buttons_h_box.addWidget(close_button)
+ submit_button = QPushButton("Submit")
+ submit_button.clicked.connect(self.handleSubmit)
+ buttons_h_box.addWidget(submit_button)
+ buttons_h_box.addStretch()
+
+ group_form_layout.addRow(buttons_h_box)
+
+ self.setLayout(group_form_layout)
+
+ def handleSubmit(self):
+ name_text = self.new_group_name.text()
+ column_text = self.new_group_column.currentText()
+ link_text = self.new_group_link.text()
+
+ if not name_text:
+ QMessageBox.warning(self, "Error Message",
+ "Name cannot be blank",
+ QMessageBox.Close,
+ QMessageBox.Close)
+ return
+
+ new_id = DB.insertGroup(Group(0, name_text, column_text, link_text))
+ Globals.groups.append(Group(new_id, name_text, column_text, link_text))
+ self.close()
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = addGroupForm()
+ sys.exit(app.exec_())
diff --git a/src/config.py b/src/config.py
new file mode 100644
index 0000000..2c80716
--- /dev/null
+++ b/src/config.py
@@ -0,0 +1,74 @@
+"""
+Handle reading of the config file, which on POSIX-compliant systems will be
+created in ~/.config/assignment-list-pyqt5/config.py
+"""
+
+import configparser
+import os
+import sys
+
+import src.globals as Globals
+
+class Config():
+ def __init__(self):
+ self.config_path = os.path.join(
+ os.path.expanduser("~"),
+ ".config",
+ "assignment-list-pyqt5",
+ "config")
+
+ if not os.path.exists(self.config_path):
+ self.createConfig()
+
+ self.loadConfig()
+
+ 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()
+ self.config["paths"] = {
+ "db_path": os.path.join(
+ os.path.expanduser("~"),
+ ".local",
+ "share",
+ "assignment-list-pyqt5",
+ "data.db"
+ )
+ }
+
+ self.updateConfig()
+
+ def updateConfig(self):
+ """
+ Update the configuration file with values from self.config
+ """
+ # Attempt to create directory if necessary
+ if not os.path.exists(os.path.dirname(self.config_path)):
+ try:
+ os.mkdir(os.path.dirname(self.config_path))
+ except:
+ print("Error: Could not create config directory '{}'".format(os.path.dirname(self.config_path)))
+ sys.exit(1)
+
+ # Attempt to write to file
+ try:
+ with open(self.config_path, 'w') as configfile:
+ self.config.write(configfile)
+ except:
+ print("Error: Could not open config file '{}'".format(self.config_path))
+ sys.exit(1)
+
+ print("Successfully created config at {}".format(self.config_path))
+
+if __name__ == "__main__":
+ Config()
diff --git a/src/db_sqlite.py b/src/db_sqlite.py
new file mode 100644
index 0000000..88a64c2
--- /dev/null
+++ b/src/db_sqlite.py
@@ -0,0 +1,346 @@
+import os
+import sys
+from time import strptime
+from PyQt5.QtCore import QDate
+from PyQt5.QtSql import QSqlDatabase, QSqlQuery
+import src.globals as Globals
+from src.group import Group
+from src.entry import Entry
+
+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_("""
+ 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
+ )
+ """)
+
+ 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
+ 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
+ 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()))
+
+ 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 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 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 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 hidden entries
+ query.exec_("""
+ DELETE FROM entries WHERE hidden = 1
+ """)
+
+ # Remove hidden groups
+ query.exec_("""
+ DELETE FROM groups WHERE hidden = 1
+ """)
+
+ database.close()
diff --git a/src/edit_entry_form.py b/src/edit_entry_form.py
new file mode 100644
index 0000000..55c388c
--- /dev/null
+++ b/src/edit_entry_form.py
@@ -0,0 +1,120 @@
+import sys
+from PyQt5.QtWidgets import QApplication, QCheckBox, QDateTimeEdit, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton
+from PyQt5.QtGui import QFont
+from PyQt5.QtCore import QDate, Qt
+
+import src.globals as Globals
+from src.entry import Entry
+import src.db_sqlite as DB
+
+class editEntryForm(QDialog):
+ """
+ Form to edit/update an entry
+ """
+ def __init__(self, id):
+ self.id = id
+ super().__init__()
+ self.initializeUI()
+
+ def initializeUI(self):
+ self.resize(400, 1)
+ self.setWindowTitle("Edit Entry")
+ self.displayWidgets()
+ self.exec()
+
+ def displayWidgets(self):
+ entry_form_layout = QFormLayout()
+ entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0]
+
+ title = QLabel("Edit Entry")
+ title.setFont(QFont("Arial", 18))
+ title.setAlignment(Qt.AlignCenter)
+ entry_form_layout.addRow(title)
+
+ self.entry_desc = QLineEdit()
+ self.entry_desc.setText(entry.desc)
+ entry_form_layout.addRow("Description:", self.entry_desc)
+
+ self.due_hbox = QHBoxLayout()
+ self.entry_due = QDateTimeEdit(QDate.currentDate())
+ self.entry_due.setDisplayFormat("MM/dd/yyyy")
+ if entry.due:
+ self.entry_due.setDate(entry.due)
+ self.due_hbox.addWidget(self.entry_due)
+ self.entry_due_checkbox = QCheckBox()
+ if entry.due:
+ self.entry_due_checkbox.setChecked(True)
+ else:
+ self.entry_due_checkbox.setChecked(False)
+ self.due_hbox.addWidget(self.entry_due_checkbox)
+ entry_form_layout.addRow("Due Date:", self.due_hbox)
+
+ self.entry_due_alt = QLineEdit()
+ self.entry_due_alt.setText(entry.due_alt)
+ entry_form_layout.addRow("Due Date (Alt):", self.entry_due_alt)
+
+ self.entry_link = QLineEdit() # TODO see if there is a widget specifically for URLs
+ self.entry_link.setText(entry.link)
+ entry_form_layout.addRow("Link:", self.entry_link)
+
+ self.entry_color = QLineEdit()
+ self.entry_color.setText(entry.color)
+ entry_form_layout.addRow("Color:", self.entry_color)
+
+ self.entry_highlight = QLineEdit()
+ self.entry_highlight.setText(entry.highlight)
+ entry_form_layout.addRow("Highlight:", self.entry_highlight)
+
+ # Submit and cancel buttons
+ buttons_h_box = QHBoxLayout()
+ buttons_h_box.addStretch()
+ close_button = QPushButton("Cancel")
+ close_button.clicked.connect(self.close)
+ buttons_h_box.addWidget(close_button)
+ submit_button = QPushButton("Submit")
+ submit_button.clicked.connect(self.handleSubmit)
+ buttons_h_box.addWidget(submit_button)
+ buttons_h_box.addStretch()
+
+ entry_form_layout.addRow(buttons_h_box)
+
+ self.setLayout(entry_form_layout)
+
+ def handleSubmit(self):
+ desc_text = self.entry_desc.text()
+ if self.entry_due_checkbox.isChecked():
+ due_text = self.entry_due.date() # due_text is a QDate
+ else:
+ due_text = "" # due is unchecked
+ due_alt_text = self.entry_due_alt.text()
+ link_text = self.entry_link.text()
+ color_text = self.entry_color.text()
+ highlight_text = self.entry_highlight.text()
+
+ if not desc_text:
+ QMessageBox.warning(self, "Error Message",
+ "Name cannot be blank",
+ QMessageBox.Close,
+ QMessageBox.Close)
+ return
+
+ # Update DB
+ entry = list(filter(lambda e: e.id == self.id, Globals.entries))[0]
+ entry.desc = desc_text
+ entry.due = due_text
+ entry.due_alt = due_alt_text
+ entry.link = link_text
+ entry.color = color_text
+ entry.highlight = highlight_text
+ DB.updateEntry(entry)
+
+ # Update global variables
+ Globals.entries = list(filter(lambda e: e.id != self.id, Globals.entries))
+ Globals.entries.append(Entry(self.id, entry.parent_id, desc_text, due_text, due_alt_text, link_text, color_text, highlight_text, entry.done, entry.hidden))
+ self.close()
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = editEntryForm()
+ sys.exit(app.exec_())
+
diff --git a/src/edit_group_form.py b/src/edit_group_form.py
new file mode 100644
index 0000000..c4a8622
--- /dev/null
+++ b/src/edit_group_form.py
@@ -0,0 +1,89 @@
+import sys
+from PyQt5.QtWidgets import QApplication, QComboBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton
+from PyQt5.QtGui import QFont
+from PyQt5.QtCore import Qt
+
+import src.globals as Globals
+from src.group import Group
+import src.db_sqlite as DB
+
+class editGroupForm(QDialog):
+ """
+ Form to edit/update a group
+ """
+ def __init__(self, id):
+ self.id = id
+ super().__init__()
+ self.initializeUI()
+
+ def initializeUI(self):
+ self.resize(400, 1)
+ self.setWindowTitle("Edit Group")
+ self.displayWidgets()
+ self.exec()
+
+ def displayWidgets(self):
+ group_form_layout = QFormLayout()
+ group = list(filter(lambda g: g.id == self.id, Globals.groups))[0]
+
+ title = QLabel("Edit Group")
+ title.setFont(QFont("Arial", 18))
+ title.setAlignment(Qt.AlignCenter)
+ group_form_layout.addRow(title)
+
+ self.group_name = QLineEdit()
+ self.group_name.setText(group.name)
+ group_form_layout.addRow("Name:", self.group_name)
+
+ self.group_column = QComboBox()
+ self.group_column.addItems(["Left", "Right"])
+ self.group_column.setCurrentIndex(0 if group.column.lower() == "left" else 1)
+ group_form_layout.addRow("Column:", self.group_column)
+
+ self.group_link = QLineEdit() # TODO see if there is a widget specifically for URLs
+ self.group_link.setText(group.link)
+ group_form_layout.addRow("Link:", self.group_link)
+
+ # Submit and cancel buttons
+ buttons_h_box = QHBoxLayout()
+ buttons_h_box.addStretch()
+ close_button = QPushButton("Cancel")
+ close_button.clicked.connect(self.close)
+ buttons_h_box.addWidget(close_button)
+ submit_button = QPushButton("Submit")
+ submit_button.clicked.connect(self.handleSubmit)
+ buttons_h_box.addWidget(submit_button)
+ buttons_h_box.addStretch()
+
+ group_form_layout.addRow(buttons_h_box)
+
+ self.setLayout(group_form_layout)
+
+ def handleSubmit(self):
+ name_text = self.group_name.text()
+ column_text = self.group_column.currentText()
+ link_text = self.group_link.text()
+
+ if not name_text:
+ QMessageBox.warning(self, "Error Message",
+ "Name cannot be blank",
+ QMessageBox.Close,
+ QMessageBox.Close)
+ return
+
+ # Update DB
+ group = list(filter(lambda g: g.id == self.id, Globals.groups))[0]
+ group.name = name_text
+ group.column = column_text
+ group.link = link_text
+ DB.updateGroup(group)
+
+ # Update global variables
+ Globals.groups = list(filter(lambda g: g.id != self.id, Globals.groups))
+ Globals.groups.append(Group(self.id, name_text, column_text, link_text, group.hidden))
+ self.close()
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = editGroupForm()
+ sys.exit(app.exec_())
diff --git a/src/entry.py b/src/entry.py
new file mode 100644
index 0000000..addc96a
--- /dev/null
+++ b/src/entry.py
@@ -0,0 +1,84 @@
+from datetime import date, datetime
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QFont
+from PyQt5.QtWidgets import QHBoxLayout, QLabel
+
+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()
+
+ bullet.setFont(QFont("Arial", 11))
+
+ body.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
+ body.setFont(QFont("Arial", 11))
+ body.setWordWrap(True)
+ body.setToolTip("Right-Click for actions")
+
+ if self.done:
+ bullet.setText("\u2713 ")
+ bullet.setStyleSheet("""
+ QLabel{
+ 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()
+ ))
+ 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)
+
+ output.addStretch()
+
+ if self.done:
+ font = body.font()
+ font.setStrikeOut(True)
+ body.setFont(font)
+ body.setStyleSheet("""
+ QLabel{
+ color: green;
+ }
+ """)
+
+ else:
+ body.setStyleSheet("""
+ QLabel{{
+ color: {0};
+ background-color: {1};
+ font-weight: {2};
+ }}""".format(
+ self.color if self.color else "default",
+ self.highlight if self.highlight else "none",
+ "bold" if self.due and self.due <= date.today() else "normal"
+ ))
+
+ return output
diff --git a/src/globals.py b/src/globals.py
new file mode 100644
index 0000000..61c6156
--- /dev/null
+++ b/src/globals.py
@@ -0,0 +1,3 @@
+groups = []
+entries = []
+db_path = "./test.db"
diff --git a/src/group.py b/src/group.py
new file mode 100644
index 0000000..18b4836
--- /dev/null
+++ b/src/group.py
@@ -0,0 +1,25 @@
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QFont
+from PyQt5.QtWidgets import QLabel, QVBoxLayout
+import src.globals as Globals
+
+class Group:
+ def __init__(self, id, name, column = "left", link = "", hidden = False):
+ self.id = id
+ self.name = name
+ self.column = column
+ self.link = link
+ self.hidden = hidden
+
+ def buildLayout(self):
+ output = QVBoxLayout()
+ output.setContentsMargins(0, 10, 0, 10)
+
+ name = QLabel(self.name)
+ name.setTextInteractionFlags(Qt.TextSelectableByMouse)
+ name_font = QFont("Arial", 13)
+ name_font.setUnderline(True)
+ name.setFont(name_font)
+ output.addWidget(name)
+
+ return output
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..dfdf8a7
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,295 @@
+#!/usr/bin/python3
+import sys
+import time
+from PyQt5.QtWidgets import QAction, QApplication, QGridLayout, QHBoxLayout, QLabel, QMainWindow, QMenu, QMessageBox, QScrollArea, QToolBar, QVBoxLayout, QWidget
+from PyQt5.QtGui import QCursor, QFont
+from PyQt5.QtCore import QDate, Qt
+from src.config import Config
+from src.preferences_dialog import PreferencesDialog
+from src.add_group_form import addGroupForm
+from src.edit_group_form import editGroupForm
+from src.add_entry_form import addEntryForm
+from src.edit_entry_form import editEntryForm
+import src.globals as Globals
+import src.db_sqlite as DB
+
+class AssignmentList(QMainWindow):
+ def __init__(self):
+ super().__init__()
+
+ self.initializeUI()
+
+ def initializeUI(self):
+ self.resize(640, 480)
+ self.setWindowTitle("Assignment List")
+ self.createMenu()
+ self.createToolbar()
+ Config()
+ self.setupDB()
+ self.displayWidgets()
+ self.show()
+
+ def createMenu(self):
+ menu_bar = self.menuBar()
+ file_menu = menu_bar.addMenu("File")
+ edit_menu = menu_bar.addMenu("Edit")
+ help_menu = menu_bar.addMenu("Help")
+
+ self.preferences_act = QAction("Preferences", self)
+ self.preferences_act.setShortcut("Alt+Return")
+ self.preferences_act.triggered.connect(PreferencesDialog)
+ file_menu.addAction(self.preferences_act)
+ # TODO implement reload of DB that works
+ self.reload_act = QAction("Reload [WIP]", self)
+ self.reload_act.setShortcut("F5")
+ #self.reload_act.triggered.connect(self.reload)
+ file_menu.addAction(self.reload_act)
+ file_menu.addSeparator()
+ exit_act = QAction("Exit", self)
+ exit_act.setShortcut("Ctrl+Q")
+ exit_act.triggered.connect(self.close)
+ file_menu.addAction(exit_act)
+
+ self.add_group_act = QAction("Add Group", self)
+ self.add_group_act.triggered.connect(self.addGroup)
+ edit_menu.addAction(self.add_group_act)
+ edit_menu.addSeparator()
+ self.clean_hidden_act = QAction("Permanently Delete Removed Groups and Entries", self)
+ self.clean_hidden_act.triggered.connect(self.cleanHidden)
+ edit_menu.addAction(self.clean_hidden_act)
+
+ about_act = QAction("About", self)
+ about_act.triggered.connect(self.aboutDialog)
+ help_menu.addAction(about_act)
+
+ def createToolbar(self):
+ tool_bar = QToolBar("Toolbar")
+ self.addToolBar(tool_bar)
+
+ tool_bar.addAction(self.add_group_act)
+
+ def setupDB(self):
+ DB.initDB()
+
+ def displayWidgets(self):
+ main_widget_scroll_area = QScrollArea(self)
+ main_widget_scroll_area.setWidgetResizable(True)
+ main_widget = QWidget()
+ self.setCentralWidget(main_widget_scroll_area)
+
+ title = QLabel(time.strftime("%A, %b %d %Y"))
+ title.setFont(QFont("Arial", 17))
+ title.setTextInteractionFlags(Qt.TextSelectableByMouse)
+
+ title_h_box = QHBoxLayout()
+ title_h_box.addStretch()
+ title_h_box.addWidget(title)
+ title_h_box.addStretch()
+
+ self.groups_layout = QGridLayout()
+ self.groups_layout.setContentsMargins(20, 5, 20, 5)
+ self.drawGroups()
+
+ v_box = QVBoxLayout()
+ v_box.addLayout(title_h_box)
+ v_box.addLayout(self.groups_layout)
+ v_box.addStretch()
+
+ main_widget.setLayout(v_box)
+ main_widget_scroll_area.setWidget(main_widget)
+
+ 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 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)
+
+ 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 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))
+
+ # 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 aboutDialog(self):
+ QMessageBox.about(self, "About Assignment List",
+ "Created by Louie S. - 2023")
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = AssignmentList()
+ sys.exit(app.exec_())
+
diff --git a/src/preferences_dialog.py b/src/preferences_dialog.py
new file mode 100644
index 0000000..e8292d6
--- /dev/null
+++ b/src/preferences_dialog.py
@@ -0,0 +1,93 @@
+import sys
+from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QTabWidget, QVBoxLayout, QWidget
+from src.config import Config
+
+class PreferencesDialog(QDialog):
+ """
+ Implemented to set configuration options in the program
+ """
+ def __init__(self):
+ super().__init__()
+
+ # class globals
+ self.config = Config()
+
+ self.initializeUI()
+
+ def initializeUI(self):
+ self.resize(500, 320)
+ self.setWindowTitle("Preferences")
+ self.displayWidgets()
+ self.exec()
+
+ def displayWidgets(self):
+ # TODO make this a scrollable window
+ # FIXME could use some work on sizing
+ main_layout = QVBoxLayout()
+ tab_bar = QTabWidget(self)
+ paths_tab = self.pathsTabLayout()
+
+ tab_bar.addTab(paths_tab, "Paths")
+ main_layout.addWidget(tab_bar)
+ main_layout.addStretch()
+
+ buttons_hbox = QHBoxLayout()
+ buttons_hbox.addStretch()
+
+ close_button = QPushButton("Close", self)
+ close_button.clicked.connect(self.close)
+ buttons_hbox.addWidget(close_button)
+
+ apply_button = QPushButton("Apply", self)
+ apply_button.clicked.connect(self.apply)
+ buttons_hbox.addWidget(apply_button)
+
+ main_layout.addLayout(buttons_hbox)
+ self.setLayout(main_layout)
+
+ def pathsTabLayout(self):
+ output = QWidget()
+ output_layout = QFormLayout()
+
+ # Dialog for setting the database file path
+ db_path_hbox = QHBoxLayout()
+ self.db_path_edit = QLineEdit()
+ if "paths" in self.config.config:
+ self.db_path_edit.setText(self.config.config["paths"]["db_path"])
+ db_path_hbox.addWidget(self.db_path_edit)
+ db_path_button = QPushButton("...")
+ db_path_button.setMaximumWidth(25)
+ db_path_button.clicked.connect(self.dbPathDialog)
+ db_path_hbox.addWidget(db_path_button)
+ output_layout.addRow("Database File:", db_path_hbox)
+
+ output.setLayout(output_layout)
+ return output
+
+ 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()
+
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = PreferencesDialog()
+ sys.exit(app.exec_())