Commits

Josh VanderLinden committed c28c00c

Made several changes to the way the XML is structured. I've also made it possible to edit and delete existing tasks.

Comments (0)

Files changed (2)

 from lxml import etree
 from datetime import datetime
 from uuid import uuid4
+import gettext
 import os
 
+gettext.install('treedo')
 USER_HOME = os.path.expanduser('~')
 DEFAULT_PATH = os.path.join(USER_HOME, 'treedo.xml')
 DATE_FORMAT = '%x %X'
 
+P_URGENT = 1
+P_IMPORTANT = 2
+P_NORMAL = 3
+P_LOW = 4
+P_TRIVIAL = 5
+DEFAULT_PRIORITY = P_NORMAL
+
+PRIORITIES = {
+    P_URGENT: _("1 - Urgent"),
+    P_IMPORTANT: _("2 - Important"),
+    P_NORMAL: _("3 - Normal"),
+    P_LOW: _("4 - Low"),
+    P_TRIVIAL: _("5 - Trivial"),
+}
+
+XML_TASK_TREE = 'TaskList'
+
 class Task(object):
 
-    def __init__(self, summary, is_complete=False, priority=3, notes="",
-                 due_date=None, children=None, uuid=None, parent=None, **kwargs):
+    STANDARD = ('summary', 'notes', 'due')
+
+    def __init__(self, summary=None, is_complete=False, priority=3, notes="",
+                 due_date=None, children=None, uuid=None, parent=None):
 
         if uuid is None or uuid.strip() == '':
             uuid = str(uuid4())
 
         self.uuid = uuid
 
-        self.summary = summary
-        self.is_complete = is_complete
-        self.priority = priority
-        self.notes = notes
+        self.summary = summary or ''
+        self.is_complete = is_complete or False
+        self._priority = priority or DEFAULT_PRIORITY
+        self.notes = notes or ''
         self.due_date = due_date
         self.parent = parent
 
             children = []
         self.children = children
 
-        # handle custom attributes
-        self.custom = kwargs
-
     @staticmethod
     def from_xml(node, parent=None):
+
+        def get_value(name):
+            element = node.find(name)
+            if element is not None:
+                value = element.text
+            else:
+                value = None
+            return value
+
+        due = get_value('DueDate')
+        if due is not None:
+            due = datetime.strptime(due, DATE_FORMAT)
+
         try:
-            due = datetime.strptime(node.get('due_date'), DATE_FORMAT)
+            complete = bool(int(node.get('is_complete')))
         except TypeError:
-            due = None
+            complete = False
 
-        standard = ('uuid', 'summary', 'is_complete', 'priority', 'due')
-        custom = dict(item for item in node.items()
-                      if item[0] not in standard)
+        try:
+            priority = int(node.get('priority'))
+        except TypeError:
+            priority = DEFAULT_PRIORITY
+
         task = Task(
                     parent=parent,
                     uuid=node.get('uuid'),
-                    summary=node.get('summary'),
-                    notes=node.text,
-                    is_complete=bool(int(node.get('is_complete'))),
-                    priority=node.get('priority'),
+                    summary=get_value('Summary'),
+                    notes=get_value('Notes'),
+                    is_complete=complete,
+                    priority=priority,
                     due_date=due,
-                    **custom
                 )
 
-        task.children = [Task.from_xml(child, task) for child in node.getchildren()]
+        children = node.find(XML_TASK_TREE)
+        if children is not None:
+            task.children = [Task.from_xml(child, task) for child in children.getchildren()]
+        else:
+            task.children = []
 
         return task
 
             due = ''
         complete = str(int(self.is_complete))
 
-        xml_node = etree.SubElement(parent,
-                                    Task.__name__,
+        xml_node = etree.SubElement(parent, Task.__name__,
                                     uuid=self.uuid,
-                                    summary=self.summary,
-                                    due=due,
-                                    priority=self.priority,
                                     is_complete=complete,
-                                    **self.custom)
+                                    priority=str(self._priority))
 
-        xml_node.text = self.notes
+        summary = etree.SubElement(xml_node, 'Summary')
+        summary.text = self.summary
 
-        for child in self.children:
-            child.to_xml(xml_node)
+        notes = etree.SubElement(xml_node, 'Notes')
+        notes.text = self.notes
+
+        due_date = etree.SubElement(xml_node, 'DueDate')
+        due_date.text = due
 
         return xml_node
 
+    def get_priority(self):
+        return PRIORITIES[self._priority]
+
+    def set_priority(self, priority_str):
+        """Sets the integer version for the priority using a string"""
+
+        self._priority = dict((p[1], p[0]) for p in PRIORITIES.items())[priority_str]
+
+    priority = property(get_priority, set_priority)
+
 class DataStore(object):
     data = None
     filename = None
         task = node.GetData()
 
         if task:
-            print 'Persisting node'
             xml_node = task.to_xml(parent)
+            task_list = etree.SubElement(xml_node, XML_TASK_TREE)
         else:
-            print 'Persisting tree'
             xml_node = parent
+            task_list = parent
 
-            for child in node.GetChildren():
-                self.to_xml(child, xml_node)
+        for child in node.GetChildren():
+            self.to_xml(child, task_list)
 
         return xml_node
 
     def persist(self, root):
         """Translate the HyperTreeList into our serialization format"""
 
-        parent = etree.Element('TaskList')
+        parent = etree.Element(XML_TASK_TREE)
         self.data._setroot(parent)
         serialized = self.to_xml(root, parent)
         self.persist_list()
             filename = self.filename
         self.data.write(self.filename,
                         encoding='utf-8',
-                        xml_declaration=True)
+                        xml_declaration=True,
+                        pretty_print=True)
 
 DATA = DataStore(DEFAULT_PATH)
 
 from wx.lib.masked import TimeCtrl
 from wx.lib.agw import hypertreelist as HTL
 from datetime import datetime, time
-from lib import Task, DATA
+from lib import Task, DATA, PRIORITIES, DEFAULT_PRIORITY
 
 __author__ = 'Josh VanderLinden'
 __appname__ = 'TreeDo'
 ID_COLLAPSE = 1020
 ID_EXPAND = 1030
 
+HIDE_COMPLETE = False
+
+def requires_selection(func):
+    def wrapped(self, *args, **kwargs):
+        if self.tree.GetSelection() != self.tree.root:
+            return func(self, *args, **kwargs)
+        else:
+            wx.MessageBox('Please select a task and try again.', 'Error')
+    return wrapped
+
 class TaskList(HTL.HyperTreeList):
     """
     This is the widget that houses the tasks
     """
     def __init__(self, parent):
+        self.parent = parent
+
         style = wx.SUNKEN_BORDER | wx.TR_HAS_BUTTONS | wx.TR_HAS_VARIABLE_ROW_HEIGHT | wx.TR_HIDE_ROOT | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_ROW_LINES | wx.TR_EDIT_LABELS #| wx.TR_COLUMN_LINES | HTL.TR_AUTO_CHECK_PARENT
         HTL.HyperTreeList.__init__(self, parent, -1, style=style)
 
         self.AddColumn('%')
+        self.AddColumn('!')
         self.AddColumn('Task')
         self.AddColumn('Due')
-        self.SetMainColumn(1)
+        self.SetMainColumn(2)
 
         self.root = self.AddRoot('Tasks')
         self.GetMainWindow().Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
         pt = evt.GetPosition()
         item, flags, column = self.HitTest(pt)
         if item and (flags & wx.TREE_HITTEST_ONITEMLABEL):
-            self.EditLabel(item)
+            #self.EditLabel(item)
+            self.parent.EditTask(item)
 
         evt.Skip()
 
         if task:
             task.is_complete = item.IsChecked()
 
+            if HIDE_COMPLETE:
+                item.Hide(task.is_complete)
+
         self.EvaluateCompleteness()
 
     def SetTasks(self, tasks):
         for task in tasks:
-            self.AddTask(task)
+            self.AddTask(task, refresh=False)
+
+        self.Refresh()
         self.ExpandAll()
 
-    def AddTask(self, task, parent=None):
-        if parent == None:
+    def AddTask(self, task, parent=None, refresh=True):
+        if parent is None:
             parent = self.root
 
         task.parent = parent
         item = self.AppendItem(parent, task.summary, ct_type=1)
 
-        self.SetItemText(item, '0%', 0)
-        item.Check(task.is_complete)
-        if task.due_date:
-            self.SetItemText(item, task.due_date.strftime('%Y-%m-%d %H:%M'), 2)
+        item.SetData(task)
 
         for child in task.children:
-            self.AddTask(child, item)
+            self.AddTask(child, item, refresh=refresh)
 
-        item.SetData(task)
+        if refresh:
+            self.Refresh()
+
+    def Refresh(self, erase=True, rect=None, parent=None):
+        """Refreshes the tree when a task has changed"""
+
+        if parent is None:
+            parent = self.root
+
+        for child in parent.GetChildren():
+            task = child.GetData()
+
+            if task:
+                self.SetItemText(child, '0%', 0)
+                self.SetItemText(child, str(task._priority), 1)
+                self.SetItemText(child, task.summary, 2)
+
+                child.Check(task.is_complete)
+                if HIDE_COMPLETE:
+                    child.Hide(task.is_complete)
+
+                if task.due_date:
+                    self.SetItemText(child, task.due_date.strftime('%H:%M %m/%d/%y'), 3)
+                else:
+                    self.SetItemText(child, '', 3)
+
+            self.Refresh(parent=child)
+
+        super(TaskList, self).Refresh()
 
 class TaskInfoDialog(wx.Dialog):
 
     def __init__(self, *args, **kwds):
-        kwds['style'] = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.THICK_FRAME
+        self.task = kwds.pop('task', None)
+
+        kwds['style'] = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.THICK_FRAME
         wx.Dialog.__init__(self, *args, **kwds)
         self.panel = wx.Panel(self, -1)
         self.txtSummary = wx.TextCtrl(self.panel, -1, "")
         self.lblNotes = wx.StaticText(self.panel, -1, _('Notes:'), style=wx.ALIGN_RIGHT)
         self.txtNotes = wx.TextCtrl(self.panel, -1, "", style=wx.TE_MULTILINE|wx.TE_RICH|wx.TE_WORDWRAP)
         self.lblPriority = wx.StaticText(self.panel, -1, _('Priority:'), style=wx.ALIGN_RIGHT)
-        self.cmbPriority = wx.ComboBox(self.panel, -1, choices=[_("1 - Urgent"), _("2 - Important"), _("3 - Normal"), _("4 - Low"), _("5 - Trivial")], style=wx.CB_DROPDOWN)
+
+        choices = [p[1] for p in sorted(PRIORITIES.items(), key=lambda p: p[0])]
+        self.cmbPriority = wx.ComboBox(self.panel, -1, choices=choices, style=wx.CB_DROPDOWN)
         self.chkIsComplete = wx.CheckBox(self.panel, -1, _('Is Complete'))
         self.lblDateDue = wx.StaticText(self.panel, -1, _('Due:'), style=wx.ALIGN_RIGHT)
         self.chkIsDue = wx.CheckBox(self.panel, -1, _('Has due date'))
         self.chkIsDue.Bind(wx.EVT_CHECKBOX, self.ToggleDueDate)
         self.txtSummary.SetFocus()
 
+        if self.task is not None:
+            self.SetTask(self.task)
+
     def __set_properties(self):
         self.SetTitle(_('Task Information'))
-        self.cmbPriority.SetSelection(2)
+        self.cmbPriority.SetValue(PRIORITIES[DEFAULT_PRIORITY])
         self.calDueDate.Enable(False)
         self.txtTime.Enable(False)
 
         self.Layout()
         self.Centre()
 
-        size = (270, 420)
+        size = (290, 450)
         self.SetMinSize(size)
         self.SetSize(size)
 
         self.txtTime.Enable(en)
 
     def GetTask(self):
+        if self.task is None:
+            self.task = Task()
+
         if self.chkIsDue.IsChecked():
             due = self.calDueDate.PyGetDate()
             tm = self.txtTime.GetValue()
         else:
             due = None
 
-        task = Task(
-                   summary=self.txtSummary.GetValue(),
-                   is_complete=self.chkIsComplete.IsChecked(),
-                   priority=self.cmbPriority.GetValue(),
-                   due_date=due,
-                   notes=self.txtNotes.GetValue()
-               )
+        self.task.summary = self.txtSummary.GetValue()
+        self.task.is_complete = self.chkIsComplete.IsChecked()
+        self.task.due_date = due
+        self.task.priority = self.cmbPriority.GetValue()
+        self.task.notes = self.txtNotes.GetValue()
 
-        return task
+        return self.task
 
     def SetTask(self, task):
         self.txtSummary.SetValue(task.summary)
         self.txtNotes.SetValue(task.notes)
         self.cmbPriority.SetStringSelection(task.priority)
-        self.chkIsComplete.SetChecked(task.is_complete)
-        self.calDueDate.PySetDate(task.due_date)
-        self.txtTime.SetValue(task.due_date.strftime('%X'))
+        self.chkIsComplete.SetValue(task.is_complete)
+
+        if task.due_date is not None:
+            self.chkIsDue.SetValue(True)
+            self.calDueDate.PySetDate(task.due_date)
+            self.txtTime.SetValue(task.due_date.strftime('%X'))
+
+        self.task = task
 
 class TreeDoFrame(wx.Frame):
     """
     """
 
     def __init__(self):
-        wx.Frame.__init__(self, None, -1, title=_('TreeDo'), size=(300, 500))
+        wx.Frame.__init__(self, None, -1, title=_('TreeDo'), size=(350, 500))
         self.SetMinSize((300, 300))
         self.CenterOnParent()
 
         self.toolbar.AddSimpleTool(wx.ID_SAVE, save_img, _('Save Task List'), _('Save the task list to the hard drive'))
         self.toolbar.AddSimpleTool(ID_ADD_TASK, add_img, _('Add Task'), _('Create a new task'))
         self.toolbar.AddSimpleTool(ID_ADD_SUBTASK, add_sub_img, _('Add Sub-Task'), _('Create a new subtask'))
-        self.toolbar.AddSimpleTool(ID_COLLAPSE, collapse_img, _('Collapse'), _('Collapse all tasks'))
+        #self.toolbar.AddSimpleTool(ID_COLLAPSE, collapse_img, _('Collapse'), _('Collapse all tasks'))
         self.toolbar.AddSimpleTool(ID_EXPAND, expand_img, _('Expand'), _('Expand all tasks'))
+        self.toolbar.AddSimpleTool(wx.ID_DELETE, collapse_img, _('Delete'), _('Delete this task'))
         self.Bind(wx.EVT_TOOL, self.OnToolClick)
         self.toolbar.Realize()
 
         width, height = self.GetSize()
 
         self.tree.SetColumnWidth(0, 40)
-        self.tree.SetColumnWidth(1, width - 150)
-        self.tree.SetColumnWidth(2, 100)
+        self.tree.SetColumnWidth(1, 20)
+        self.tree.SetColumnWidth(2, width - 180)
+        self.tree.SetColumnWidth(3, 100)
 
         evt.Skip()
 
+    def AddTask(self, parent=None):
+        """Allows the user to add a new task"""
+
+        taskDlg = TaskInfoDialog(self, -1, _('Task Info'))
+        if taskDlg.ShowModal() == wx.ID_OK:
+            task = taskDlg.GetTask()
+            self.tree.AddTask(task, parent)
+
+    @requires_selection
+    def AddSubTask(self):
+        """Allows the user to add a new task to the selected task"""
+
+        parent = self.tree.GetSelection()
+        return self.AddTask(parent)
+
+    @requires_selection
+    def EditSelectedTask(self):
+        """Allows the user to edit the selected task"""
+
+        item = self.tree.GetSelection()
+        self.EditTask(item)
+
+    def EditTask(self, item):
+        """Allows the user to edit a task's information"""
+
+        task = item.GetData()
+        taskDlg = TaskInfoDialog(self, -1, _('Task Info'), task=task)
+        if taskDlg.ShowModal() == wx.ID_OK:
+            task = taskDlg.GetTask()
+            item.SetData(task)
+            self.tree.Refresh()
+
+    @requires_selection
+    def DeleteSelectedTask(self):
+        """Allows the user to delete the selected task"""
+
+        item = self.tree.GetSelection()
+        self.DeleteTask(item)
+
+    def DeleteTask(self, item):
+        """Allows the user to delete a task"""
+
+        if item.HasChildren():
+            print 'Deleting item with children'
+
+        self.tree.DeleteChildren(item)
+        self.tree.Delete(item)
+
     def OnToolClick(self, evt):
-        if evt.GetId() in (ID_ADD_TASK, ID_ADD_SUBTASK):
-            taskDlg = TaskInfoDialog(self, -1, _('Task Info'))
-            if taskDlg.ShowModal() == wx.ID_OK:
-                task = taskDlg.GetTask()
-                parent = None
-                if evt.GetId() == ID_ADD_SUBTASK:
-                    parent = self.tree.GetSelection()
-                self.tree.AddTask(task, parent)
-        elif evt.GetId() == ID_COLLAPSE:
-            self.tree.Collapse()
-        elif evt.GetId() == ID_EXPAND:
+        eid = evt.GetId()
+
+        if eid == ID_ADD_TASK:
+            self.AddTask()
+        elif eid == ID_ADD_SUBTASK:
+            self.AddSubTask()
+        elif eid == ID_COLLAPSE:
+            for item in self.tree.GetChildren():
+                item.Collapse(self.tree)
+        elif eid == ID_EXPAND:
             self.tree.ExpandAll()
-        elif evt.GetId() == wx.ID_SAVE:
+        elif eid == wx.ID_SAVE:
             self.Persist()
+        elif eid == wx.ID_DELETE:
+            self.DeleteSelectedTask()
 
     def Persist(self):
         """Persists the task list to the filesystem"""