From 604b485fab100785c4def10dc7242e6408e1f1c6 Mon Sep 17 00:00:00 2001 From: Louie S Date: Sat, 16 Sep 2023 09:36:14 -0400 Subject: Create project using setuptools --- add_entry_form.py | 95 ------- add_group_form.py | 76 ------ assignment-list | 10 + assignment-list.py | 295 --------------------- assignment_list.egg-info/PKG-INFO | 10 + assignment_list.egg-info/SOURCES.txt | 20 ++ assignment_list.egg-info/dependency_links.txt | 1 + assignment_list.egg-info/requires.txt | 1 + assignment_list.egg-info/top_level.txt | 1 + config.py | 74 ------ db_sqlite.py | 346 ------------------------- dist/assignment-list-0.0.1.linux-x86_64.tar.gz | Bin 0 -> 22515 bytes dist/assignment_list-0.0.1-py3.6.egg | Bin 0 -> 35127 bytes edit_entry_form.py | 120 --------- edit_group_form.py | 89 ------- entry.py | 84 ------ files.txt | 2 + globals.py | 3 - group.py | 25 -- preferences_dialog.py | 93 ------- pyproject.toml | 3 + setup.cfg | 12 + setup.py | 3 + src/__init__.py | 0 src/add_entry_form.py | 95 +++++++ src/add_group_form.py | 76 ++++++ src/config.py | 74 ++++++ src/db_sqlite.py | 346 +++++++++++++++++++++++++ src/edit_entry_form.py | 120 +++++++++ src/edit_group_form.py | 89 +++++++ src/entry.py | 84 ++++++ src/globals.py | 3 + src/group.py | 25 ++ src/main.py | 295 +++++++++++++++++++++ src/preferences_dialog.py | 93 +++++++ 35 files changed, 1363 insertions(+), 1300 deletions(-) delete mode 100644 add_entry_form.py delete mode 100644 add_group_form.py create mode 100644 assignment-list delete mode 100755 assignment-list.py create mode 100644 assignment_list.egg-info/PKG-INFO create mode 100644 assignment_list.egg-info/SOURCES.txt create mode 100644 assignment_list.egg-info/dependency_links.txt create mode 100644 assignment_list.egg-info/requires.txt create mode 100644 assignment_list.egg-info/top_level.txt delete mode 100644 config.py delete mode 100644 db_sqlite.py create mode 100644 dist/assignment-list-0.0.1.linux-x86_64.tar.gz create mode 100644 dist/assignment_list-0.0.1-py3.6.egg delete mode 100644 edit_entry_form.py delete mode 100644 edit_group_form.py delete mode 100644 entry.py create mode 100644 files.txt delete mode 100644 globals.py delete mode 100644 group.py delete mode 100644 preferences_dialog.py create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/__init__.py create mode 100644 src/add_entry_form.py create mode 100644 src/add_group_form.py create mode 100644 src/config.py create mode 100644 src/db_sqlite.py create mode 100644 src/edit_entry_form.py create mode 100644 src/edit_group_form.py create mode 100644 src/entry.py create mode 100644 src/globals.py create mode 100644 src/group.py create mode 100644 src/main.py create mode 100644 src/preferences_dialog.py diff --git a/add_entry_form.py b/add_entry_form.py deleted file mode 100644 index a5cf7c2..0000000 --- a/add_entry_form.py +++ /dev/null @@ -1,95 +0,0 @@ -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 entry import Entry -Globals = __import__("globals") -DB = __import__("db_sqlite") - -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/add_group_form.py b/add_group_form.py deleted file mode 100644 index f667b66..0000000 --- a/add_group_form.py +++ /dev/null @@ -1,76 +0,0 @@ -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 - -from add_entry_form import Globals -from group import Group -DB = __import__("db_sqlite") - -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/assignment-list b/assignment-list new file mode 100644 index 0000000..cb0af8a --- /dev/null +++ b/assignment-list @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +from __future__ import absolute_import +import sys + +from PyQt5.QtWidgets import QApplication +from src.main import AssignmentList as main + +app = QApplication(sys.argv) +window = main() +sys.exit(app.exec_()) diff --git a/assignment-list.py b/assignment-list.py deleted file mode 100755 index 1219661..0000000 --- a/assignment-list.py +++ /dev/null @@ -1,295 +0,0 @@ -#!/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 config import Config -from preferences_dialog import PreferencesDialog -from add_group_form import addGroupForm -from edit_group_form import editGroupForm -from add_entry_form import addEntryForm -from edit_entry_form import editEntryForm -Globals = __import__("globals") -DB = __import__("db_sqlite") - -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/assignment_list.egg-info/PKG-INFO b/assignment_list.egg-info/PKG-INFO new file mode 100644 index 0000000..363bb92 --- /dev/null +++ b/assignment_list.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: assignment-list +Version: 0.0.1 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: Louie S +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/assignment_list.egg-info/SOURCES.txt b/assignment_list.egg-info/SOURCES.txt new file mode 100644 index 0000000..c9c9420 --- /dev/null +++ b/assignment_list.egg-info/SOURCES.txt @@ -0,0 +1,20 @@ +assignment-list +setup.cfg +setup.py +assignment_list.egg-info/PKG-INFO +assignment_list.egg-info/SOURCES.txt +assignment_list.egg-info/dependency_links.txt +assignment_list.egg-info/requires.txt +assignment_list.egg-info/top_level.txt +src/__init__.py +src/add_entry_form.py +src/add_group_form.py +src/config.py +src/db_sqlite.py +src/edit_entry_form.py +src/edit_group_form.py +src/entry.py +src/globals.py +src/group.py +src/main.py +src/preferences_dialog.py \ No newline at end of file diff --git a/assignment_list.egg-info/dependency_links.txt b/assignment_list.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/assignment_list.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/assignment_list.egg-info/requires.txt b/assignment_list.egg-info/requires.txt new file mode 100644 index 0000000..2d07ca0 --- /dev/null +++ b/assignment_list.egg-info/requires.txt @@ -0,0 +1 @@ +PyQt5 diff --git a/assignment_list.egg-info/top_level.txt b/assignment_list.egg-info/top_level.txt new file mode 100644 index 0000000..85de9cf --- /dev/null +++ b/assignment_list.egg-info/top_level.txt @@ -0,0 +1 @@ +src diff --git a/config.py b/config.py deleted file mode 100644 index da40ed4..0000000 --- a/config.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Handle reading of the config file, which on POSIX-compliant systems will be -created in ~/.config/assignment-list-pyqt5/config.py -""" - -import configparser -import os -import sys - -Globals = __import__("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/db_sqlite.py b/db_sqlite.py deleted file mode 100644 index af7f185..0000000 --- a/db_sqlite.py +++ /dev/null @@ -1,346 +0,0 @@ -import os -import sys -from time import strptime -from PyQt5.QtCore import QDate -from PyQt5.QtSql import QSqlDatabase, QSqlQuery -Globals = __import__("globals") -from group import Group -from 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/dist/assignment-list-0.0.1.linux-x86_64.tar.gz b/dist/assignment-list-0.0.1.linux-x86_64.tar.gz new file mode 100644 index 0000000..f93884c Binary files /dev/null and b/dist/assignment-list-0.0.1.linux-x86_64.tar.gz differ diff --git a/dist/assignment_list-0.0.1-py3.6.egg b/dist/assignment_list-0.0.1-py3.6.egg new file mode 100644 index 0000000..b339413 Binary files /dev/null and b/dist/assignment_list-0.0.1-py3.6.egg differ diff --git a/edit_entry_form.py b/edit_entry_form.py deleted file mode 100644 index 3e3503b..0000000 --- a/edit_entry_form.py +++ /dev/null @@ -1,120 +0,0 @@ -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 - -Globals = __import__("globals") -from entry import Entry -DB = __import__("db_sqlite") - -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/edit_group_form.py b/edit_group_form.py deleted file mode 100644 index faa676c..0000000 --- a/edit_group_form.py +++ /dev/null @@ -1,89 +0,0 @@ -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 - -Globals = __import__("globals") -from group import Group -DB = __import__("db_sqlite") - -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/entry.py b/entry.py deleted file mode 100644 index addc96a..0000000 --- a/entry.py +++ /dev/null @@ -1,84 +0,0 @@ -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() + "".format( - self.link, - self.color if self.color else "default" - )) - body.setText(body.text() + self.desc) - if self.link: - body.setText(body.text() + "") - body.setToolTip("{}".format(self.link)) - output.addWidget(body) - - 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/files.txt b/files.txt new file mode 100644 index 0000000..c04aaa4 --- /dev/null +++ b/files.txt @@ -0,0 +1,2 @@ +/usr/local/lib/python3.6/site-packages/assignment_list-0.0.1-py3.6.egg +/usr/local/bin/assignment-list diff --git a/globals.py b/globals.py deleted file mode 100644 index 61c6156..0000000 --- a/globals.py +++ /dev/null @@ -1,3 +0,0 @@ -groups = [] -entries = [] -db_path = "./test.db" diff --git a/group.py b/group.py deleted file mode 100644 index 372a38e..0000000 --- a/group.py +++ /dev/null @@ -1,25 +0,0 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QLabel, QVBoxLayout -Globals = __import__("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/preferences_dialog.py b/preferences_dialog.py deleted file mode 100644 index df192ba..0000000 --- a/preferences_dialog.py +++ /dev/null @@ -1,93 +0,0 @@ -import sys -from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QFormLayout, QHBoxLayout, QLineEdit, QPushButton, QTabWidget, QVBoxLayout, QWidget -from 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_()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7b08ac0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[metadata] +name = assignment-list +version = 0.0.1 +author = Louie S + +[options] +packages = + src +scripts = + assignment-list +install_requires = + PyQt5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 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() + "".format( + self.link, + self.color if self.color else "default" + )) + body.setText(body.text() + self.desc) + if self.link: + body.setText(body.text() + "") + body.setToolTip("{}".format(self.link)) + output.addWidget(body) + + 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_()) -- cgit