home · contact · privacy
Improve accounting scripts.
[misc] / todo.py
1 from plomlib import PlomDB, run_server, PlomHandler, PlomException
2 import json
3 from uuid import uuid4
4 from datetime import datetime, timedelta
5 from urllib.parse import parse_qs
6 from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader 
7 from urllib.parse import urlparse
8 from os.path import split as path_split
9 db_path = '/home/plom/org/todo_new.json'
10 server_port = 8082
11 DATE_FORMAT = '%Y-%m-%d'
12 j2env = JinjaEnv(loader=JinjaFSLoader('todo_templates'))
13
14 class Task:
15
16     def __init__(self, db, id_, title_history=None, tags_history=None, default_effort_history=None, links_history=None, comment=''):
17         self.id_ = id_
18         self.db = db
19         self.title_history = title_history if title_history else {}
20         self.tags_history = tags_history if tags_history else {}
21         self.default_effort_history = default_effort_history if default_effort_history else {}
22         self.links_history = links_history if links_history else {}
23         self.comment = comment
24         self.visible = True
25
26     def _set_with_history(self, history, value):
27         keys = sorted(history.keys())
28         if len(history) == 0 or value != history[keys[-1]]:
29             history[str(datetime.now())[:19]] = value
30
31     def _last_of_history(self, history, default):
32         keys = sorted(history.keys())
33         return default if 0 == len(history) else history[keys[-1]]
34
35     @classmethod
36     def from_dict(cls, db, d, id_):
37         t = cls(
38                db,
39                id_,
40                d['title_history'],
41                {k: set(v) for k, v in d['tags_history'].items()},
42                d['default_effort_history'],
43                {k: set(v) for k, v in d['links_history'].items()},
44                d['comment'])
45         return t
46
47     def to_dict(self):
48         return {
49             'title_history': self.title_history,
50             'default_effort_history': self.default_effort_history,
51             'tags_history': {k: list(v) for k,v in self.tags_history.items()},
52             'links_history': {k: list(v) for k,v in self.links_history.items()},
53             'comment': self.comment,
54         }
55
56     @property
57     def default_effort(self):
58         return self._last_of_history(self.default_effort_history, 1)
59
60     @default_effort.setter
61     def default_effort(self, default_effort):
62         self._set_with_history(self.default_effort_history, default_effort)
63
64     def default_effort_at(self, queried_date):
65         ret = self.default_effort_history[sorted(self.default_effort_history.keys())[0]]
66         for date_key, default_effort in self.default_effort_history.items():
67             if date_key > f'{queried_date} 23:59:59':
68                 break
69             ret = default_effort
70         return ret
71
72     @property
73     def current_default_effort(self):
74         return self.default_effort_at(self.db.selected_date)
75
76     def matches(self, search):
77         if search is None:
78             return False
79         else:
80             return search in self.title or search in self.comment or search in '$'.join(self.tags) or search in self.title
81
82     @property
83     def title(self):
84         return self._last_of_history(self.title_history, '')
85
86     @title.setter
87     def title(self, title):
88         self._set_with_history(self.title_history, title)
89
90     def title_at(self, queried_date):
91         ret = self.title_history[sorted(self.title_history.keys())[0]]
92         for date_key, title in self.title_history.items():
93             if date_key > f'{queried_date} 23:59:59':
94                 break
95             ret = title
96         return ret
97
98     @property
99     def current_title(self):
100         return self.title_at(self.db.selected_date)
101
102     @property
103     def tags(self):
104         return self._last_of_history(self.tags_history, set())
105
106     @tags.setter
107     def tags(self, tags):
108         self._set_with_history(self.tags_history, set(tags))
109
110     @property
111     def links(self):
112         return self._last_of_history(self.links_history, set())
113
114     @links.setter
115     def links(self, links):
116         self._set_with_history(self.links_history, set(links))
117
118     # @property
119     # def id_(self):
120     #     for k, v in self.db.tasks.items():
121     #         if v == self:
122     #             return k
123
124
125 class Day:
126
127     # def __init__(self, db, todos=None, comment='', date=None):
128     def __init__(self, db, date, comment=''):
129         self.date = date 
130         self.db = db
131         self.comment = comment
132         self.archived = True
133         self.todos = {} # legacy
134         self.linked_todos_as_list = []
135         # if todos:
136         #     for id_, todo_dict in todos.items():
137         #         self.add_todo(id_, todo_dict)
138
139     @classmethod
140     def from_dict(cls, db, d, date=None):
141         # todos = {}
142         comment = d['comment'] if 'comment' in d.keys() else ''
143         day = cls(db, date, comment)
144         # if 'todos' in d.keys():
145         #     for uuid, todo_dict in d['todos'].items():
146         #         day.add_todo(uuid, todo_dict)
147         # if 'linked_todos' in d.keys():
148         for id_ in d['linked_todos']:
149             # if id_ in day.linked_todos.keys():
150             #     continue
151             # if id_ is None:
152             #     continue
153             # linked_todo = db.todos[id_]
154             # linked_todo._day = day 
155             # day.linked_todos_as_list += [linked_todo]
156             day.linked_todos_as_list += [db.todos[id_]]
157         return day
158
159     def to_dict(self):
160         d = {'comment': self.comment, 'linked_todos': []}
161         for todo_id in self.linked_todos.keys():
162             d['linked_todos'] += [todo_id]
163         # for task_uuid, todo in self.todos.items():
164         # for id_, todo in self.todos.items():
165         # #     d['todos'][task_uuid] = todo.to_dict()
166         #     new_type_todo_id = f'{self.date}_{task_uuid}'
167         #     if not new_type_todo_id in d['linked_todos']:
168         #         # d['linked_todos'] += [todo.id_] 
169         #         d['linked_todos'] += [new_type_todo_id] 
170         return d
171
172     # def add_todo(self, task_id, dict_source=None):
173     #     new_type_todo_id = f'{self.date}_{task_id}'
174     #     task = self.db.tasks[task_id]
175     #     if new_type_todo_id in self.db.todos.keys():
176     #         todo = self.db.todos[new_type_todo_id]
177     #     # elif task_id in self.db.keys():
178     #     #     todo = self.db.todos[task_id]
179     #     else:
180     #         # todo = Todo.from_dict(self.db, dict_source, id_=new_type_todo_id) if dict_source else Todo(db=self.db, day=self)
181     #         todo = Todo.from_dict(self.db, dict_source)
182     #     # todo._id = new_type_todo_id
183     #     todo._task = self.db.tasks[task_id] 
184     #     todo._id = new_type_todo_id
185     #     todo._day = self 
186     #     # self.todos[task_id] = todo 
187     #     self.linked_todos_as_list += [todo]
188     #     self.db.todos[new_type_todo_id] = todo
189     #     # self.db.todos[todo.task_id] = [todo]
190     #     # self.db.todos_as_list += [todo]
191     #     # self.db.all_todos[todo.task_id] = todo
192     #     return todo 
193
194     @property
195     def linked_todos(self):
196         linked_todos = {}
197         for todo in self.linked_todos_as_list:
198             linked_todos[todo.id_] = todo
199         return linked_todos
200
201     def _todos_sum(self, include_undone=False):
202         s = 0
203         for todo in [todo for todo in self.linked_todos.values()
204                      if self.date in todo.efforts.keys()]:
205             day_effort = todo.efforts[self.date]
206             if todo.done:
207                 s += day_effort if day_effort else todo.task.default_effort_at(self.date)
208             elif include_undone:
209                 s += day_effort if day_effort else 0 
210         return s
211
212     @property
213     def todos_sum(self):
214         return self._todos_sum()
215
216     @property
217     def todos_sum2(self):
218         return self._todos_sum(True)
219
220     # @property
221     # def date(self):
222     #     if self._date:
223     #         return self._date
224     #     else:
225     #         for k, v in self.db.days.items():
226     #             # print("DEBUG date", k, v)
227     #             if v == self:
228     #                 return k
229     #     print("DEBUG FAIL", self.test_date, self)
230
231
232 class Todo:
233
234     # def __init__(self, db, id_, task, done=False, day_effort=None, comment='', day_tags=None, importance=1.0, efforts=None):
235     def __init__(self, db, id_, task, done=False, comment='', day_tags=None, importance=1.0, efforts=None):
236         self.id_ = id_
237         self.db = db 
238         self.task = task 
239         self.done = done
240         # self.day_effort = day_effort
241         self.efforts = efforts if efforts else {}
242         self.comment = comment
243         self.day_tags = day_tags if day_tags else set()
244         self.importance = importance
245
246     @classmethod
247     def from_dict(cls, db, d, id_):
248         # todo = cls(db, None, None, d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'])
249         # todo._task = db.tasks[d['task']] if 'task' in d.keys() else None
250         # todo._efforts = d['efforts'] if 'efforts' in d.keys() else None
251         # todo = cls(db, id_, db.tasks[d['task']], d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'], d['efforts'])
252         todo = cls(db, id_, db.tasks[d['task']], d['done'], d['comment'], set(d['day_tags']), d['importance'], d['efforts'])
253         return todo
254
255     # @classmethod
256     # def OLD_from_dict(cls, day, d):
257     #     todo = cls(day, d['done'], d['day_effort'], d['comment'], set(d['day_tags']), d['importance'])
258     #     if 'efforts' in d.keys():
259     #         todo._efforts = d['efforts']
260     #     return todo
261
262     def to_dict(self):
263         # 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}
264         return {'task': self.task.id_, 'done': self.done, 'comment': self.comment, 'day_tags': list(self.day_tags), 'importance': self.importance, 'efforts': self.efforts}
265
266     @property
267     def default_effort(self):
268         return self.task.default_effort_at(self.day.date)
269
270     # @property
271     # def effort(self):
272     #     if self.day_effort:
273     #         return self.day_effort
274     #     else:
275     #         return self.day_effort if self.day_effort else self.default_effort
276
277     # @property
278     # def task(self):
279     #     if self._task:
280     #         return self._task
281     #     # else:
282     #     #     for k, v in self.day.todos.items():
283     #     #         if v == self:
284     #     #             return self.db.tasks[k]
285
286     def matches(self, search):
287         if search is None:
288             return False
289         else:
290             return search in self.comment or search in '$'.join(self.tags) or search in self.title
291
292     @property
293     def title(self):
294         return self.task.title_at(self.day.date)
295
296     @property
297     def tags(self):
298         return self.day_tags | self.task.tags
299
300     def internals_empty(self):
301         return len(self.comment) == 0 and len(self.day_tags) == 0
302
303     # def ensure_day_efforts_table(self):
304     #     # 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.
305     #     if not hasattr(self, '_efforts'):
306     #         self._efforts = {} # {self.day.date: self.day_effort}
307
308     # def set_day_effort(self, date, effort):
309     #     self.ensure_day_efforts_table()
310     #     self._efforts[date] = self.day_effort
311
312     @property
313     def day_effort(self):
314         return self.efforts[self.db.selected_date]
315
316     @property
317     def day(self):
318         if len(self.efforts) == 0:
319             return None
320         dates = list(self.efforts.keys())
321         dates.sort()
322         todo_start_date = dates[0]
323         return self.db.days[todo_start_date]
324
325     # @property
326     # def efforts(self):
327     #     self.ensure_day_efforts_table()
328     #     return self._efforts
329
330     # @property
331     # def id_(self):
332     #     if self._id:
333     #         return self._id
334     #     for k, v in self.db.todos.items():
335     #         if v == self:
336     #             return k
337     #     # return f'{self.day.date}_{self.task.id_}'
338
339
340 class TodoDB(PlomDB):
341
342     def __init__(self, prefix, selected_date=None, t_filter_and = None, t_filter_not = None, hide_unchosen=False, hide_done=False):
343         self.prefix = prefix
344         self.selected_date = selected_date if selected_date else str(datetime.now())[:10]
345         self.t_filter_and = t_filter_and if t_filter_and else []
346         self.t_filter_not = t_filter_not if t_filter_not else []
347         self.hide_unchosen = hide_unchosen
348         self.hide_done = hide_done
349         self.days = {}
350         self.tasks = {}
351         self.t_tags = set()
352         self.todos = {}
353         super().__init__(db_path)
354
355     def read_db_file(self, f):
356         d = json.load(f)
357         for id_, t_dict in d['tasks'].items():
358             t = self.add_task(id_=id_, dict_source=t_dict)
359             for tag in t.tags:
360                 self.t_tags.add(tag)
361         # if 'todos' in d.keys():
362         for id_, todo_dict in d['todos'].items():
363             # todo = Todo.from_dict(self, todo_dict, id_)
364             # todo._id = id_ 
365             # self.todos[id_] = todo
366             todo = self.add_todo(todo_dict, id_) # Todo.from_dict(self, todo_dict, id_)
367             self.todos[id_] = todo 
368             for tag in todo.day_tags:
369                 self.t_tags.add(tag)
370         for date, day_dict in d['days'].items():
371             self.add_day(dict_source=day_dict, date=date)
372         # for todo in self.todos.values():
373         # for day in self.days.values():
374         #     for todo in day.todos.values():
375         #         for tag in todo.day_tags:
376         #             self.t_tags.add(tag)
377         self.set_visibilities()
378
379     def set_visibilities(self):
380         for uuid, t in self.tasks.items():
381             t.visible = len([tag for tag in self.t_filter_and if not tag in t.tags]) == 0\
382                     and len([tag for tag in self.t_filter_not if tag in t.tags]) == 0\
383                     and ((not self.hide_unchosen) or uuid in self.selected_day.todos.keys())
384         # for day in self.days.values():
385         #     for todo in day.todos.values():
386         #         todo.visible = len([tag for tag in self.t_filter_and if not tag in todo.day_tags | todo.task.tags ]) == 0\
387         #             and len([tag for tag in self.t_filter_not if tag in todo.day_tags | todo.task.tags ]) == 0\
388         #             and ((not self.hide_done) or (not todo.done))
389         for todo in self.todos.values():
390             todo.visible = len([tag for tag in self.t_filter_and if not tag in todo.day_tags | todo.task.tags ]) == 0\
391                 and len([tag for tag in self.t_filter_not if tag in todo.day_tags | todo.task.tags ]) == 0\
392                 and ((not self.hide_done) or (not todo.done))
393
394     def to_dict(self):
395         d = {'tasks': {}, 'days': {}, 'todos': {}}
396         for uuid, t in self.tasks.items():
397              d['tasks'][uuid] = t.to_dict()
398         for date, day in self.days.items():
399             d['days'][date] = day.to_dict()
400             for todo in day.todos.values():
401                 d['todos'][todo.id_] = todo.to_dict()
402         for id_, todo in self.todos.items():
403             d['todos'][id_] = todo.to_dict()
404         return d
405
406     # @property
407     # def all_todos(self):
408     #     todos = {}
409     #     for todo in self.todos_as_list:
410     #         todos[todo.id_] = todo
411     #     return todos 
412
413     @property
414     def selected_day(self):
415         if not self.selected_date in self.days.keys():
416             self.days[self.selected_date] = self.add_day(date=self.selected_date)
417             # print("DEBUG selected_day", self.days[self.selected_date].date)
418         return self.days[self.selected_date]
419
420     def write(self):
421         dates_to_purge = []
422         for date, day in self.days.items():
423             if len(day.linked_todos) == 0 and len(day.comment) == 0:
424                 dates_to_purge += [date]
425         for date in dates_to_purge:
426             del self.days[date]
427         self.write_text_to_db(json.dumps(self.to_dict()))
428
429     def add_task(self, id_=None, dict_source=None, return_id=False):
430         id_ = id_ if id_ else str(uuid4())
431         t = Task.from_dict(self, dict_source, id_) if dict_source else Task(self, id)
432         self.tasks[id_] = t
433         if return_id:
434             return id_, t
435         else:
436             return t
437
438     def add_todo(self, todo_dict, id_=None):
439         id_ = id_ if id_ else str(uuid4())
440         todo = Todo.from_dict(self, todo_dict, id_)
441         self.todos[id_] = todo 
442         return todo
443
444     def add_day(self, date, dict_source=None):
445         day = Day.from_dict(self, dict_source, date) if dict_source else Day(self, date)
446         self.days[date] = day 
447         return day
448
449     def show_day(self, task_sort=None):
450         task_sort = task_sort if task_sort else 'title' 
451         current_date = datetime.strptime(self.selected_date, DATE_FORMAT)
452         prev_date = current_date - timedelta(days=1)
453         prev_date_str = prev_date.strftime(DATE_FORMAT)
454         next_date = current_date + timedelta(days=1)
455         next_date_str = next_date.strftime(DATE_FORMAT)
456         task_rows = []
457         for uuid, task in self.tasks.items():
458             if not task.visible:
459                 continue
460             todo = None
461             if uuid in self.selected_day.todos.keys():
462                 todo = self.selected_day.todos[uuid]
463                 if not todo.visible:
464                     continue
465             task_rows += [{'uuid': uuid, 'task': task, 'todo': todo}] 
466         if task_sort == 'title':
467             task_rows.sort(key=lambda r: r['task'].title)
468         elif task_sort == 'default_effort':
469             task_rows.sort(key=lambda r: r['task'].default_effort, reverse=True)
470         elif task_sort == 'done':
471             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)
472         elif task_sort == 'importance':
473             task_rows.sort(key=lambda r: 0.0 if not r['todo'] else r['todo'].importance, reverse=True)
474         elif task_sort == 'chosen':
475             task_rows.sort(key=lambda r: False if not r['todo'] else True, reverse=True)
476         elif task_sort == 'comment':
477             task_rows.sort(key=lambda r: '' if not r['todo'] else r['todo'].comment, reverse=True)
478         done_tasks = []
479         for uuid, task in self.tasks.items():
480             if uuid in self.selected_day.todos.keys():
481                 todo = self.selected_day.todos[uuid]
482                 if todo.done:
483                     done_tasks += [todo]
484         done_tasks.sort(key=lambda t: t.effort, reverse=True)
485         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)
486
487     def neighbor_dates(self):
488         current_date = datetime.strptime(self.selected_date, DATE_FORMAT)
489         prev_date = current_date - timedelta(days=1)
490         prev_date_str = prev_date.strftime(DATE_FORMAT)
491         next_date = current_date + timedelta(days=1)
492         next_date_str = next_date.strftime(DATE_FORMAT)
493         return prev_date_str, next_date_str
494
495     def show_do_day(self, sort_order=None):
496         prev_date_str, next_date_str = self.neighbor_dates()
497         # current_date = datetime.strptime(self.selected_date, DATE_FORMAT)
498         # prev_date = current_date - timedelta(days=1)
499         # prev_date_str = prev_date.strftime(DATE_FORMAT)
500         # next_date = current_date + timedelta(days=1)
501         # next_date_str = next_date.strftime(DATE_FORMAT)
502         todos = [t for t in self.selected_day.linked_todos_as_list if t.visible]
503         if sort_order == 'title':
504             todos.sort(key=lambda t: t.task.title)
505         elif sort_order == 'done':
506             todos.sort(key=lambda t: t.day_effort if t.day_effort else t.default_effort if t.done else 0, reverse=True)
507         elif sort_order == 'default_effort':
508             todos.sort(key=lambda t: t.task.default_effort, reverse=True)
509         elif sort_order == 'importance':
510             todos.sort(key=lambda t: t.importance, reverse=True)
511         return j2env.get_template('do_day.html').render(
512                 day=self.selected_day,
513                 prev_date=prev_date_str,
514                 next_date=next_date_str,
515                 todos=todos,
516                 sort=sort_order,
517                 hide_done=self.hide_done)
518
519     def show_calendar(self, start_date_str, end_date_str):
520         self.t_filter_and = ['calendar']
521         self.t_filter_not = ['deleted']
522         self.set_visibilities()
523         days_to_show = {}
524         todays_date_str = str(datetime.now())[:10]
525         todays_date_obj = datetime.strptime(todays_date_str, DATE_FORMAT) 
526         yesterdays_date_obj = todays_date_obj - timedelta(1)
527         yesterdays_date_str = yesterdays_date_obj.strftime(DATE_FORMAT) 
528         start_date_obj = datetime.strptime(sorted(self.days.keys())[0], DATE_FORMAT)
529         if start_date_str and len(start_date_str) > 0:
530             if start_date_str in {'today', 'yesterday'}:
531                 start_date_obj = todays_date_obj if start_date_str == 'today' else yesterdays_date_obj
532             else:
533                 start_date_obj = datetime.strptime(start_date_str, DATE_FORMAT)
534         end_date_obj = datetime.strptime(sorted(self.days.keys())[-1], DATE_FORMAT)
535         if end_date_str and len(end_date_str) > 0:
536             if end_date_str in {'today', 'yesterday'}:
537                 end_date_obj = todays_date_obj if end_date_str == 'today' else yesterdays_date_obj
538             else:
539                 end_date_obj = datetime.strptime(end_date_str, DATE_FORMAT)
540         for n in range(int((end_date_obj - start_date_obj).days) + 1):
541             current_date_obj = start_date_obj + timedelta(n)
542             current_date_str = current_date_obj.strftime(DATE_FORMAT)
543             if current_date_str not in self.days.keys():
544                 days_to_show[current_date_str] = self.add_day(current_date_str)
545             else:
546                 days_to_show[current_date_str] = self.days[current_date_str]
547             days_to_show[current_date_str].weekday = datetime.strptime(current_date_str, DATE_FORMAT).strftime('%A')[:2]
548         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)
549
550     def show_todo(self, id_, return_to):
551         todo = self.todos[id_]
552         # if selected_date not in self.days.keys():
553         #     self.days[selected_date] = self.add_day(test_date=f'3:{selected_date}') 
554         #     # print("DEBUG show_todo", self.days[selected_date].date)
555         # if task_uuid in self.days[selected_date].todos:
556         #     todo = self.days[selected_date].todos[task_uuid]
557         # else:
558         #     todo = self.days[selected_date].add_todo(task_uuid)
559         return j2env.get_template('todo.html').render(db=self, todo=todo, action=self.prefix+'/todo', return_to=return_to)
560
561     def update_todo_mini(self, task_uuid, date, day_effort, done, importance):
562         if date not in self.days.keys():
563             self.days[date] = self.add_day(test_date=f'Y:{date}') 
564             # print("DEBUG update_todo_min", self.days[date].date)
565         if task_uuid in self.days[date].todos.keys():
566             todo = self.days[date].todos[task_uuid]
567         else:
568             todo = self.days[date].add_todo(task_uuid)
569         todo.day_effort = day_effort
570         todo.done = done
571         todo.importance = importance
572         return todo
573
574     def collect_tags(self, tags_joined, tags_checked):
575         tags = set()
576         for tag in [tag.strip() for tag in tags_joined.split(';') if tag.strip() != '']:
577             tags.add(tag)
578         for tag in tags_checked:
579             tags.add(tag)
580         return tags
581
582     def update_todo_for_day(self, id_, date, effort, done, comment, importance):
583         todo = self.todos[id_]
584         todo.done = done
585         todo.efforts[date] = effort 
586         todo.comment = comment 
587         todo.importance = importance 
588
589     def update_todo(self, id_, efforts, done, comment, day_tags_joined, day_tags_checked, importance):
590         if len(efforts) == 0:
591             raise PlomException('todo must have at least one effort!')
592         todo = self.todos[id_]
593         todo.done = done
594         todo.efforts = efforts 
595         for date in todo.efforts:
596             if not date in self.days.keys():
597                 self.add_day(date=date) 
598             if not self in self.days[date].linked_todos_as_list:
599                 self.days[date].linked_todos_as_list += [todo]
600         todo.comment = comment 
601         todo.day_tags = self.collect_tags(day_tags_joined, day_tags_checked) 
602         todo.importance = importance 
603
604     def delete_todo(self, id_):
605         todo = self.todos[id_]
606         for date in todo.efforts.keys():
607             self.delete_effort(todo, date)
608         del self.todos[id_]
609
610     def delete_effort(self, todo, date):
611         if todo in self.days[date].linked_todos_as_list:
612             self.days[date].linked_todos_as_list.remove(todo)
613
614
615     # def update_todo(self, task_uuid, date, day_effort, done, comment, day_tags_joined, day_tags_checked, importance):
616     #     day_effort = float(day_effort) if len(day_effort) > 0 else None
617     #     importance = float(importance)
618     #     todo = self.update_todo_mini(task_uuid, date, day_effort, done, importance)
619     #     todo.comment = comment
620     #     todo.day_tags = self.collect_tags(day_tags_joined, day_tags_checked) 
621
622     def link_day_with_todo(self, date, todo_id):
623         print("DEBUG link", date, todo_id)
624         todo_creation_date, task_uuid = todo_id.split('_')
625         todo = self.days[todo_creation_date].todos[task_uuid]
626         if date in todo.efforts.keys():
627             raise PlomException('todo already linked to respective day')
628         todo.set_day_effort(date, None)
629         if date not in self.days.keys():
630             print("DEBUG link_day_with_todo", date)
631             self.days[date] = self.add_day(test_date=f'Z:{date}') 
632         self.days[date].linked_todos_as_list += [todo]
633         print("DEBUG", date, self.days[date].linked_todos)
634
635     def show_task(self, id_, return_to=''):
636         task = self.tasks[id_] if id_ else self.add_task()
637         selected = id_ in self.selected_day.todos.keys()
638         return j2env.get_template('task.html').render(db=self, task=task, action=self.prefix+'/task', return_to=return_to, selected=selected)
639
640     def update_task(self, id_, title, default_effort, tags_joined, tags_checked, links, comment):
641         task = self.tasks[id_] if id_ in self.tasks.keys() else self.add_task(id_)
642         task.title = title
643         task.default_effort = float(default_effort) if len(default_effort) > 0 else None
644         task.tags = self.collect_tags(tags_joined, tags_checked) 
645         task.links = links
646         for link in links:
647             borrowed_links = self.tasks[link].links 
648             borrowed_links.add(id_)
649             self.tasks[link].links = borrowed_links 
650         task.comment = comment 
651
652     def show_tasks(self, expand_uuid):
653         expanded_tasks = {}
654         if expand_uuid:
655             for uuid in self.tasks[expand_uuid].links:
656                 expanded_tasks[uuid] = self.tasks[uuid]
657         return j2env.get_template('tasks.html').render(db=self, action=self.prefix+'/tasks', expand_uuid=expand_uuid, expanded_tasks=expanded_tasks)
658
659     def new_day(self, search):
660         prev_date_str, next_date_str = self.neighbor_dates()
661         relevant_todos = []
662         for todo in self.todos.values():
663             # if todo.done or (not todo.visible) or (not todo.matches(search)) or todo.day.date == self.selected_day.date:  # TODO or todo is linked by day  
664             if todo.done or (not todo.visible) or (not todo.matches(search)): # or todo.day.date == self.selected_day.date:  # TODO or todo is linked by day  
665                 continue
666             relevant_todos += [todo] 
667         tasks = []
668         for uuid, task in self.tasks.items():
669             if not task.visible or (not task.matches(search)):
670                 continue
671             tasks += [task]
672         return j2env.get_template('new_day.html').render(
673                 day=self.selected_day,
674                 prev_date=prev_date_str,
675                 next_date=next_date_str,
676                 tasks=tasks,
677                 relevant_todos=relevant_todos,
678                 search=search)
679
680
681 class ParamsParser:
682
683     def __init__(self, parsed_url_query, cookie_db):
684         self.params = parse_qs(parsed_url_query)
685         self.cookie_db = cookie_db
686
687     def get(self, key, default=None):
688         boolean = bool == type(default) 
689         param = self.params.get(key, [default])[0]
690         if boolean:
691             param = param != '0' 
692         return param 
693
694     def get_cookied(self, key, default=None):
695         param = self.get(key, default)
696         if param == '-':
697             param = None
698             if key in self.cookie_db.keys():
699                 del self.cookie_db[key]
700         if param is None and key in self.cookie_db.keys():
701             param = self.cookie_db[key]
702         if param is not None:
703             self.cookie_db[key] = param
704         return param
705
706
707 class TodoHandler(PlomHandler):
708
709     def config_init(self):
710         return {
711             'cookie_name': 'todo_cookie',
712             'prefix': '',
713             'cookie_path': '/'
714         }
715
716     def app_init(self, handler):
717         default_path = '/todo'
718         handler.add_route('GET', default_path, self.show_db)
719         handler.add_route('POST', default_path, self.write_db)
720         return 'todo', {'cookie_name': 'todo_cookie', 'prefix': default_path, 'cookie_path': default_path}
721
722     def do_POST(self):
723         self.try_do(self.config_init)
724         self.try_do(self.write_db)
725
726     def write_db(self):
727         from urllib.parse import urlencode
728         config = self.apps['todo'] if hasattr(self, 'apps') else self.config_init()
729         length = int(self.headers['content-length'])
730         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
731         parsed_url = urlparse(self.path)
732         site = path_split(urlparse(self.path).path)[1]
733         # site = path_split(parsed_url.path)[1]
734         db = TodoDB(prefix=config['prefix'])
735         redir_params = []
736         # for param_name, filter_db_name in {('t_and', 't_filter_and'), ('t_not', 't_filter_not')}:
737         #     filter_db = getattr(db, filter_db_name)
738         #     if param_name in postvars.keys():
739         #         for target in postvars[param_name]:
740         #             if len(target) > 0 and not target in filter_db:
741         #                 filter_db += [target]
742         #         if len(filter_db) == 0:
743         #             redir_params += [(param_name, '-')]
744         #     redir_params += [(param_name, f) for f in filter_db]
745
746         def collect_checked(prefix, postvars):
747             tags_checked = []
748             for k in postvars.keys():
749                 if k.startswith(prefix):
750                     tags_checked += [k[len(prefix):]]
751             return tags_checked
752         
753         if 'calendar' == site:
754             redir_params += [('start', postvars['start'][0] if len(postvars['start'][0]) > 0 else '-')]
755             redir_params += [('end', postvars['end'][0] if len(postvars['end'][0]) > 0 else '-')]
756
757         elif 'todo' == site:
758             # task_uuid = postvars['task_uuid'][0]
759             todo_id = postvars['todo_id'][0]
760             # date = postvars['date'][0]
761             old_todo = db.todos[todo_id] 
762             efforts = {}
763             for i, date in enumerate(postvars['effort_date']):
764                 if '' == date:
765                     continue
766                 efforts[date] = postvars['effort'][i] if len(postvars['effort'][i]) > 0 else None 
767             if 'delete_effort' in postvars.keys():
768                 for date in postvars['delete_effort']:
769                     del efforts[date]
770             if 'delete' in postvars.keys():
771                 # old_todo = db.days[date].todos[task_uuid]
772                 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:
773                     raise PlomException('will not remove todo of preserve-worthy values')
774                 db.delete_todo(todo_id) 
775                 # db.write()
776                 # self.redirect('new_day')
777             else:
778                 # redir_params += [('task', task_uuid), ('date', date)]
779                 redir_params += [('id', todo_id)]
780                 # 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])
781                 # efforts = {}
782                 # for i, date in enumerate(postvars['effort_date']):
783                 #     if '' == date:
784                 #         continue
785                 #     efforts[date] = postvars['effort'][i] if len(postvars['effort'][i]) > 0 else None 
786                 if 'delete_effort' in postvars.keys():
787                     for date in postvars['delete_effort']:
788                         db.delete_effort(old_todo, date)
789                 db.update_todo(id_=todo_id,
790                                efforts=efforts,
791                                done='done' in postvars.keys(),
792                                comment=postvars['comment'][0],
793                                day_tags_joined=postvars['joined_day_tags'][0],
794                                day_tags_checked=collect_checked('day_tag_', postvars),
795                                importance=float(postvars['importance'][0]))
796
797         elif 'task' == site:
798             id_ = postvars['id'][0]
799             redir_params += [('id', id_)]
800             if 'title' in postvars.keys():
801                 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])
802                 if 'as_todo' in postvars.keys() and id_ not in db.selected_day.todos.keys():
803                     db.update_todo_mini(id_, db.selected_date, None, False, 1.0)
804                 elif 'as_todo' not in postvars.keys() and id_ in db.selected_day.todos.keys():
805                     todo = db.selected_day.todos[id_]
806                     if todo.internals_empty() and (not todo.done) and todo.day_effort is None:
807                         del db.selected_day.todos[id_]
808                     else:
809                         raise PlomException('cannot deselect task as todo of preserve-worthy values')
810
811         elif 'new_day' == site:
812             redir_params += [('search', postvars['search'][0])]
813             if 'choose_task' in postvars.keys():
814                 for i, uuid in enumerate(postvars['choose_task']):
815                      if not uuid in db.selected_day.todos.keys():
816                         # task = db.tasks[uuid]
817                          db.update_todo_mini(uuid, db.selected_date, None, False, 1.0)
818             if 'choose_todo' in postvars.keys():
819                 for i, id_ in enumerate(postvars['choose_todo']):
820                     if not id_ in [todo.id_ for todo in db.selected_day.linked_todos_as_list]:
821                         db.link_day_with_todo(db.selected_date, id_)
822
823         elif 'do_day' == site:
824             if 'filter' in postvars.keys():
825                 redir_params += [('hide_done', int('hide_done' in postvars.keys()))]
826             else:
827                 db.selected_date = postvars['date'][0]
828                 redir_params += [('date', db.selected_date)]
829                 db.selected_day.comment = postvars['day_comment'][0]
830                 if 'todo_id' in postvars.keys():
831                     for i, todo_id in enumerate(postvars['todo_id']):
832                         old_todo = None if not todo_id in db.todos.keys() else db.todos[todo_id]
833                         done = ('done' in postvars) and (todo_id in postvars['done'])
834                         day_effort_input = postvars['effort'][i]
835                         day_effort = float(day_effort_input) if len(day_effort_input) > 0 else None
836                         comment = postvars['effort_comment'][i]
837                         importance = float(postvars['importance'][i])
838                         if old_todo and old_todo.done == done and old_todo.day_effort == day_effort and comment == old_todo.comment and old_todo.importance == importance:
839                             continue
840                         db.update_todo_for_day(todo_id, db.selected_date, day_effort, done, comment, importance)
841
842         elif 'day' == site:
843             # always store the two hide params in the URL if possible … TODO: find out if really necessary
844             if 'expect_unchosen_done' in postvars.keys():
845                 redir_params += [('hide_unchosen', int('hide_unchosen' in postvars.keys()))] + [('hide_done', int('hide_done' in postvars.keys()))]
846
847             if 'date' in postvars.keys():
848                 db.selected_date = postvars['date'][0]
849                 if 'day_comment' in postvars.keys():
850                     db.selected_day.comment = postvars['day_comment'][0]
851                 redir_params += [('date', db.selected_date)]
852
853                 # handle todo list updates via task UUIDs
854                 if 't_uuid' in postvars.keys():
855                     for i, uuid in enumerate(postvars['t_uuid']):
856                         task = db.tasks[uuid]
857                         old_todo = None if not uuid in db.selected_day.todos.keys() else db.selected_day.todos[uuid]
858                         selects_as_todo = 'choose' in postvars and uuid in postvars['choose']
859                         too_much_keepworthy_data = ('done' in postvars and uuid in postvars['done']) or postvars['day_effort'][i] != '' or (old_todo and not old_todo.internals_empty())
860                         if old_todo and too_much_keepworthy_data and not selects_as_todo:
861                             raise PlomException('cannot deselect task as todo of preserve-worthy values')
862                         elif old_todo and not selects_as_todo:
863                             del db.selected_day.todos[uuid]
864                         elif too_much_keepworthy_data or selects_as_todo:
865                             done = ('done' in postvars) and (uuid in postvars['done'])
866                             day_effort_input = postvars['day_effort'][i]
867                             day_effort = float(day_effort_input) if len(day_effort_input) > 0 else None
868                             importance = float(postvars['importance'][i])
869                             if old_todo and old_todo.done == done and old_todo.day_effort == day_effort and old_todo.importance == importance:
870                                 continue
871                             db.update_todo_mini(uuid, db.selected_date, day_effort, done, importance)
872
873         if 'return_to' in postvars.keys() and len(postvars['return_to'][0]) > 0:
874             homepage = postvars['return_to'][0]
875         else:
876             encoded_params = urlencode(redir_params)
877             # homepage = f'{parsed_url.path}?{encoded_params}'
878             homepage = f'{site}?{encoded_params}'
879         db.write()
880         self.redirect(homepage)
881
882     def do_GET(self):
883         self.try_do(self.config_init)
884         self.try_do(self.show_db)
885
886     def show_db(self):
887         config = self.apps['todo'] if hasattr(self, 'apps') else self.config_init()
888         cookie_db = self.get_cookie_db(config['cookie_name'])
889         parsed_url = urlparse(self.path)
890         site = path_split(parsed_url.path)[1]
891         # params = parse_qs(parsed_url.query)
892         params = ParamsParser(parsed_url.query, cookie_db)
893
894         # def get_param(param_name, boolean=False, chained=False, none_as_empty_string=False):
895         #     if chained:
896         #         param = params.get(param_name, None)
897         #     elif none_as_empty_string:
898         #         param = params.get(param_name, [''])[0]
899         #     else: 
900         #         param = params.get(param_name, [None])[0]
901         #     if (not chained and param == '-') or (chained and param == ['-']):
902         #         param = None
903         #         if param_name in cookie_db.keys():
904         #             del cookie_db[param_name]
905         #     if param is None and param_name in cookie_db.keys():
906         #         param = cookie_db[param_name]
907         #     if param is not None:
908         #         if boolean:
909         #             param = param != '0'
910         #             cookie_db[param_name] = str(int(param))
911         #         else:
912         #             cookie_db[param_name] = param
913         #     elif param is boolean:
914         #         param = False
915         #     return param
916
917         selected_date = t_filter_and = t_filter_not = None
918         hide_unchosen = hide_done = False
919         # return_to = params.get('return_to', [''])[0]
920         return_to = params.get('return_to', '')
921         if site in {'day', 'do_day', 'new_day'}:
922             selected_date = params.get_cookied('date')
923         # if site in {'day','tasks', 'task', 'new_day'}:
924         #     t_filter_and = get_param('t_and', chained=True)
925         #     t_filter_not = get_param('t_not', chained=True)
926         if site in {'day', 'do_day'}:
927             # hide_unchosen = get_param('hide_unchosen', boolean=True)
928             hide_done = params.get('hide_done', False) 
929         db = TodoDB(config['prefix'], selected_date, t_filter_and, t_filter_not, hide_unchosen, hide_done)
930         if 'day' == site:
931             pass
932         elif 'do_day' == site:
933             sort_order = params.get_cookied('sort')
934             page = db.show_do_day(sort_order)
935         elif site == 'todo':
936             todo_id = params.get('id')
937             page = db.show_todo(todo_id, return_to)
938             # todo_id = params.get('id')
939             # if todo_id:
940             #     todo_date, task_uuid = todo_id.split('_') 
941             # else:
942             #     todo_date = params.get('date')
943             #     task_uuid = params.get('task')
944             # page = db.show_todo(task_uuid, todo_date, return_to)
945         elif 'task' == site:
946             id_ = params.get('id')
947             page = db.show_task(id_, return_to)
948         elif 'tasks' == site:
949             expand_uuid = params.get('expand_uuid')
950             page = db.show_tasks(expand_uuid)
951         elif 'add_task' == site:
952             page = db.show_task(None)
953         elif 'new_day' == site:
954             search = params.get('search', '')
955             page = db.new_day(search)
956         elif 'unset_cookie' == site:
957             page = 'no cookie to unset.'
958             if len(cookie_db) > 0:
959                 self.unset_cookie(config['cookie_name'], config['cookie_path'])
960                 page = 'cookie unset!'
961         else:
962             start_date = params.get_cookied('start')
963             end_date = params.get_cookied('end')
964             page = db.show_calendar(start_date, end_date)
965         if 'unset_cookie' != site:
966             self.set_cookie(config['cookie_name'], config['cookie_path'], cookie_db)
967         self.send_HTML(page)
968
969
970 if __name__ == "__main__":
971     run_server(server_port, TodoHandler)