home · contact · privacy
Improve accounting scripts.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 17 Jan 2024 05:04:02 +0000 (06:04 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 17 Jan 2024 05:04:02 +0000 (06:04 +0100)
ledger.py
todo.py
todo_templates/base.html
todo_templates/calendar.html
todo_templates/do_todos.html [new file with mode: 0644]
todo_templates/pick_tasks.html [new file with mode: 0644]
todo_templates/task.html
todo_templates/tasks.html
todo_templates/todo.html

index 46035bc3790ac42815fd1b9c2a1910cf574da07e..f29f73ec190d3b1391b4a3f05ca088607ac221d2 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -449,8 +449,8 @@ class Booking:
             self.description = description
             self.top_comment = top_comment 
             self.validate_head()
-            self.transfer_lines = transfer_lines
-            if len(self.transfer_lines) < 2 and self.validate:
+            self.transfer_lines = transfer_lines if transfer_lines else []
+            if self.validate and len(self.transfer_lines) < 2:
                 raise PlomException(f'{self.intro}: too few transfer lines')
             self.calculate_account_changes()
             self.lines = [LedgerTextLine(l) for l in self.for_writing]
@@ -1032,8 +1032,11 @@ class LedgerDB(PlomDB):
             content = sent if sent else ([] if 'textarea'==edit_mode else None)
         else:
             content = self.bookings[index]
+        date_today = str(datetime.now())[:10]
         if copy:
-            content.date = str(datetime.now())[:10]
+            content.date = date_today 
+        elif -1 == index and (content is None or [] == content):
+            content = Booking(date=date_today, validate=False)
         if 'textarea' == edit_mode and content:
             content = content.for_writing
         else:
diff --git a/todo.py b/todo.py
index a4d7da0e56fce598906e40ad42fd7a247d3934f7..1e493acd982473be9958703d861722c8e7d24515 100644 (file)
--- a/todo.py
+++ b/todo.py
@@ -11,26 +11,82 @@ server_port = 8082
 DATE_FORMAT = '%Y-%m-%d'
 j2env = JinjaEnv(loader=JinjaFSLoader('todo_templates'))
 
+
+
+def today_date(with_time=False):
+    length = 19 if with_time else 10
+    return str(datetime.now())[:length]
+
+
+
+class AttributeWithHistory:
+
+    def __init__(self, default_if_empty, history=None, then_date='2000-01-01', set_check=None, alt_get=None):
+        self.default = default_if_empty
+        self.then_date = then_date
+        self.history = history if history else {}
+        self.set_check = set_check
+        self.alt_get = alt_get 
+
+    def set(self, value):
+        if self.set_check:
+            self.set_check(value)
+        keys = sorted(self.history.keys())
+        if len(self.history) == 0 or value != self.history[keys[-1]]:
+            self.history[today_date(with_time=True)] = value
+
+    def at(self, queried_date):
+        if self.alt_get:
+            ret = self.alt_get('at', queried_date)
+            if ret is not None:
+                return ret
+        if 0 == len(self.history):
+            return self.default
+        ret = self.history[sorted(self.history.keys())[0]]
+        for date_key, item in self.history.items():
+            if date_key > f'{queried_date} 23:59:59':
+                break
+            ret = item
+        return ret
+
+    @property
+    def now(self):
+        if self.alt_get:
+            ret = self.alt_get('now')
+            if ret is not None:
+                return ret
+        keys = sorted(self.history.keys())
+        return self.default if 0 == len(self.history) else self.history[keys[-1]]
+
+    @property
+    def then(self):
+        if self.alt_get:
+            ret = self.alt_get('then')
+            if ret is not None:
+                return ret
+        return self.at(self.then_date)
+
+
+
 class Task:
 
-    def __init__(self, db, id_, title_history=None, tags_history=None, default_effort_history=None, links_history=None, comment=''):
-        self.id_ = id_
+    def __init__(self,
+            db,
+            id_,
+            title_history=None,
+            tags_history=None,
+            default_effort_history=None,
+            subtask_ids_history=None,
+            comment=''):
         self.db = db
-        self.title_history = title_history if title_history else {}
-        self.tags_history = tags_history if tags_history else {}
-        self.default_effort_history = default_effort_history if default_effort_history else {}
-        self.links_history = links_history if links_history else {}
+        self.id_ = id_
+        self.title = AttributeWithHistory('', title_history, self.db.selected_date)
+        self.tags = AttributeWithHistory(set(), tags_history, self.db.selected_date)
+        self.default_effort = AttributeWithHistory(0.0, default_effort_history, self.db.selected_date, alt_get=self.subtasks_sum_maker())
+        self.subtask_ids = AttributeWithHistory(set(), subtask_ids_history, self.db.selected_date, set_check=self.subtask_loop_checker())
         self.comment = comment
-        self.visible = True
-
-    def _set_with_history(self, history, value):
-        keys = sorted(history.keys())
-        if len(history) == 0 or value != history[keys[-1]]:
-            history[str(datetime.now())[:19]] = value
 
-    def _last_of_history(self, history, default):
-        keys = sorted(history.keys())
-        return default if 0 == len(history) else history[keys[-1]]
+        self.visible = True
 
     @classmethod
     def from_dict(cls, db, d, id_):
@@ -40,119 +96,78 @@ class Task:
                d['title_history'],
                {k: set(v) for k, v in d['tags_history'].items()},
                d['default_effort_history'],
-               {k: set(v) for k, v in d['links_history'].items()},
+               {k: set(v) for k, v in d['subtasks_history'].items()},
                d['comment'])
         return t
 
     def to_dict(self):
         return {
-            'title_history': self.title_history,
-            'default_effort_history': self.default_effort_history,
-            'tags_history': {k: list(v) for k,v in self.tags_history.items()},
-            'links_history': {k: list(v) for k,v in self.links_history.items()},
+            'title_history': self.title.history,
+            'default_effort_history': self.default_effort.history,
+            'tags_history': {k: list(v) for k, v in self.tags.history.items()},
+            'subtasks_history': {k: list(v) for k, v in self.subtask_ids.history.items()},
             'comment': self.comment,
         }
 
     @property
-    def default_effort(self):
-        return self._last_of_history(self.default_effort_history, 1)
-
-    @default_effort.setter
-    def default_effort(self, default_effort):
-        self._set_with_history(self.default_effort_history, default_effort)
-
-    def default_effort_at(self, queried_date):
-        ret = self.default_effort_history[sorted(self.default_effort_history.keys())[0]]
-        for date_key, default_effort in self.default_effort_history.items():
-            if date_key > f'{queried_date} 23:59:59':
-                break
-            ret = default_effort
-        return ret
-
-    # @property
-    # def current_default_effort(self):
-    #     return self.default_effort_at(self.db.selected_date)
+    def subtasks(self):
+        subtasks = []
+        for id_ in self.subtask_ids.now:
+            subtasks += [self.db.tasks[id_]]
+        return subtasks
+
+    def subtask_loop_checker(self):
+        def f(subtask_ids_now):
+            loop_msg = "can't set subtask, would create loop"
+            for id_ in subtask_ids_now:
+                if id_ == self.id_:
+                    raise PlomException(loop_msg)
+                elif id_ in self.db.tasks.keys():
+                    subtask = self.db.tasks[id_]
+                    f(subtask.subtask_ids.now)
+        return f 
+
+    def subtasks_sum_maker(self):
+        def f(f_name, queried_date=None):
+            subtask_ids_to_check = getattr(self.subtask_ids, f_name)
+            if queried_date:
+                subtasks = [self.db.tasks[id_] for id_ in subtask_ids_to_check(queried_date)]
+            else:
+                subtasks = [self.db.tasks[id_] for id_ in subtask_ids_to_check]
+            if len(self.subtasks) > 0:
+                summe = 0
+                for subtask in self.subtasks:
+                    if queried_date:
+                        to_add = getattr(subtask.default_effort, f_name)
+                        summe += to_add(queried_date)
+                    else:
+                        summe += getattr(subtask.default_effort, f_name)
+                return summe
+            return None
+        return f
 
     def matches(self, search):
         if search is None:
             return False
         else:
-            return search in self.title or search in self.comment or search in '$'.join(self.tags) or search in self.title
-
-    @property
-    def title(self):
-        return self._last_of_history(self.title_history, '')
-
-    @title.setter
-    def title(self, title):
-        self._set_with_history(self.title_history, title)
-
-    def title_at(self, queried_date):
-        ret = self.title_history[sorted(self.title_history.keys())[0]]
-        for date_key, title in self.title_history.items():
-            if date_key > f'{queried_date} 23:59:59':
-                break
-            ret = title
-        return ret
-
-    @property
-    def current_title(self):
-        return self.title_at(self.db.selected_date)
-
-    @property
-    def tags(self):
-        return self._last_of_history(self.tags_history, set())
-
-    @tags.setter
-    def tags(self, tags):
-        self._set_with_history(self.tags_history, set(tags))
-
-    @property
-    def links(self):
-        return self._last_of_history(self.links_history, set())
-
-    @links.setter
-    def links(self, links):
-        self._set_with_history(self.links_history, set(links))
-
-    # @property
-    # def id_(self):
-    #     for k, v in self.db.tasks.items():
-    #         if v == self:
-    #             return k
+            return search in self.title.now or search in self.comment or search in '$'.join(self.tags.now)
 
 
 class Day:
 
-    # def __init__(self, db, todos=None, comment='', date=None):
     def __init__(self, db, date, comment=''):
         self.date = date 
         self.db = db
         self.comment = comment
         self.archived = True
-        self.todos = {} # legacy
         self.linked_todos_as_list = []
-        # if todos:
-        #     for id_, todo_dict in todos.items():
-        #         self.add_todo(id_, todo_dict)
+        self.todos = {}
 
     @classmethod
     def from_dict(cls, db, d, date=None):
-        # todos = {}
         comment = d['comment'] if 'comment' in d.keys() else ''
         day = cls(db, date, comment)
-        # if 'todos' in d.keys():
-        #     for uuid, todo_dict in d['todos'].items():
-        #         day.add_todo(uuid, todo_dict)
-        # if 'linked_todos' in d.keys():
         for id_ in d['linked_todos']:
-            # if id_ in day.linked_todos.keys():
-            #     continue
-            # if id_ is None:
-            #     continue
-            # linked_todo = db.todos[id_]
-            # linked_todo._day = day 
-            # day.linked_todos_as_list += [linked_todo]
             day.linked_todos_as_list += [db.todos[id_]]
         return day
 
@@ -160,37 +175,8 @@ class Day:
         d = {'comment': self.comment, 'linked_todos': []}
         for todo_id in self.linked_todos.keys():
             d['linked_todos'] += [todo_id]
-        # for task_uuid, todo in self.todos.items():
-        # for id_, todo in self.todos.items():
-        # #     d['todos'][task_uuid] = todo.to_dict()
-        #     new_type_todo_id = f'{self.date}_{task_uuid}'
-        #     if not new_type_todo_id in d['linked_todos']:
-        #         # d['linked_todos'] += [todo.id_] 
-        #         d['linked_todos'] += [new_type_todo_id] 
         return d
 
-    # def add_todo(self, task_id, dict_source=None):
-    #     new_type_todo_id = f'{self.date}_{task_id}'
-    #     task = self.db.tasks[task_id]
-    #     if new_type_todo_id in self.db.todos.keys():
-    #         todo = self.db.todos[new_type_todo_id]
-    #     # elif task_id in self.db.keys():
-    #     #     todo = self.db.todos[task_id]
-    #     else:
-    #         # todo = Todo.from_dict(self.db, dict_source, id_=new_type_todo_id) if dict_source else Todo(db=self.db, day=self)
-    #         todo = Todo.from_dict(self.db, dict_source)
-    #     # todo._id = new_type_todo_id
-    #     todo._task = self.db.tasks[task_id] 
-    #     todo._id = new_type_todo_id
-    #     todo._day = self 
-    #     # self.todos[task_id] = todo 
-    #     self.linked_todos_as_list += [todo]
-    #     self.db.todos[new_type_todo_id] = todo
-    #     # self.db.todos[todo.task_id] = [todo]
-    #     # self.db.todos_as_list += [todo]
-    #     # self.db.all_todos[todo.task_id] = todo
-    #     return todo 
-
     @property
     def linked_todos(self):
         linked_todos = {}
@@ -203,8 +189,9 @@ class Day:
         for todo in [todo for todo in self.linked_todos.values()
                      if self.date in todo.efforts.keys()]:
             day_effort = todo.efforts[self.date]
+            # print("DEBUG b", day_effort, todo.task.default_effort_at(self.date))
             if todo.done:
-                s += day_effort if day_effort else todo.task.default_effort_at(self.date)
+                s += day_effort if day_effort else todo.task.default_effort.at(self.date)
             elif include_undone:
                 s += day_effort if day_effort else 0 
         return s
@@ -217,94 +204,116 @@ class Day:
     # def todos_sum2(self):
     #     return self._todos_sum(True)
 
-    # @property
-    # def date(self):
-    #     if self._date:
-    #         return self._date
-    #     else:
-    #         for k, v in self.db.days.items():
-    #             # print("DEBUG date", k, v)
-    #             if v == self:
-    #                 return k
-    #     print("DEBUG FAIL", self.test_date, self)
-
 
 class Todo:
 
-    # def __init__(self, db, id_, task, done=False, day_effort=None, comment='', day_tags=None, importance=1.0, efforts=None):
-    def __init__(self, db, id_, task, done=False, comment='', day_tags=None, importance=1.0, efforts=None, children=None):
+    def __init__(self,
+                 db,
+                 id_,
+                 task,
+                 done=False,
+                 comment='',
+                 day_tags=None,
+                 importance=1.0,
+                 efforts=None,
+                 child_ids=None):
         self.id_ = id_
         self.db = db 
         self.task = task 
-        self.done = done
-        # self.day_effort = day_effort
-        self.efforts = efforts if efforts else {}
+        self._done = done
+        self._efforts = efforts if efforts else {}
         self.comment = comment
         self.day_tags = day_tags if day_tags else set()
         self.importance = importance
-        self.children = children if children else [] 
+        self.child_ids = child_ids if child_ids else [] 
+
         self.parent = None 
 
     @classmethod
     def from_dict(cls, db, d, id_):
-        # todo = cls(db, None, None, d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'])
-        # todo._task = db.tasks[d['task']] if 'task' in d.keys() else None
-        # todo._efforts = d['efforts'] if 'efforts' in d.keys() else None
-        # todo = cls(db, id_, db.tasks[d['task']], d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'], d['efforts'])
-        todo = cls(db, id_, db.tasks[d['task']], d['done'], d['comment'], set(d['day_tags']), d['importance'], d['efforts'])
-        if 'children' in d:
-            todo.children = d['children']
+        todo = cls(
+                db,
+                id_,
+                db.tasks[d['task']],
+                d['done'],
+                d['comment'],
+                set(d['day_tags']),
+                d['importance'],
+                d['efforts'],
+                d['children'])
         return todo
 
-    # @classmethod
-    # def OLD_from_dict(cls, day, d):
-    #     todo = cls(day, d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'])
-    #     if 'efforts' in d.keys():
-    #         todo._efforts = d['efforts']
-    #     return todo
-
     def to_dict(self):
-        # return {'task': self.task.id_, 'done': self.done, 'day_effort': self.day_effort, 'comment': self.comment, 'day_tags': list(self.day_tags), 'importance': self.importance, 'efforts': self.efforts}
-        return {'task': self.task.id_, 'done': self.done, 'comment': self.comment, 'day_tags': list(self.day_tags), 'importance': self.importance, 'efforts': self.efforts, 'children': self.children}
+        return {
+                'task': self.task.id_,
+                'done': self.done,
+                'comment': self.comment,
+                'day_tags': list(self.day_tags),
+                'importance': self.importance,
+                'efforts': self.efforts,
+                'children': self.child_ids}
+
+    @property
+    def title(self):
+        return self.task.title.at(self.earliest_date)
+
+    @property
+    def children(self):
+        return [self.db.todos[id_] for id_ in self.child_ids]
 
     @property
     def default_effort(self):
-        return self.task.default_effort_at(self.sorted_effort_dates[0])
+        return self.task.default_effort.at(self.earliest_date)
 
-    # @property
-    # def effort(self):
-    #     if self.day_effort:
-    #         return self.day_effort
-    #     else:
-    #         return self.day_effort if self.day_effort else self.default_effort
+    @property
+    def done(self):
+        if len(self.children) > 0:
+            for child in self.children:
+                if not child.done:
+                    return False
+            return True
+        else:
+            return self._done
 
-    # @property
-    # def task(self):
-    #     if self._task:
-    #         return self._task
-    #     # else:
-    #     #     for k, v in self.day.todos.items():
-    #     #         if v == self:
-    #     #             return self.db.tasks[k]
+    @done.setter
+    def done(self, doneness):
+        self._done = doneness
 
-    def is_effort_removable(self, date):
-        if not date in self.efforts.keys():
-            return False
-        # if len(self.efforts) == 1:
-        #     return False
-        if self.efforts[date]:
-            return False
-        if self.done and date == self.sorted_effort_dates[-1]:
-            return False
-        return True
+    @property
+    def efforts(self):
+        if self.children:
+            efforts = {} 
+            for date in self._efforts.keys():
+                efforts[date] = None
+            for child in self.children:
+                to_add = None 
+                for date, effort in child.efforts.items():
+                    if not date in efforts.keys():
+                        efforts[date] = None 
+                    if effort is not None:
+                        to_add = effort
+                    elif child.done:
+                        to_add = child.task.default_effort.at(date)
+                if to_add is not None:
+                    if efforts[date] is not None:
+                        efforts[date] += to_add
+                    else:
+                        efforts[date] = to_add
+            return efforts
+        else:
+            return self._efforts
+
+    @efforts.setter
+    def efforts(self, efforts_dict):
+        self._efforts = efforts_dict
 
     @property
-    def total_effort(self):
+    def all_days_effort(self):
         total = 0
         for effort in self.efforts.values():
             total += effort if effort else 0
         if self.done:
-            total = max(total, self.task.default_effort_at(self.sorted_effort_dates[-1]))
+            total = max(total, self.task.default_effort.at(self.latest_date))
         return total
 
     def matches(self, search):
@@ -313,9 +322,14 @@ class Todo:
         else:
             return search in self.comment or search in '$'.join(self.tags) or search in self.title
 
-    @property
-    def title(self):
-        return self.task.title_at(self.sorted_effort_dates[0])
+    def is_effort_removable(self, date):
+        if not date in self.efforts.keys():
+            return False
+        if self.efforts[date]:
+            return False
+        if self.done and date == self.latest_date:
+            return False
+        return True
 
     @property
     def path(self):
@@ -326,24 +340,16 @@ class Todo:
 
     @property
     def tags(self):
-        return self.day_tags | self.task.tags
-
-    # def internals_empty(self):
-    #     return len(self.comment) == 0 and len(self.day_tags) == 0
-
-    # def ensure_day_efforts_table(self):
-    #     # We don't do this yet at __init__ because self.day.date is unknown, since Todo may be imported with Day, and during the import process the Day is not yet keyed in TodoDB.days.
-    #     if not hasattr(self, '_efforts'):
-    #         self._efforts = {} # {self.day.date: self.day_effort}
-
-    # def set_day_effort(self, date, effort):
-    #     self.ensure_day_efforts_table()
-    #     self._efforts[date] = self.day_effort
+        return self.day_tags | self.task.tags.now
 
     @property
     def day_effort(self):
         return self.efforts[self.db.selected_date]
 
+    @property
+    def day(self):
+        return self.db.days[self.earliest_date]
+
     @property
     def sorted_effort_dates(self):
         dates = list(self.efforts.keys())
@@ -351,36 +357,30 @@ class Todo:
         return dates
 
     @property
-    def day(self):
-        if len(self.efforts) == 0:
-            return None
-        todo_start_date = self.sorted_effort_dates[0]
-        return self.db.days[todo_start_date]
-
-    # @property
-    # def efforts(self):
-    #     self.ensure_day_efforts_table()
-    #     return self._efforts
+    def earliest_date(self):
+        return self.sorted_effort_dates[0]
 
-    # @property
-    # def id_(self):
-    #     if self._id:
-    #         return self._id
-    #     for k, v in self.db.todos.items():
-    #         if v == self:
-    #             return k
-    #     # return f'{self.day.date}_{self.task.id_}'
+    @property
+    def latest_date(self):
+        return self.sorted_effort_dates[-1]
 
 
 class TodoDB(PlomDB):
 
-    def __init__(self, prefix, selected_date=None, t_filter_and = None, t_filter_not = None, hide_unchosen=False, hide_done=False):
+    def __init__(self,
+            prefix,
+            selected_date=None,
+            t_filter_and = None,
+            t_filter_not = None,
+            hide_unchosen=False,
+            hide_done=False):
         self.prefix = prefix
-        self.selected_date = selected_date if selected_date else str(datetime.now())[:10]
+        self.selected_date = selected_date if selected_date else today_date() #str(datetime.now())[:10]
         self.t_filter_and = t_filter_and if t_filter_and else []
         self.t_filter_not = t_filter_not if t_filter_not else []
         self.hide_unchosen = hide_unchosen
         self.hide_done = hide_done
+
         self.days = {}
         self.tasks = {}
         self.t_tags = set()
@@ -391,50 +391,29 @@ class TodoDB(PlomDB):
         d = json.load(f)
         for id_, t_dict in d['tasks'].items():
             t = self.add_task(id_=id_, dict_source=t_dict)
-            for tag in t.tags:
+            for tag in t.tags.now:
                 self.t_tags.add(tag)
-        # if 'todos' in d.keys():
         for id_, todo_dict in d['todos'].items():
-            # todo = Todo.from_dict(self, todo_dict, id_)
-            # todo._id = id_ 
-            # self.todos[id_] = todo
-            todo = self.add_todo(todo_dict, id_) # Todo.from_dict(self, todo_dict, id_)
+            todo = self.add_todo(todo_dict, id_)
             self.todos[id_] = todo 
             for tag in todo.day_tags:
                 self.t_tags.add(tag)
         for date, day_dict in d['days'].items():
             self.add_day(dict_source=day_dict, date=date)
         for todo in self.todos.values():
-            for child_id in todo.children:
-                self.todos[child_id].parent = todo
-            # if len(todo.efforts) == 0:
-            #     print("DEBUG", todo.id_, todo.task.title, todo.efforts)
+            for child in todo.children:
+                child.parent = todo
             for date in todo.efforts.keys():
                 if not date in self.days.keys():
                     self.add_day(date)
                 if not todo in self.days[date].linked_todos_as_list:
                     self.days[date].linked_todos_as_list += [todo]
-                    # print("DEBUG", todo.title, date)
-                # if type(todo.efforts[date]) == str:
-                #     todo.efforts[date] = float(todo.efforts[date])
-                #     print("DEBUG", todo.title, date, todo.efforts[date])
-        # for todo in self.todos.values():
-        # for day in self.days.values():
-        #     for todo in day.todos.values():
-        #         for tag in todo.day_tags:
-        #             self.t_tags.add(tag)
-        # delete = None
-        # for todo in self.todos.values():
-        #     if len(todo.efforts) == 0:
-        #         print("DEBUG", todo.task.title)
-        #         delete = todo.id_ 
-        # self.delete_todo(delete)
         self.set_visibilities()
 
     def set_visibilities(self):
         for uuid, t in self.tasks.items():
-            t.visible = len([tag for tag in self.t_filter_and if not tag in t.tags]) == 0\
-                    and len([tag for tag in self.t_filter_not if tag in t.tags]) == 0\
+            t.visible = len([tag for tag in self.t_filter_and if not tag in t.tags.now]) == 0\
+                    and len([tag for tag in self.t_filter_not if tag in t.tags.now]) == 0\
                     and ((not self.hide_unchosen) or uuid in self.selected_day.todos.keys())
         # for day in self.days.values():
         #     for todo in day.todos.values():
@@ -442,8 +421,8 @@ class TodoDB(PlomDB):
         #             and len([tag for tag in self.t_filter_not if tag in todo.day_tags | todo.task.tags ]) == 0\
         #             and ((not self.hide_done) or (not todo.done))
         for todo in self.todos.values():
-            todo.visible = len([tag for tag in self.t_filter_and if not tag in todo.day_tags | todo.task.tags ]) == 0\
-                and len([tag for tag in self.t_filter_not if tag in todo.day_tags | todo.task.tags ]) == 0\
+            todo.visible = len([tag for tag in self.t_filter_and if not tag in todo.day_tags | todo.task.tags.now ]) == 0\
+                and len([tag for tag in self.t_filter_not if tag in todo.day_tags | todo.task.tags.now ]) == 0\
                 and ((not self.hide_done) or (not todo.done))
 
     def to_dict(self):
@@ -458,13 +437,6 @@ class TodoDB(PlomDB):
             d['todos'][id_] = todo.to_dict()
         return d
 
-    # @property
-    # def all_todos(self):
-    #     todos = {}
-    #     for todo in self.todos_as_list:
-    #         todos[todo.id_] = todo
-    #     return todos 
-
     @property
     def selected_day(self):
         if not self.selected_date in self.days.keys():
@@ -481,14 +453,11 @@ class TodoDB(PlomDB):
             del self.days[date]
         self.write_text_to_db(json.dumps(self.to_dict()))
 
-    def add_task(self, id_=None, dict_source=None, return_id=False):
+    def add_task(self, id_=None, dict_source=None):
         id_ = id_ if id_ else str(uuid4())
         t = Task.from_dict(self, dict_source, id_) if dict_source else Task(self, id_)
         self.tasks[id_] = t
-        if return_id:
-            return id_, t
-        else:
-            return t
+        return t
 
     def add_todo(self, todo_dict=None, id_=None, task=None, efforts=None):
         id_ = id_ if id_ else str(uuid4())
@@ -497,10 +466,9 @@ class TodoDB(PlomDB):
         elif task and efforts:
             todo = Todo(self, id_, task, efforts=efforts)
             children = []
-            for child_id in task.links:
-                child_task = self.tasks[child_id]
+            for child_task in task.subtasks:
                 children += [self.add_todo(task=child_task, efforts=efforts)]
-            todo.children = [child.id_ for child in children]
+            todo.child_ids = [child.id_ for child in children]
         self.todos[id_] = todo 
         return todo
 
@@ -509,44 +477,6 @@ class TodoDB(PlomDB):
         self.days[date] = day 
         return day
 
-    # def show_day(self, task_sort=None):
-    #     task_sort = task_sort if task_sort else 'title' 
-    #     current_date = datetime.strptime(self.selected_date, DATE_FORMAT)
-    #     prev_date = current_date - timedelta(days=1)
-    #     prev_date_str = prev_date.strftime(DATE_FORMAT)
-    #     next_date = current_date + timedelta(days=1)
-    #     next_date_str = next_date.strftime(DATE_FORMAT)
-    #     task_rows = []
-    #     for uuid, task in self.tasks.items():
-    #         if not task.visible:
-    #             continue
-    #         todo = None
-    #         if uuid in self.selected_day.todos.keys():
-    #             todo = self.selected_day.todos[uuid]
-    #             if not todo.visible:
-    #                 continue
-    #         task_rows += [{'uuid': uuid, 'task': task, 'todo': todo}] 
-    #     if task_sort == 'title':
-    #         task_rows.sort(key=lambda r: r['task'].title)
-    #     elif task_sort == 'default_effort':
-    #         task_rows.sort(key=lambda r: r['task'].default_effort, reverse=True)
-    #     elif task_sort == 'done':
-    #         task_rows.sort(key=lambda r: 0 if not r['todo'] else r['todo'].day_effort if r['todo'].day_effort else r['task'].default_effort if r['todo'].done else 0, reverse=True)
-    #     elif task_sort == 'importance':
-    #         task_rows.sort(key=lambda r: 0.0 if not r['todo'] else r['todo'].importance, reverse=True)
-    #     elif task_sort == 'chosen':
-    #         task_rows.sort(key=lambda r: False if not r['todo'] else True, reverse=True)
-    #     elif task_sort == 'comment':
-    #         task_rows.sort(key=lambda r: '' if not r['todo'] else r['todo'].comment, reverse=True)
-    #     done_tasks = []
-    #     for uuid, task in self.tasks.items():
-    #         if uuid in self.selected_day.todos.keys():
-    #             todo = self.selected_day.todos[uuid]
-    #             if todo.done:
-    #                 done_tasks += [todo]
-    #     done_tasks.sort(key=lambda t: t.effort, reverse=True)
-    #     return j2env.get_template('day.html').render(db=self, action=self.prefix+'/day', prev_date=prev_date_str, next_date=next_date_str, task_rows=task_rows, sort=task_sort, done_tasks=done_tasks)
-
     def neighbor_dates(self):
         current_date = datetime.strptime(self.selected_date, DATE_FORMAT)
         prev_date = current_date - timedelta(days=1)
@@ -555,18 +485,18 @@ class TodoDB(PlomDB):
         next_date_str = next_date.strftime(DATE_FORMAT)
         return prev_date_str, next_date_str
 
-    def show_do_day(self, sort_order=None):
+    def show_do_todos(self, sort_order=None):
         prev_date_str, next_date_str = self.neighbor_dates()
         todos = [t for t in self.selected_day.linked_todos_as_list if t.visible]
         if sort_order == 'title':
-            todos.sort(key=lambda t: t.task.title)
+            todos.sort(key=lambda t: t.task.title.then)
         elif sort_order == 'done':
-            todos.sort(key=lambda t: t.day_effort if t.day_effort else t.default_effort if t.done else 0, reverse=True)
+            todos.sort(key=lambda t: t.day_effort if t.day_effort else t.default_effort.then if t.done else 0, reverse=True)
         elif sort_order == 'default_effort':
             todos.sort(key=lambda t: t.default_effort, reverse=True)
         elif sort_order == 'importance':
             todos.sort(key=lambda t: t.importance, reverse=True)
-        return j2env.get_template('do_day.html').render(
+        return j2env.get_template('do_todos.html').render(
                 day=self.selected_day,
                 tags=self.t_tags,
                 filter_and=self.t_filter_and,
@@ -582,7 +512,7 @@ class TodoDB(PlomDB):
         self.t_filter_not = ['deleted']
         self.set_visibilities()
         days_to_show = {}
-        todays_date_str = str(datetime.now())[:10]
+        todays_date_str = today_date() # str(datetime.now())[:10]
         todays_date_obj = datetime.strptime(todays_date_str, DATE_FORMAT) 
         yesterdays_date_obj = todays_date_obj - timedelta(1)
         yesterdays_date_str = yesterdays_date_obj.strftime(DATE_FORMAT) 
@@ -606,39 +536,22 @@ class TodoDB(PlomDB):
             else:
                 days_to_show[current_date_str] = self.days[current_date_str]
             days_to_show[current_date_str].weekday = datetime.strptime(current_date_str, DATE_FORMAT).strftime('%A')[:2]
-        return j2env.get_template('calendar.html').render(db=self, days=days_to_show, action=self.prefix+'/calendar', start_date=start_date_str, end_date=end_date_str)
+        return j2env.get_template('calendar.html').render(
+                db=self,
+                days=days_to_show,
+                start_date=start_date_str,
+                end_date=end_date_str)
 
     def show_todo(self, id_, return_to):
         todo = self.todos[id_]
-        # if selected_date not in self.days.keys():
-        #     self.days[selected_date] = self.add_day(test_date=f'3:{selected_date}') 
-        #     # print("DEBUG show_todo", self.days[selected_date].date)
-        # if task_uuid in self.days[selected_date].todos:
-        #     todo = self.days[selected_date].todos[task_uuid]
-        # else:
-        #     todo = self.days[selected_date].add_todo(task_uuid)
-        linked_todos = [self.todos[c] for c in todo.children]
         filtered_todos = [] #[t for t in self.todos.values() if t.visible and t != self and (t not in linked_todos) and (len(search) == 0 or t.matches(search))] 
         return j2env.get_template('todo.html').render(
                 db=self,
                 todo=todo,
                 filtered_todos=filtered_todos,
-                linked_todos=linked_todos,
+                child_todos=todo.children,
                 return_to=return_to)
 
-    # def update_todo_mini(self, task_uuid, date, day_effort, done, importance):
-    #     if date not in self.days.keys():
-    #         self.days[date] = self.add_day(test_date=f'Y:{date}') 
-    #         # print("DEBUG update_todo_min", self.days[date].date)
-    #     if task_uuid in self.days[date].todos.keys():
-    #         todo = self.days[date].todos[task_uuid]
-    #     else:
-    #         todo = self.days[date].add_todo(task_uuid)
-    #     todo.day_effort = day_effort
-    #     todo.done = done
-    #     todo.importance = importance
-    #     return todo
-
     def collect_tags(self, tags_joined, tags_checked):
         tags = set()
         for tag in [tag.strip() for tag in tags_joined.split(';') if tag.strip() != '']:
@@ -655,9 +568,9 @@ class TodoDB(PlomDB):
         todo.importance = importance 
 
     def update_todo(self, id_, efforts, done, comment, day_tags_joined, day_tags_checked, importance):
-        if len(efforts) == 0:
-            raise PlomException('todo must have at least one effort!')
         todo = self.todos[id_]
+        if len(efforts) == 0 and not todo.children:
+            raise PlomException('todo must have at least one effort!')
         todo.done = done
         todo.efforts = efforts 
         for date in todo.efforts:
@@ -676,6 +589,8 @@ class TodoDB(PlomDB):
             dates_to_delete += [date]
         for date in dates_to_delete:
             self.delete_effort(todo, date, force=True)
+        if todo.parent:
+            todo.parent.child_ids.remove(todo.id_)
         del self.todos[id_]
 
     def delete_effort(self, todo, date, force=False):
@@ -684,32 +599,10 @@ class TodoDB(PlomDB):
         self.days[date].linked_todos_as_list.remove(todo)
         del todo.efforts[date]
 
-
-    # def update_todo(self, task_uuid, date, day_effort, done, comment, day_tags_joined, day_tags_checked, importance):
-    #     day_effort = float(day_effort) if len(day_effort) > 0 else None
-    #     importance = float(importance)
-    #     todo = self.update_todo_mini(task_uuid, date, day_effort, done, importance)
-    #     todo.comment = comment
-    #     todo.day_tags = self.collect_tags(day_tags_joined, day_tags_checked) 
-
-    # def link_day_with_todo(self, date, todo_id):
-    #     print("DEBUG link", date, todo_id)
-    #     todo_creation_date, task_uuid = todo_id.split('_')
-    #     todo = self.days[todo_creation_date].todos[task_uuid]
-    #     if date in todo.efforts.keys():
-    #         raise PlomException('todo already linked to respective day')
-    #     todo.set_day_effort(date, None)
-    #     if date not in self.days.keys():
-    #         print("DEBUG link_day_with_todo", date)
-    #         self.days[date] = self.add_day(test_date=f'Z:{date}') 
-    #     self.days[date].linked_todos_as_list += [todo]
-    #     print("DEBUG", date, self.days[date].linked_todos)
-
     def show_task(self, id_, return_to='', search=''):
         task = self.tasks[id_] if id_ else self.add_task()
         selected = task.id_ in self.selected_day.todos.keys()
-        linked_tasks = [self.tasks[l] for l in task.links]
-        filtered_tasks = [t for t in self.tasks.values() if t.visible and t != self and (t not in linked_tasks) and (len(search) == 0 or t.matches(search))] 
+        filtered_tasks = [t for t in self.tasks.values() if t.visible and t != self and (t not in task.subtasks) and (len(search) == 0 or t.matches(search))] 
         return j2env.get_template('task.html').render(
                 db=self,
                 search=search,
@@ -717,31 +610,35 @@ class TodoDB(PlomDB):
                 filter_and=self.t_filter_and,
                 filter_not=self.t_filter_not,
                 filtered_tasks=filtered_tasks,
-                linked_tasks=linked_tasks,
                 task=task,
                 return_to=return_to)
 
-    def update_task(self, id_, title, default_effort, tags_joined, tags_checked, links, comment):
+    def update_task(self, id_, title, default_effort, tags_joined, tags_checked, subtask_ids, comment):
         task = self.tasks[id_] if id_ in self.tasks.keys() else self.add_task(id_)
-        task.title = title
-        task.default_effort = float(default_effort) if len(default_effort) > 0 else None
-        task.tags = self.collect_tags(tags_joined, tags_checked
-        task.links = links
+        task.title.set(title)
+        task.default_effort.set(float(default_effort) if len(default_effort) > 0 else None)
+        task.tags.set(self.collect_tags(tags_joined, tags_checked)
+        task.subtask_ids.set(subtask_ids)
         task.comment = comment 
 
     def show_tasks(self, expand_uuid):
         expanded_tasks = {}
         if expand_uuid:
-            for uuid in self.tasks[expand_uuid].links:
+            for uuid in self.tasks[expand_uuid].subtask_ids.now:
                 expanded_tasks[uuid] = self.tasks[uuid]
-        return j2env.get_template('tasks.html').render(db=self, action=self.prefix+'/tasks', expand_uuid=expand_uuid, expanded_tasks=expanded_tasks)
+        return j2env.get_template('tasks.html').render(
+                db=self,
+                filter_and=self.t_filter_and,
+                filter_not=self.t_filter_not,
+                expand_uuid=expand_uuid,
+                expanded_tasks=expanded_tasks)
 
-    def show_new_day(self, search, hide_chosen_tasks, sort_order=None):
+    def show_pick_tasks(self, search, hide_chosen_tasks, sort_order=None):
         prev_date_str, next_date_str = self.neighbor_dates()
         chosen_todos = self.selected_day.linked_todos_as_list
         relevant_todos = []
         for todo in self.todos.values():
-            if todo.done or (not todo.visible) or (not todo.matches(search)) or todo in self.selected_day.linked_todos_as_list: # or todo.day.date == self.selected_day.date:  # TODO or todo is linked by day  
+            if todo.done or (not todo.visible) or (not todo.matches(search)) or todo.earliest_date >= self.selected_date:
                 continue
             relevant_todos += [todo] 
         tasks = []
@@ -753,12 +650,12 @@ class TodoDB(PlomDB):
         if sort_order == 'title':
             chosen_todos.sort(key=lambda t: t.title)
             relevant_todos.sort(key=lambda t: t.title)
-            tasks.sort(key=lambda t: t.title)
+            tasks.sort(key=lambda t: t.title.then)
         elif sort_order == 'effort':
             chosen_todos.sort(key=lambda t: t.day_effort if t.day_effort else t.default_effort if t.done else 0, reverse=True)
-            relevant_todos.sort(key=lambda t: t.total_effort, reverse=True)
-            tasks.sort(key=lambda t: t.default_effort, reverse=True)
-        return j2env.get_template('new_day.html').render(
+            relevant_todos.sort(key=lambda t: t.all_days_effort, reverse=True)
+            tasks.sort(key=lambda t: t.default_effort.then, reverse=True)
+        return j2env.get_template('pick_tasks.html').render(
                 sort=sort_order,
                 tags=self.t_tags,
                 chosen_todos=chosen_todos,
@@ -838,7 +735,6 @@ class TodoHandler(PlomHandler):
         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
         parsed_url = urlparse(self.path)
         site = path_split(urlparse(self.path).path)[1]
-        # site = path_split(parsed_url.path)[1]
         db = TodoDB(prefix=config['prefix'])
         redir_params = []
         for param_name, filter_db_name in {('t_and', 't_filter_and'), ('t_not', 't_filter_not')}:
@@ -863,37 +759,34 @@ class TodoHandler(PlomHandler):
             redir_params += [('end', postvars['end'][0] if len(postvars['end'][0]) > 0 else '-')]
 
         elif 'todo' == site:
-            # task_uuid = postvars['task_uuid'][0]
             todo_id = postvars['todo_id'][0]
-            # date = postvars['date'][0]
             old_todo = db.todos[todo_id] 
             efforts = {}
             for i, date in enumerate(postvars['effort_date']):
                 if '' == date:
                     continue
-                efforts[date] = float(postvars['effort'][i]) if len(postvars['effort'][i]) > 0 else None 
-            if 'delete_effort' in postvars.keys():
-                for date in postvars['delete_effort']:
-                    del efforts[date]
+                try:
+                    datetime.strptime(date, DATE_FORMAT)
+                except ValueError:
+                    raise PlomException(f'bad date string')
+                efforts[date] = None
+                if not old_todo.children:
+                    efforts[date] = float(postvars['effort'][i]) if len(postvars['effort'][i]) > 0 else None 
             if 'delete' in postvars.keys():
-                # old_todo = db.days[date].todos[task_uuid]
-                if 'done' in postvars or postvars['comment'][0] != '' or len(collect_checked('day_tag_', postvars)) > 0 or postvars['joined_day_tags'][0] != '' or len(efforts) > 0:
+                has_day_effort = False 
+                for effort in efforts.values():
+                    if effort is not None:
+                        has_day_effort = True 
+                        break
+                if 'done' in postvars or postvars['comment'][0] != '' or len(collect_checked('day_tag_', postvars)) > 0 or postvars['joined_day_tags'][0] != '' or has_day_effort:
                     raise PlomException('will not remove todo of preserve-worthy values')
                 db.delete_todo(todo_id) 
-                # db.write()
-                # self.redirect('new_day')
             else:
-                # redir_params += [('task', task_uuid), ('date', date)]
                 redir_params += [('id', todo_id)]
-                # db.update_todo(task_uuid, date, postvars['day_effort'][0], 'done' in postvars.keys(), postvars['comment'][0], postvars['joined_day_tags'][0], collect_checked('day_tag_', postvars), postvars['importance'][0])
-                # efforts = {}
-                # for i, date in enumerate(postvars['effort_date']):
-                #     if '' == date:
-                #         continue
-                #     efforts[date] = postvars['effort'][i] if len(postvars['effort'][i]) > 0 else None 
                 if 'delete_effort' in postvars.keys():
                     for date in postvars['delete_effort']:
                         db.delete_effort(old_todo, date)
+                        del efforts[date]
                 db.update_todo(id_=todo_id,
                                efforts=efforts,
                                done='done' in postvars.keys(),
@@ -904,23 +797,15 @@ class TodoHandler(PlomHandler):
 
         elif 'task' == site:
             id_ = postvars['id'][0]
-            redir_params += [('id', id_)]
             if 'filter' in postvars.keys():
                 redir_params += [('search', postvars['search'][0])]
             elif 'title' in postvars.keys():
-                links = [] if not 'link_task' in postvars.keys() else postvars['link_task']
-                # db.update_task(id_, postvars['title'][0], postvars['default_effort'][0], postvars['joined_tags'][0], collect_checked('tag_', postvars), collect_checked('link_', postvars), postvars['comment'][0])
-                db.update_task(id_, postvars['title'][0], postvars['default_effort'][0], postvars['joined_tags'][0], collect_checked('tag_', postvars), links, postvars['comment'][0])
-                # if 'as_todo' in postvars.keys() and id_ not in db.selected_day.todos.keys():
-                #     db.update_todo_mini(id_, db.selected_date, None, False, 1.0)
-                # elif 'as_todo' not in postvars.keys() and id_ in db.selected_day.todos.keys():
-                #     todo = db.selected_day.todos[id_]
-                #     if todo.internals_empty() and (not todo.done) and todo.day_effort is None:
-                #         del db.selected_day.todos[id_]
-                #     else:
-                #         raise PlomException('cannot deselect task as todo of preserve-worthy values')
-
-        elif 'new_day' == site:
+                subtask_ids = [] if not 'subtask' in postvars.keys() else postvars['subtask']
+                default_effort = '0' if not 'default_effort' in postvars.keys() else postvars['default_effort'][0]
+                db.update_task(id_, postvars['title'][0], default_effort, postvars['joined_tags'][0], collect_checked('tag_', postvars), subtask_ids, postvars['comment'][0])
+            redir_params += [('id', id_)]
+
+        elif 'pick_tasks' == site:
             if 'filter' in postvars.keys():
                 redir_params += [('search', postvars['search'][0])]
                 redir_params += [('hide_chosen_tasks', int('hide_chosen_tasks' in postvars.keys()))]
@@ -951,17 +836,7 @@ class TodoHandler(PlomHandler):
                         todo = db.todos[id_]
                         todo.efforts[db.selected_date] = None
 
-            # if 'choose_task' in postvars.keys():
-            #     for i, uuid in enumerate(postvars['choose_task']):
-            #          if not uuid in db.selected_day.todos.keys():
-            #             # task = db.tasks[uuid]
-            #              db.update_todo_mini(uuid, db.selected_date, None, False, 1.0)
-            # if 'choose_todo' in postvars.keys():
-            #     for i, id_ in enumerate(postvars['choose_todo']):
-            #         if not id_ in [todo.id_ for todo in db.selected_day.linked_todos_as_list]:
-            #             db.link_day_with_todo(db.selected_date, id_)
-
-        elif 'do_day' == site:
+        elif 'do_todos' == site:
             if 'filter' in postvars.keys():
                 redir_params += [('hide_done', int('hide_done' in postvars.keys()))]
             else:
@@ -1015,7 +890,6 @@ class TodoHandler(PlomHandler):
             homepage = postvars['return_to'][0]
         else:
             encoded_params = urlencode(redir_params)
-            # homepage = f'{parsed_url.path}?{encoded_params}'
             homepage = f'{site}?{encoded_params}'
         db.write()
         self.redirect(homepage)
@@ -1029,67 +903,30 @@ class TodoHandler(PlomHandler):
         cookie_db = self.get_cookie_db(config['cookie_name'])
         parsed_url = urlparse(self.path)
         site = path_split(parsed_url.path)[1]
-        # params = parse_qs(parsed_url.query)
-        params = ParamsParser(parsed_url.query, cookie_db)
-
-        # def get_param(param_name, boolean=False, chained=False, none_as_empty_string=False):
-        #     if chained:
-        #         param = params.get(param_name, None)
-        #     elif none_as_empty_string:
-        #         param = params.get(param_name, [''])[0]
-        #     else: 
-        #         param = params.get(param_name, [None])[0]
-        #     if (not chained and param == '-') or (chained and param == ['-']):
-        #         param = None
-        #         if param_name in cookie_db.keys():
-        #             del cookie_db[param_name]
-        #     if param is None and param_name in cookie_db.keys():
-        #         param = cookie_db[param_name]
-        #     if param is not None:
-        #         if boolean:
-        #             param = param != '0'
-        #             cookie_db[param_name] = str(int(param))
-        #         else:
-        #             cookie_db[param_name] = param
-        #     elif param is boolean:
-        #         param = False
-        #     return param
 
+        params = ParamsParser(parsed_url.query, cookie_db)
         selected_date = t_filter_and = t_filter_not = None
         hide_unchosen = hide_done = False
-        # return_to = params.get('return_to', [''])[0]
         return_to = params.get('return_to', '')
-        if site in {'do_day', 'new_day'}:
+        if site in {'do_todos', 'pick_tasks'}:
             selected_date = params.get_cookied('date')
-        if site in {'do_day', 'new_day', 'task'}:
+        if site in {'do_todos', 'pick_tasks', 'task', 'tasks'}:
             t_filter_and = params.get_cookied_chain('t_and')
-            # t_filter_not = params.get_cookied_chain('t_not', ['deleted', 'cancelled'])
             t_filter_not = params.get_cookied_chain('t_not')
-        # if site in {'day','tasks', 'task', 'new_day'}:
-        #     t_filter_and = get_param('t_and', chained=True)
-        #     t_filter_not = get_param('t_not', chained=True)
-        if 'do_day' == site:
-            # hide_unchosen = get_param('hide_unchosen', boolean=True)
+        if 'do_todos' == site:
             hide_done = params.get('hide_done', False) 
         db = TodoDB(config['prefix'], selected_date, t_filter_and, t_filter_not, hide_unchosen, hide_done)
-        if 'do_day' == site:
+        if 'do_todos' == site:
             sort_order = params.get_cookied('sort')
-            page = db.show_do_day(sort_order)
-        elif 'new_day' == site:
+            page = db.show_do_todos(sort_order)
+        elif 'pick_tasks' == site:
             sort_order = params.get_cookied('sort')
             hide_chosen_tasks = params.get('hide_chosen_tasks', False)
             search = params.get('search', '')
-            page = db.show_new_day(search, hide_chosen_tasks, sort_order)
+            page = db.show_pick_tasks(search, hide_chosen_tasks, sort_order)
         elif site == 'todo':
             todo_id = params.get('id')
             page = db.show_todo(todo_id, return_to)
-            # todo_id = params.get('id')
-            # if todo_id:
-            #     todo_date, task_uuid = todo_id.split('_') 
-            # else:
-            #     todo_date = params.get('date')
-            #     task_uuid = params.get('task')
-            # page = db.show_todo(task_uuid, todo_date, return_to)
         elif 'task' == site:
             id_ = params.get('id')
             search = params.get('search', '')
@@ -1108,6 +945,7 @@ class TodoHandler(PlomHandler):
             start_date = params.get_cookied('start')
             end_date = params.get_cookied('end')
             page = db.show_calendar(start_date, end_date)
+
         if 'unset_cookie' != site:
             self.set_cookie(config['cookie_name'], config['cookie_path'], cookie_db)
         self.send_HTML(page)
index 476459aedba30f407436579d096f028bfb60916a..92cdff01b7482761f9588358ff075f0b200b4daf 100644 (file)
@@ -6,8 +6,8 @@ input { font-family: monospace; padding: 0em; margin: 0em; }
 </style>
 <body>
 tasks: <a href="tasks">list</a> <a href="add_task">add</a> | day:
-<a href="new_day">new day</a>
-<a href="do_day">do day</a>
+<a href="pick_tasks">pick tasks</a>
+<a href="do_todos">do todos</a>
 | <a href="calendar">calendar</a>
 | <a href="unset_cookie">unset cookie</a>
 <hr />
index 18399d78488e3a2dc00ca491adb6fdc984313e81..82a9ca2e487e16517ef127f7a41859aedede051f 100644 (file)
@@ -6,7 +6,7 @@ tr.day_row td { background-color: #cccccc }
 td.checkbox { width: 0.1em; height: 0.1em; padding: 0em; text-align: center; }
 {% endblock %}
 {% block content %}
-<form action="{{action|e}}" method="POST">
+<form action="calendar" method="POST">
 <p>
 from: <input name="start" {% if start_date %}value="{{ start_date }}"{% endif %} placeholder="yesterday" />
 to: <input name="end" {% if end_date %}value="{{ end_date }}"{% endif %} placeholder="2030-12-31" />
@@ -15,7 +15,7 @@ to: <input name="end" {% if end_date %}value="{{ end_date }}"{% endif %} placeho
 <table>
 {% for date, day in days.items() | sort() %}
 {% if day.weekday == 'Mo' %}<tr class="week_row"><td colspan=3></td></tr>{% endif %}
-<tr class="day_row"><td colspan=3><a href="do_day?date={{date}}&hide_unchosen=1">{{ day.weekday }} {{ date }}</a> |{{ '%04.1f' % day.todos_sum|round(2) }}| {{ day.comment|e }}</td></tr>
+<tr class="day_row"><td colspan=3><a href="do_tasks?date={{date}}&hide_unchosen=1">{{ day.weekday }} {{ date }}</a> |{{ "%5.1f" |format(day.todos_sum)}}| {{ day.comment|e }}</td></tr>
 {% for todo in day.linked_todos_as_list %}
 
 {% if todo.visible %}
diff --git a/todo_templates/do_todos.html b/todo_templates/do_todos.html
new file mode 100644 (file)
index 0000000..7a8ae82
--- /dev/null
@@ -0,0 +1,85 @@
+{% extends 'base.html' %}
+{% block css %}
+table.alternating tr:nth-child(even) {
+    background-color: #cccccc;
+}
+table.alternating tr:nth-child(odd) {
+    background-color: #ffffff;
+}
+th, td { text-align: left}
+th.desc { background: linear-gradient(to bottom, white, grey); }
+th.center { text-align: center; }
+td.checkbox, td.number { height: 0.1em; padding: 0em; text-align: center; }
+td.checkbox { width: 0.1em; text-align: center; }
+td button { height: 1.5em; padding: 0em; margin: 0em }
+input[type="number"] { text-align: right; }
+th.desc { background: linear-gradient(to bottom, white, grey); }
+{% endblock %}
+{% block content %}
+<form action="do_todos" method="POST">
+{% include 'tagfilters.html' %}
+<br />
+<input name="hide_done" type="checkbox" {% if hide_done %}checked{% endif %} /> hide done
+<input type="submit" name="filter" value="filter" />
+</form>
+<h3>do day</h3>
+<form id="form_to_watch" action="do_todos" method="POST">
+<p>
+| <a href="do_todos?date={{prev_date}}">prev</a> | {{day.date}} | <a href="do_todos?date={{next_date}}">next</a> | 
+comment: <input name="day_comment" value="{{day.comment|e}}">
+<input type="submit" value="update">
+<input type="hidden" name="date" value="{{day.date}}" />
+</p>
+<table class="alternating">
+<tr>
+<th {% if sort=='done' %}class="desc"{% endif %}><a href="?sort=done">done</a></th>
+<th class="center{% if sort=='default_effort' %} desc{% endif %}"><a href="?sort=default_effort">effort</a></th>
+<th {% if sort=='importance' %}class ="desc"{% endif %}><a href="?sort=importance">importance</a></th>
+<th {% if sort=='title' %}class="desc"{% endif %}><a href="?sort=title">todo</a></th>
+<th>comment</th>
+{% for todo in todos %}
+<tr>
+<input name="todo_id" value="{{todo.id_}}" type="hidden" >
+<td class="checkbox">
+{% if todo.children %}
+{% if todo.done %}✓{% else %}&nbsp;&nbsp;{% endif %}
+{% else %}
+<input name="done" type="checkbox" value="{{todo.id_}}" {% if todo.done %}checked{% endif %}>
+{% endif %}
+</td>
+<td class="number">
+{% if todo.children %}
+<input type="hidden" name="effort" value="0" />
+{% else %}
+<input class="effort_input" name="effort" type="number" step=0.1 size=7 value="{% if todo.day_effort is not none %}{{todo.day_effort}}{% endif %}" placeholder={{"%.1f"|format(todo.task.default_effort.then)}} >
+{% endif %}
+</td>
+<td class="number"><input name="importance" type="number" step=0.1 size=7 value={{todo.importance}} } ></td>
+<td><a href="todo?id={{todo.id_}}">{{todo.path}}{{todo.title}}</a></td>
+<td><input name="effort_comment" type="text" size=100 value="{{todo.comment|e}}" /></td>
+</tr>
+{% endfor %}
+</table>
+<input type="submit" value="update">
+</form>
+{% include 'watch_form.html' %}
+<script>
+var effort_inputs = document.getElementsByClassName("effort_input");
+for (let i = 0; i < effort_inputs.length; i++) {
+    let input = effort_inputs[i];
+    let button = document.createElement('button');
+    button.innerHTML = '+' + parseFloat(input.placeholder).toFixed(1);
+    button.onclick = function(event) {
+        event.preventDefault();
+        if (input.value) {
+            input.value = parseFloat(input.value) + parseFloat(input.placeholder);
+        } else {
+            input.value = parseFloat(input.placeholder);
+        }
+        input.value = parseFloat(input.value).toFixed(1);
+        formHasChanged = true; 
+    };
+    input.insertAdjacentElement('afterend', button);
+}
+</script>
+{% endblock %}
diff --git a/todo_templates/pick_tasks.html b/todo_templates/pick_tasks.html
new file mode 100644 (file)
index 0000000..ea1b324
--- /dev/null
@@ -0,0 +1,96 @@
+{% extends 'base.html' %}
+{% block css %}
+td.number { text-align: right; }
+table.alternating tr:nth-child(even) {
+    background-color: #cccccc;
+}
+table.alternating tr:nth-child(odd) {
+    background-color: #ffffff;
+}
+th { text-align: left; background-color: white; border: 1px solid black; }
+th.desc { background: linear-gradient(to bottom, white, grey); }
+{% endblock %}
+{% block content %}
+<form action="pick_tasks" method="POST">
+{% include 'tagfilters.html' %}
+<br />
+<input name="hide_chosen_tasks" type="checkbox" {% if hide_chosen_tasks %}checked{% endif %} /> hide chosen tasks<br />
+search: <input name="search" value="{{search|e}}" />
+<input type="submit" name="filter" value="filter" />
+</form>
+
+<h3>pick todos for day</h3>
+<p><a href="pick_tasks?date={{prev_date}}">prev</a> | {{day.date}} | <a href="pick_tasks?date={{next_date}}">next</a> | {{day.comment}}</p>
+<form id="form_to_watch" action="{{action|e}}" method="POST">
+<input type="hidden" name="date" value="{{day.date}}" />
+<input id="pick_upper" type="submit" value="pick" />
+<table class="alternating">
+<tr>
+<th>do</th>
+<th {% if sort=='effort' %}class="desc"{% endif %}><a href="?sort=effort">effort</a></th>
+<th {% if sort=='title' %}class="desc"{% endif %}><a href="?sort=title">what</a></th>
+<th>comment</th>
+</tr>
+{% for todo in chosen_todos %}
+<tr>
+<td class="checkbox">
+<input name="chosen_todo" {% if todo.done or todo.day_effort %}class="protected"{% endif %} type="checkbox" value="{{todo.id_}}" checked>
+</td>
+<td class="number">
+{% if todo.day_effort %}{{todo.day_effort}}&nbsp;{% else %}({{todo.default_effort}}){% endif %}{% if todo.done and not "cancelled" in todo.contemporary_tags%}✓{% else %}&nbsp;{% endif %}</td>
+<td><a href="todo?id={{todo.id_}}">{% if "cancelled" in todo.contemporary_tags %}<s>{% endif %}{% if "deadline" in todo.contemporary_tags %}DEADLINE: {% endif %}<a href="todo?id={{todo.id_}}&return_to=pick_tasks" />{{todo.path|e}}{{todo.title|e}}</a>{%if "cancelled" in todo.contemporary_tags%}</s>{% endif %}</a></td>
+<td>{{todo.comment|e}}</td>
+</tr>
+{% endfor %}
+<tr>
+<th colspan=4>earlier todos to continue</th>
+</tr>
+{% for todo in relevant_todos %}
+<tr>
+<td class="checkbox">
+<input name="choose_todo" type="checkbox" value="{{todo.id_}}">
+</td>
+<td class="number">{{todo.all_days_effort}}/{{todo.default_effort}}</td>
+<td><a href="todo?id={{todo.id_}}">{{todo.day.date}} {{todo.task.title.then|e}}</a></td>
+<td>{{todo.comment|e}}</td>
+</tr>
+{% endfor %}
+<th colspan=4>tasks</th>
+{% for task in tasks %}
+<tr>
+<td class="checkbox">
+<input name="choose_task" type="checkbox" value="{{task.id_}}">
+</td>
+<td class="number">({{task.default_effort.then}})&nbsp;</td>
+<td><a href="task?id={{ task.id_ }}&return_to=pick_tasks" />{{ task.title.then|e }}</a></td>
+<td>{{task.comment|e}}</td>
+</tr>
+{% endfor %}
+</table>
+<input type="hidden" name="search" value="{{search|e}}" />
+<input id="pick_lower" type="submit" value="pick" />
+</form>
+{% include 'watch_form.html' %}
+<script>
+function make_selectables_toggler(neighbor_id, input_name) {
+    var neighbor = document.getElementById(neighbor_id);
+    var button = document.createElement('button');
+    button.textContent = 'toggle selectables';
+    button.onclick = function(event) {
+        event.preventDefault();
+        let checkboxes = document.getElementsByName(input_name);
+        for (let i = 0; i < checkboxes.length; i++) {
+            if (checkboxes[i].classList.contains('protected')) { 
+                continue;      
+       }
+            checkboxes[i].click();
+            formHasChanged = true;
+        }
+    }
+    neighbor.insertAdjacentElement('afterend', button);
+}
+make_selectables_toggler('pick_upper', 'chosen_todo');
+make_selectables_toggler('pick_lower', 'choose_task');
+</script>
+{% endblock %}
+
index 85347efece93587e0f595050af3880ad9ec6e956..11f45377caab0d42ebe1bac09ff7cb7e7f3defb4 100644 (file)
@@ -13,25 +13,34 @@ textarea { width: 100% };
 <input type="hidden" name="id" value="{{ task.id_ }}" />
 <input type="hidden" name="return_to" value="{{ return_to }}" />
 <table>
-<tr><th>title</th><td class="input"><input name="title" type="text" value="{{ task.title|e }}" /><details><summary>history</summary><ul>{% for k,v in task.title_history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details></td></tr>
+<tr><th>title</th><td class="input"><input name="title" type="text" value="{{ task.title.now|e }}" /><details><summary>history</summary><ul>{% for k,v in task.title.history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details></td></tr>
 <tr><th></th><td><input type="checkbox" name="as_todo" {% if selected %}checked{% endif%}> selected for {{db.selected_date}}</td></tr>
 <tr><th>comment</th><td class="input"><textarea name="comment">{{task.comment|e}}</textarea></td></tr>
-<tr><th>default effort</th><td class="input"><input type="number" name="default_effort" value="{{ task.default_effort }}" step=0.1 size=8 required /><details><summary>history</summary><ul>{% for k,v in task.default_effort_history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details></td></tr>
+<tr>
+<th>default effort</th>
+<td class="input">
+{% if task.subtasks %}
+{{ task.default_effort.now }}
+{% else %}
+<input type="number" name="default_effort" value="{{ task.default_effort.now }}" step=0.1 size=8 required /><details><summary>history</summary><ul>{% for k,v in task.default_effort.history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details>
+{% endif %}
+</td>
+</tr>
 <tr><th>tags</th>
 <td>
 {% for tag in db.t_tags | sort %}
-<input type="checkbox" name="tag_{{tag|e}}"{% if tag in task.tags %} checked{% endif %}/> {{ tag }}<br />
+<input type="checkbox" name="tag_{{tag|e}}"{% if tag in task.tags.now %} checked{% endif %}/> {{ tag }}<br />
 {% endfor %}
 add: <input name="joined_tags" type="text" value="" ><br />
-<details><summary>history</summary><ul>{% for k,v in task.tags_history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details>
+<details><summary>history</summary><ul>{% for k,v in task.tags.history.items() | sort(attribute='0', reverse=True) %}<li>{{ k }}: {{ v|e }}{% endfor %}</ul></details>
 </td></tr>
 <tr><th>children</th>
 <td>
 <table>
-{% for subtask in linked_tasks %}
+{% for subtask in task.subtasks %}
 <tr>
-<td><input name="link_task" type="checkbox" value="{{subtask.id_}}" checked/></td>
-<td><a href="task?id={{subtask.id_}}">{{subtask.title}}</a></td>
+<td><input name="subtask" type="checkbox" value="{{subtask.id_}}" checked/></td>
+<td><a href="task?id={{subtask.id_}}">{{subtask.title.now}}</a></td>
 </tr>
 {% endfor %}
 <tr>
@@ -39,8 +48,8 @@ add: <input name="joined_tags" type="text" value="" ><br />
 </tr>
 {% for subtask in filtered_tasks %}
 <tr>
-<td><input name="link_task" type="checkbox" value="{{subtask.id_}}"/></td>
-<td><a href="task?id={{subtask.id_}}">{{subtask.title}}</a></td>
+<td><input name="subtask" type="checkbox" value="{{subtask.id_}}"/></td>
+<td><a href="task?id={{subtask.id_}}">{{subtask.title.now}}</a></td>
 </tr>
 {% endfor %}
 </table>
index 7943b8231eb233630ac3871f0687186960ba8a89..ed01d3071646d0adf58ea6658e23347b4ca5400b 100644 (file)
@@ -10,33 +10,33 @@ td.number { text-align: right; }
 tr.expanded { color: #888888; }
 {% endblock %}
 {% block content %}
-<form action="{{action|e}}" method="POST">
+<form action="tasks" method="POST">
 {% include 'tagfilters.html' %}
 </form>
 <table class="alternating">
 <tr><th>default<br />effort</th><th>task</th><th>tags</th></tr>
 
-{% for uuid, t in db.tasks.items() | sort(attribute='1.title') %}
+{% for uuid, t in db.tasks.items() | sort(attribute='1.title.now') %}
 {% if t.visible %}
 
 <tr>
-<td class="number">{{ t.default_effort }}</a></td>
+<td class="number">{{ t.default_effort.now }}</a></td>
 <td>
 {% if uuid == expand_uuid %}
 <a href="{{db.prefix}}/tasks">[-]</a>
-{% elif t.links|count > 0 %}
+{% elif t.subtasks|count > 0 %}
 <a href="{{db.prefix}}/tasks?expand_uuid={{uuid}}">[+]</a>
 {% endif %}
-<a href="{{db.prefix}}/task?id={{ uuid }}" />{{ t.title|e }}</a></td>
-<td>{% for tag in t.tags | sort %}<a href="{{db.prefix}}/tasks?t_and={{tag|e}}">{{ tag }}</a> {% endfor %}</td>
+<a href="{{db.prefix}}/task?id={{ uuid }}" />{{ t.title.now|e }}</a></td>
+<td>{% for tag in t.tags.now | sort %}<a href="{{db.prefix}}/tasks?t_and={{tag|e}}">{{ tag }}</a> {% endfor %}</td>
 </tr>
 
 {% if uuid == expand_uuid %}
 {% for uuid, t in expanded_tasks.items() %}
 <tr class="expanded">
 <td class="number">{{ t.default_effort }}</a></td>
-<td>&nbsp; [+] <a href="{{db.prefix}}/task?id={{ uuid }}" />{{ t.title|e }}</a></td>
-<td>{% for tag in t.tags | sort %}<a href="{{db.prefix}}/tasks?t_and={{tag|e}}">{{ tag }}</a> {% endfor %}</td>
+<td>&nbsp; [+] <a href="{{db.prefix}}/task?id={{ uuid }}" />{{ t.title.now|e }}</a></td>
+<td>{% for tag in t.tags.now | sort %}<a href="{{db.prefix}}/tasks?t_and={{tag|e}}">{{ tag }}</a> {% endfor %}</td>
 </tr>
 {% endfor %}
 {% endif %}
index 035c9496ddac666e9785c163e1b301e4f097e730..beccb75e2d051d5446b9adce9018ef7b3d5f3dce 100644 (file)
@@ -1,11 +1,8 @@
 {% extends 'base.html' %}
 {% block css %}
 th, td { vertical-align: top; text-align: left}
-td.input { width: 100%; }
 td.checkbox { width: 0.1em; height: 0.1em; padding: 0em; text-align: center; }
 input[type="number"] { text-align: right; }
-input[type="text"] { width: 100% }
-textarea { width: 100% };
 {% endblock %}
 {% block content %}
 <form action="todo" method="POST">
@@ -13,7 +10,7 @@ textarea { width: 100% };
 <input type="hidden" name="todo_id" value="{{todo.id_}}" />
 <input type="hidden" name="return_to" value="{{return_to}}" />
 <table>
-<tr><th>task</th><td><a href="{{db.prefix}}/task?id={{ todo.task.id_ }}">{{ todo.task.title|e }}</a></td></tr>
+<tr><th>task</th><td><a href="{{db.prefix}}/task?id={{ todo.task.id_ }}">{{ todo.task.title.then|e }}</a></td></tr>
 <tr><th>default effort</th><td>{{ todo.default_effort }}</td></tr>
 <tr>
 <th>efforts</th>
@@ -22,48 +19,59 @@ textarea { width: 100% };
 <tr><th>date</th><th>effort</th><th>delete</th>
 {% for date, effort in todo.efforts.items() %}
 <tr>
-<td><input name="effort_date" size=10 value="{{date}}"></td>
-<td><input type="number" name="effort" step=0.1 size=8 value="{{effort}}" placeholder="{{todo.default_effort}}" /></td>
-<td><input type="checkbox" name="delete_effort" value="{{date}}" />
+<td><input name="effort_date" size=10 value="{{date}}" {% if todo.children and effort %}disabled{% endif %}></td>
+<td><input type="number" name="effort" step=0.1 size=8 value="{{effort}}" placeholder="{{todo.default_effort}}" {% if todo.children and effort %}disabled{% endif %} /></td>
+<td>{% if not (todo.children and effort) %}<input type="checkbox" name="delete_effort" value="{{date}}" />{% endif %}</td>
 </tr>
 {% endfor %}
 <tr>
 <td><input name="effort_date" size=10 value=""></td>
-<td><input type="number" name="effort" step=0.1 size=8 value="" placeholder="{{todo.default_effort}}" /></td>
+<td><input type="number" name="effort" step=0.1 size=8 value="" {% if todo.children %} disabled {% else %} placeholder="{{todo.default_effort}}" {% endif %} /></td>
 </tr>
 </table>
 </td>
 </tr>
+<tr>
+<th>total effort</th><td>{{todo.all_days_effort}}</td>
+</tr>
 <tr><th>importance</th><td class="input"><input type="number" name="importance" step=0.1 size=8 value="{{ todo.importance }}" /></td></tr>
-<tr><th>comment</th><td class="input"><textarea name="comment">{{todo.comment|e}}</textarea></td></tr>
-<tr><th>done</th><td class="input"><input type="checkbox" name="done" {% if todo.done %}checked{% endif %}/></td></tr>
+<tr><th>comment</th><td class="input"><textarea name="comment" rows=3 cols=100>{{todo.comment|e}}</textarea></td></tr>
+<tr>
+<th>done</th>
+<td class="input">
+{% if todo.children %}✓{% else %}<input type="checkbox" name="done" {% if todo.done %}checked{% endif %}/>{% endif %}
+</td>
+</tr>
 <tr><th>day tags</th>
 <td>
 {% for tag in db.t_tags | sort %}
-{% if tag in todo.task.tags %}&nbsp;✓{% else %}<input type="checkbox" name="day_tag_{{tag|e}}"{% if tag in todo.day_tags %} checked{% endif %}/>{% endif %} {{ tag }}<br />
+{% if tag in todo.task.tags.now %}&nbsp;✓{% else %}<input type="checkbox" name="day_tag_{{tag|e}}"{% if tag in todo.day_tags %} checked{% endif %}/>{% endif %} {{ tag }}<br />
 {% endfor %}
-add: <input name="joined_day_tags" type="text" value="" >
+add: <input name="joined_day_tags" value="" size=100 >
 </td>
 </tr>
 <tr><th>parent</th><td>{% if todo.parent %}<a href="todo?id={{todo.parent.id_}}">{{todo.parent.title}}</a>{% else %}–{% endif %}</td></tr>
 <tr><th>children</th>
 <td>
 <table>
-{% for todo in linked_todos %}
+{% for todo in child_todos %}
 <tr>
-<td><input name="link_child" type="checkbox" value="{{todo.id_}}" checked/></td>
+       <!-- <td><input name="link_child" type="checkbox" value="{{todo.id_}}" checked disabled/></td> -->
+       <td>{% if todo.done %}✓{% endif %}</td><td>{{todo.all_days_effort}}</td>
 <td><a href="todo?id={{todo.id_}}">{{todo.title}}</a></td>
 </tr>
 {% endfor %}
+<!--
 <tr>
 <th colspan=2>---</th>
 </tr>
 {% for todo in filtered_todos %}
 <tr>
-<td><input name="link_todo" type="checkbox" value="{{todo.id_}}"/></td>
+<td><input name="link_todo" type="checkbox" value="{{todo.id_}}" disabled/></td>
 <td><a href="todo?id={{todo.id_}}">{{todo.title}}</a></td>
 </tr>
 {% endfor %}
+-->
 </table>
 </td>
 </table>