From 791a5bc4b214863df679870354f6ef0237f6a6bc Mon Sep 17 00:00:00 2001 From: Chris Punches Date: Sun, 23 Jul 2017 07:07:00 -0400 Subject: [PATCH] Surro Industries presents Ivory Tower Pre-Alpha --- .gitignore | 1 + Ivory-Tower | 765 ++++++++++++++++++++++++++++++++++++++++++++++++++++ default.db | Bin 0 -> 81920 bytes 3 files changed, 766 insertions(+) create mode 100644 .gitignore create mode 100644 Ivory-Tower create mode 100644 default.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/Ivory-Tower b/Ivory-Tower new file mode 100644 index 0000000..59b7024 --- /dev/null +++ b/Ivory-Tower @@ -0,0 +1,765 @@ +#!/usr/bin/python3 + +# Systems Dependencies +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk +import sqlite3 as lite + + +# This is a UI component that is used by UI_Task +# Should only be a stack switch for either a tab or a page view +# Important to remember that this handles both states (editable vs. non-editable) for both page and tab. +class PageStack(Gtk.EventBox): + def __init__(self, parent, parentNotebook): + Gtk.EventBox.__init__(self) + + self.ctrl_parent = parent + self.parentNotebook = parentNotebook + + self.db_task = self.ctrl_parent.db_task + + # Create the stack + self.stack = Gtk.Stack() + + # Create the view label with the textvalue supplied during instantiation + self.viewLabel = Gtk.Label(parent.db_task.description) + # Give it line wrap just because it's pretty + self.viewLabel.set_line_wrap(True) + self.viewLabel.set_halign(True) + self.viewLabel.set_padding(6, 6) + + self.viewLabel.show() + + # Create the view area to switch to + self.viewArea = Gtk.HBox(spacing=6, orientation=Gtk.Orientation.VERTICAL) + # Create the edit area to switch to + self.editArea = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + + # Create an edit widget for a page + self.editField = Gtk.TextView() + self.editField.set_wrap_mode(2) + self.editField.connect("key-press-event", self.returnPressed, self.stack, self.editField, self.viewLabel, self.db_task) + self.editField.set_left_margin(6) + + # Add the edit field to the edit area + self.editArea.pack_start(self.editField, False, True, 0) + self.editArea.show_all() + + # Add the edit area to this stack type + self.stack.add_titled(self.editArea, 'editArea', "Edit Area") + + # Add view label to the view area + self.viewArea.pack_start(self.viewLabel, False, True, 0) + + # Add the view area to this stack type + self.stack.add_titled(self.viewArea, 'viewArea', "View Area") + self.stack.show_all() + + # View Area is the default view + self.stack.set_visible_child(self.viewArea) + + self.add(self.stack) + #self.set_above_child(False) + #self.set_visible_window(True) + self.show() + self.show_all() + + # generate the contextual menu for this page + pageMenu = self.gen_context_menu() + self.connect_object("event", self.ContextMenu, pageMenu) + + def getParentTaskLoaderID(self): + return self.parentNotebook.taskID + + # page_context_menu(): + # Generates a contextual menu for a page. + # + # return: the Gtk.Menu contextual menu + def gen_context_menu(self): + menu = Gtk.Menu() + + itemAddPeer = Gtk.MenuItem("Add a new item to this task.") + itemAddPeer.connect("activate", self.addPeer_menuclicked) + + menu.append(itemAddPeer) + + + itemRename = Gtk.MenuItem("Rename this task...") + itemRename.connect("activate", self.pageEditMenuClicked) + itemAddAction = Gtk.MenuItem("Add a child task...") + itemAddAction.connect("activate", self.addChild_menuclicked, self.ctrl_parent, self.db_task) + itemCompleteAction = Gtk.MenuItem("Mark as Complete") + itemCompleteAction.connect("activate", self.itemCompletionClicked) + + menu.append(itemRename) + menu.append(itemAddAction) + menu.append(itemCompleteAction) + + menu.show_all() + return menu + +# def enterPressedOnEntryWidget(self, widget, stack, label, entry, area, task): +# self.viewLabel.set_text(entry.get_text()) +# self.stack.set_visible_child(area) +# dbhandle = self.get_parent().get_parent().dbhandle +# dbhandle.updateTaskDescription(task, entry.get_text()) + + + def itemCompletionClicked(self, widget): + self.parentNotebook.dbhandle.updateTaskStatus(self.db_task, True ) + self.parentNotebook.remove_page(self.parentNotebook.page_num(self.parentNotebook.get_current_page_container())) + + + def addPeer_menuclicked(self, widget): + # This is handled by the ctrl_parent TaskFactory since a peer is being created. + newDB_Task = self.ctrl_parent.dbhandle.generate_new_task(parent_taskID=self.ctrl_parent.db_task.taskID) + newUI_Task = UI_Task(newDB_Task, self.ctrl_parent) + self.ctrl_parent.add_UI_Task(newUI_Task) + + def addChild_menuclicked(self, widget): + task = self.ctrl_parent.dbhandle.generate_new_task(self.ctrl_parent.db_task.taskID) + if not hasattr(self.ctrl_parent, 'childTaskBook'): + self.parentNotebook.add_child_TaskBook(self, self.parentNotebook.get_current_page_container()) + self.ctrl_parent.add_childTask(task) + + def ContextMenu(self, widget, event): + if event.type == Gdk.EventType.BUTTON_PRESS and event.get_button().button == 3: + widget.popup(None, None, None, None, event.get_button().button, event.time) + + def editMenuClicked(self, widget, parentNotebook): + # parentNotebook.toggleCurrentTabStackArea() + #win = EditWindow() + self.stack.set_visible_child(self.editArea) + + def returnPressed(self, widget, event, stack, entry, label, db_task): + #print(Gdk.keyval_name(event.keyval)) + if Gdk.keyval_name(event.keyval) == 'KP_Enter': + buffer = entry.get_buffer() + label.set_text(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)) + + self.stack.set_visible_child(self.viewArea) + dbhandle = self.ctrl_parent.dbhandle + dbhandle.updateTaskDescription(db_task, buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)) + + def pageEditMenuClicked(self, widget): + self.stack.set_visible_child(self.editArea) + + +# This is a UI component that is used by UI_Task +# Should only be a stack switch for either a tab or a page view +# Important to remember that this handles both states (editable vs. non-editable) for both page and tab. +class TabStack(Gtk.EventBox): + def __init__(self, parent, parentNoteBook): + Gtk.EventBox.__init__(self) + + # bind the ctrl_parent + self.ctrl_parent = parent + self.parentNotebook = parentNoteBook + + # bind the ctrl_parent's task + self.db_task = self.ctrl_parent.db_task + + # Create the stack + self.stack = Gtk.Stack() + + # Create the view label with the textvalue supplied during instantiation + self.viewLabel = Gtk.Label(self.db_task.title) + + # Give it line wrap just because it's pretty + self.viewLabel.set_line_wrap(True) + self.viewLabel.set_halign(True) + self.viewLabel.set_padding(6, 6) + + # Make it visible + self.viewLabel.show() + + # Create the Switch areas. + # Create the view area to switch to + self.viewArea = Gtk.HBox() + # Create the edit area to switch to + self.editArea = Gtk.Box() + + # it's a tab so use GTK.Entry() + self.editField = Gtk.Entry() + + # connect the edit field to the enterPressedOnEntryWidget event handler, tell it the label and edit field, and pass the viewArea + # This call should be where db io and ui changes are hooked together + self.editField.connect("activate", self.enterPressedOnEntryWidget) + # connect the contextual menus to our new stack switches + # generate the contextual menu for this tab + tabMenu = self.gen_context_menu() + self.connect_object("event", self.ContextMenu, tabMenu) + + + # Add the edit field to the edit area + self.editArea.pack_start(self.editField, True, True, 0) + self.editArea.show_all() + + # Add the edit area to this stack type + self.stack.add_titled(self.editArea, 'editArea', "Edit Area") + + # Add view label to the view area + self.viewArea.pack_start(self.viewLabel, False, False, 0) + + # Add the view area to this stack type + self.stack.add_titled(self.viewArea, 'viewArea', "View Area") + self.stack.show_all() + + # View Area is the default view + self.stack.set_visible_child(self.viewArea) + + self.add(self.stack) + #self.set_above_child(False) + #self.set_visible_window(True) + self.show() + self.show_all() + + def getParentTaskLoaderID(self): + return self.parentNotebook.taskID + + # tab_context_menu(): + # Generates a contextual menu for a tab. + # + # db_task: the DB_Task object to take + # parentTaskFactory: the ctrl_parent notebook + # return: the Gtk.Menu contextual menu + def gen_context_menu(self): + menu = Gtk.Menu() + + createTask_MenuItem = Gtk.MenuItem("Create New") + + createTask_MenuItem.connect("activate", self.addPeer_menuclicked) + + menu.append(createTask_MenuItem) + + itemRename = Gtk.MenuItem("Change Title") + itemRename.connect("activate", self.tabEditMenuClicked) + + itemAddAction = Gtk.MenuItem("Split") + itemAddAction.connect("activate", self.addChild_menuclicked) + + itemCompleteAction = Gtk.MenuItem("Mark Complete") + itemCompleteAction.connect("activate", self.itemCompletionClicked) + + menu.append(itemRename) + menu.append(itemAddAction) + menu.append(itemCompleteAction) + + menu.show_all() + return menu + + def itemCompletionClicked(self, widget): + self.parentNotebook.dbhandle.updateTaskStatus(self.db_task, True) + self.parentNotebook.remove_page(self.parentNotebook.page_num(self.parentNotebook.get_current_page_container() )) + + def addChild_menuclicked(self, widget): + task = self.ctrl_parent.dbhandle.generate_new_task(self.ctrl_parent.db_task.taskID) + if not hasattr(self.ctrl_parent, 'childTaskBook'): + self.ctrl_parent.add_child_TaskBook(self.parentNotebook.get_current_page_container()) + self.ctrl_parent.add_childTask(task) + + def addPeer_menuclicked(self, widget): + # This is handled by the ctrl_parent TaskFactory since a peer is being created. + newDB_Task = self.ctrl_parent.dbhandle.generate_new_task(parent_taskID=self.getParentTaskLoaderID()) + newUI_Task = UI_Task(db_task=newDB_Task, parentTaskFactory=self.parentNotebook) + self.parentNotebook.add_UI_Task(newUI_Task) + + + + def tabEditMenuClicked(self, widget): + self.editField.set_text(self.viewLabel.get_text()) + self.stack.set_visible_child(self.editArea) + + # menu event handlers + + + + + def enterPressedOnEntryWidget(self, widget): + # set the label to the value of what is in the Entry field + self.viewLabel.set_text( self.editField.get_text() ) + + # make the label visible again and the Entry field invisible + self.stack.set_visible_child(self.viewArea) + + # get the dbhandle from ctrl_parent + dbhandle = self.ctrl_parent.dbhandle + + # Update the database for the changed task + dbhandle.updateTaskTitle( self.db_task, self.viewLabel.get_text() ) + + + def ContextMenu(self, widget, event): + if event.type == Gdk.EventType.BUTTON_PRESS and event.get_button().button == 3: + widget.popup(None, None, None, None, event.get_button().button, event.time) + + def editMenuClicked(self, widget): + # parentNotebook.toggleCurrentTabStackArea() + #win = EditWindow() + self.stack.set_visible_child(self.editArea) + + + + +# The transition object from DB_Task to UI_Task. A UI task takes in a DB_Task object and then creates a tab and +# corresponding page in the notebook. This object should be seen as an amalgamation of the page and the tab in the +# notebook. + +# This entire class needs reworked next. +class UI_Task(Gtk.Box): + def __init__(self, db_task, parentTaskFactory): + super(Gtk.Box, self).__init__() + + # Aesthetics + self.set_border_width(6) + + # bind the data structure object + self.db_task = db_task + + # bind the ctrl_parent + self.ctrl_parent = parentTaskFactory + + self.dbhandle = self.ctrl_parent.dbhandle + + # determine if we have children + self.has_children = self.ctrl_parent.dbhandle.has_children(self.db_task) + + + + + # create the stack switch for the tab + self.tabDisplay = TabStack(self, self.ctrl_parent) + #self.tabDisplay.connect_object("event", self.tabDisplay.ContextMenu, self.tabDisplay.gen_context_menu() ) + + # and now one for the page + self.pageDisplay = PageStack(self, self.ctrl_parent) + # self.pageDisplay.connect_object("event", self.pageDisplay.ContextMenu, self.pageDisplay.gen_context_menu() ) + + # Add the tabStack to this instance +# self.pack_start(self.tabDisplay, True, True, 6) + self.add(self.tabDisplay) + + # of the visible ones, show all the things + self.show_all() + + + + # add_child_TaskLoader() + # Prepares a task for being able to hold children. + # + # return: None + def add_child_TaskBook(self, page_to_split): + self.childTaskBook = TaskLoader(parent=self, taskID=self.db_task.taskID) + + # Set the tabs on the subnotebook to be on the left. + self.childTaskBook.set_tab_pos(Gtk.PositionType.LEFT) + + # Make the tabs on the subnotebook scrollable and reorderable + self.childTaskBook.set_scrollable(True) + + + # decontext all of this + page_to_split.pack_start(self.childTaskBook, True, True, 6) + page_to_split.show() + + + + # add_childTask(): + # + # db_task: the DB_Task object to take + # parentTaskFactory: the ctrl_parent notebook + # return: the Gtk.Menu contextual menu + def add_childTask(self, db_task): + print("CHILD TASK ID") + print(db_task.taskID) + #if not hasattr(self, 'childTaskBook'): + # self.add_child_TaskBook(self, page_to_split) + + newTask = UI_Task(db_task=db_task, parentTaskFactory=self.childTaskBook) + self.childTaskBook.add_UI_Task(newTask) + self.has_children = True + + +# The notebook object. Populates tasks for a given ID. +# Ultimately a notebook represents a task much like a tab or page. +class TaskLoader(Gtk.Notebook): + def __init__(self, parent, taskID): + Gtk.Notebook.__init__(self) + + # attach the taskID + self.taskID = taskID + + # attach the supplied dbhandle from the ctrl_parent to this for convenient referencing + self.dbhandle = parent.dbhandle + + # Set the tabs on the notebook to be on the left. + self.set_tab_pos(Gtk.PositionType.LEFT) + + # Make the tabs on the notebook scrollable + self.set_scrollable(True) + + #self.connect("page-added", self.event_proto) + + self.show_all() + + # disabled + def event_proto(self, widget, event, data=None): + print("triggered") + if self.taskID !=0: + self.add_all_children() + + def add_UI_Task(self, ui_task): + # Create a box container for the tab + pageContainer = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + pageContainer.set_border_width(10) + + # Add the project description label to the notebook tab + pageContainer.pack_start(ui_task.pageDisplay, False, True, 6) + + # Add the new task to the page of the current TaskFactory instance + self.append_page(pageContainer, ui_task) + + if ui_task.has_children: + ui_task.add_child_TaskBook(pageContainer) + ui_task.childTaskBook.add_all_children(ui_task.db_task.taskID) + + + # show + self.show_all() + + # adds the children of the task to the calling TaskLoader + def add_all_children(self, taskID): + # fetch tasks for this task's taskID + tasks = self.dbhandle.getTaskChildren(taskID, 0) + + for db_task in tasks: + # create a UI_Task object from the DB_Task object who thinks its ctrl_parent is this notebook + ui_task = UI_Task(db_task, self) + + + # add each task belonging to this notebook + self.add_UI_Task(ui_task) + + + + def get_current_page_container(self): + print("CURRENT PAGE") + print(self.get_current_page()) + return self.get_nth_page(self.get_current_page()) + + +# Some static strings for reuse in various places. +class DefaultStrings(): + projectTitle = "New Task Item" + projectDescription = "This is an empty task item." + + allTasksTitle = "All Tasks" + allTasksDescription = "This is the all tasks view. Should display all items whose ctrl_parent = 0 in the sqlite database." + + archiveTasksTitle = "Archived Tasks" + archiveTasksDescription = "These are tasks that have been completed. Items are removed from their place and moved here to reduce clutter and track work already done." + + ApplicationTitle = "Ivory Tower" + ApplicationOwner = "SILO GROUP, LTD." + +# The headerbar for TaskFactoryWindow +class MainHeaderBar(Gtk.HeaderBar): + def __init__(self, Window): + Gtk.HeaderBar.__init__(self) + + self.set_show_close_button(True) + self.props.title = DefaultStrings.ApplicationTitle + self.props.subtitle = DefaultStrings.ApplicationOwner + + self.set_decoration_layout("menu:minimize,close") + self.set_border_width(3) + + Window.set_titlebar(self) + + +# This is the MainWindow class. This is the main window for the application. +class TaskLoaderWindow(Gtk.Window): + def __init__(self, dbhandle): + Gtk.Window.__init__(self, title=DefaultStrings.ApplicationOwner) + # Do the autoattachy things with a new headerbar + self.headerBar = MainHeaderBar(self) + + # create a fixed point of reference for anything in this window to the database IO obj handle + self.dbhandle = dbhandle + + # placeholder taskID for root Window + self.taskID = 0 + + # Attach the notebook to the window. + self.rootTaskFactory = TaskLoader(parent=self, taskID=0) + self.add_root_items() + + # Add the root projects notebook to the ctrl_parent window + self.add(self.rootTaskFactory) + + # bind to the delete-event to Gtk.main_quit + self.connect("delete-event", Gtk.main_quit) + + # show all items + self.show_all() + + + def add_root_items(self): + pass + + def start_TaskLoader(self): + if not self.dbhandle.has_children(0): + newDB_Task = self.dbhandle.generate_new_task(parent_taskID=0) + newUI_Task = UI_Task(db_task=newDB_Task, parentTaskFactory=self) + self.rootTaskFactory.add_UI_Task(newUI_Task) + # add the children for this root notebook + self.rootTaskFactory.add_all_children(0) + +# Class for all direct DB interaction +class DBIO(): + def __init__(self, fileLocation): + self.db_connection = None + + self.db_connection = lite.connect(fileLocation) + + self.cursor = self.db_connection.cursor() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + # Helper for __exit__ + def close(self): + if self.db_connection: + self.db_connection.close() + + def generate_new_task(self, parent_taskID): + taskINPUT = (0, 0, "Default Task", "Default Description") + + task = DB_Task(taskINPUT) + taskID = self.createTask(task, 0) + self.removeAllAssociations(taskID) + task.taskID = taskID + self.setTaskParent(taskID, parent_taskID) + return task + + # runQuery(): + # Runtime safe wrapper for the cursor's execution method. + # + # query: the SQL query + # params: any interpolated values in the query, supplied as a tuple + # return: raw rows as an array of tuples, multidimensional[n][n] (needs deserialized) + def runQuery(self, query, params=None): + try: + if params: + self.cursor.execute(query, params) + else: + self.cursor.execute(query) + self.data = self.cursor.fetchall() + self.db_connection.commit() + + except lite.Error as e: + print('Error: {}'.format(e.args)) + + try: + return self.data + except: + return None + + # has_children(): + # Boolean representing whether or not the supplied task or taskID has child associations + # + # db_task_or_id: status filter for all returned rows + # return: bool whether it has children associated with it + def has_children(self, db_task_or_id): + + if isinstance(db_task_or_id, DB_Task): + this_id = int(db_task_or_id.taskID) + if isinstance(db_task_or_id, int): + this_id = int(db_task_or_id) + if isinstance(db_task_or_id, str): + this_id = int(db_task_or_id) + + num_rows = self.runQuery("SELECT count(*) from nodal_associations WHERE parent_id=?", (this_id,)) + return (num_rows.pop()[0] > 0) + + # getAllTasksbyStatus(): + # Gets all rows of tasks corresp. to the supplied status + # + # status: status filter for all returned rows + # return: DB_Tasks object + def getAllTasksbyStatus(self, status): + rawTasks = self.runQuery("SELECT * from tasks WHERE status == ?", (status,)) + return DB_Tasks(rawTasks) + + # getTaskChildren(): + # Supplies row entries for tasks of a given parentID and status. + # + # parentID: the ID of the task whose children you want to fetch + # status: the status filter for children that are fetched + # return: DB_Tasks object + def getTaskChildren(self, parentID, status): + rawTasks = self.runQuery( + 'select tasks.* from tasks INNER JOIN nodal_associations on tasks.id=nodal_associations.item_id where nodal_associations.parent_id=? and tasks.status=?', + (int(parentID), int(status),)) + return DB_Tasks(rawTasks) + + # setTaskParent(): + # Creates an association between a child node and a ctrl_parent node. + # + # parentID: the ID of the ctrl_parent + # childID: the ID of the child + # return: None + def setTaskParent(self, childID, parentID): + self.runQuery( + "INSERT INTO nodal_associations (item_id, parent_id) VALUES (?, ?)", + ( + int(childID), + int(parentID) + ) + ) + + # unsetTaskParent(): + # removes an association between a ctrl_parent and child + # + # parentID: the ID of the ctrl_parent + # childID: the ID of the child + # return: None + def unsetTaskParent(self, childID, parentID): + self.runQuery( + "DELETE from nodal_associations WHERE item_id=? AND parent_id=?", + ( + int(childID), + int(parentID) + ) + ) + + # removeAllAssociations(): + # Removes ALL associations of a supplied taskID. Handle with care. + # + # taskID: the ID of the task + # return: None + def removeAllAssociations(self, taskID): + self.runQuery("DELETE from nodal_associations WHERE item_id=? OR parent_id=?", (taskID, taskID,)) + + # createTask(): + # adds a new task to the database + # + # task: DB_Task object to pull the data from. Must be deserialized or created in app. This forces compat + # between DB API, UI API and actual data being stored. + # parentID: The ID of the ctrl_parent to make an association with. + # return: the ID of the created task + def createTask(self, task, parentID): + # New tasks are not completed by default. Ensure this. + task.status = 0 + + # Create the task + self.runQuery( + "INSERT INTO tasks (status, title, description) VALUES (?, ?, ?)", + ( + task.status, + task.title, + task.description, + ) + ) + # Fetch the ID of the newly created task + newTaskID = str(self.cursor.lastrowid) + + # Create an association to the new task's initial ctrl_parent. + self.setTaskParent(newTaskID, parentID) + + # Return the ID of the newly created task as in some cases the UI needs to use it, otherwise we'd be using + # one transaction for both task creation and nodal association in this case. + return newTaskID + + # deleteTask(): + # removes a task from the database + # + # task: DB_Task object to remove. Must be deserialized or created in app. This forces compat + # between DB API, UI API and actual data being stored. + # return: None + def deleteTask(self, task): + # Remove the task + self.runQuery("DELETE FROM tasks WHERE id=?", (task.taskID,)) + self.removeAllAssociations(task.id) + + # updateTaskStatus(): + # updates the status of a task in the database + # + # task: DB_Task object to pull the data from. Must be deserialized or created in app. This forces compat + # between DB API, UI API and actual data being stored. + # new_status: 0|1 / Represents completion status. Can be bool. + # return: None + def updateTaskStatus(self, task, new_status): + self.runQuery("UPDATE tasks SET status=? WHERE id=?", (int(new_status), task.taskID)) + + # updateTaskTitle(): + # updates the title of a task in the database + # + # task: DB_Task object to pull the data from. Must be deserialized or created in app. This forces compat + # between DB API, UI API and actual data being stored. + # new_title: The text of the new title. + # return: None + def updateTaskTitle(self, task, new_title): + self.runQuery("UPDATE tasks SET title=? WHERE id=?", (new_title, task.taskID)) + + # updateTaskDescription(): + # updates the title of a task in the database + # + # task: DB_Task object to pull the data from. Must be deserialized or created in app. This forces compat + # between DB API, UI API and actual data being stored. + # new_title: The text of the new title. + # return: None + def updateTaskDescription(self, task, new_description): + self.runQuery("UPDATE tasks SET description=? WHERE id=?", (new_description, task.taskID)) + +# Deserializes a row to an object consumable from modification methods in DBIO and from UI +class DB_Task(): + def __init__(self, QueryResponse): + if len(QueryResponse) is 4: + self.taskID = QueryResponse[0] + self.status = QueryResponse[1] + self.title = QueryResponse[2] + self.description = QueryResponse[3] + else: + print(len(QueryResponse)) + print(QueryResponse) + raise ValueError("The database is corrupted.") + + # Look Mom, you can print a DB_Task + def __str__(self): + return "\n{\n\t'TaskID':\t'%s',\n\t'Status':\t'%s',\n\t'Title':\t'%s',\n\t'Description':\t'%s',\n}" % ( + self.taskID, + self.status, + self.title, + self.description + ) + + def __enter__(self): + return self + +# Creates a list of deserialized DB_Tasks from a query response +class DB_Tasks(list): + def __init__(self, QueryResponse): + super().__init__() + for rawEntry in QueryResponse: + self.append(DB_Task(rawEntry)) + + +# The main loop. +def Main(): + # Add a file selector dialog for this along with a prompt for the AES password. + dbhandle = DBIO('default.db') + + # load the mainwindow with the dbhandle intended for that window and all its objects + win = TaskLoaderWindow(dbhandle) + win.start_TaskLoader() + Gtk.main() + + +if __name__=="__main__": + Main() + diff --git a/default.db b/default.db new file mode 100644 index 0000000000000000000000000000000000000000..24c7d544cf4dfe514c3a6b2ea9babf9874c915cd GIT binary patch literal 81920 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 kN|GP>*q{shd{tu literal 0 HcmV?d00001