From: Christian Heller Date: Mon, 4 Dec 2023 20:16:55 +0000 (+0100) Subject: Add todo accounting. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/%7B%7B%20web_path%20%7D%7D/static/git-favicon.png?a=commitdiff_plain;h=2960beb644986049fd975eac8e348ca61ab122b8;p=misc Add todo accounting. --- diff --git a/todo.py b/todo.py new file mode 100644 index 0000000..08fb8b8 --- /dev/null +++ b/todo.py @@ -0,0 +1,314 @@ +from plomlib import PlomDB, run_server, PlomHandler, PlomException +import json +from uuid import uuid4 +from datetime import datetime +from urllib.parse import parse_qs +db_path = '/home/plom/org/todo_new.json' +# db_path = '/home/plom/public_repos/misc/todo_new.json' +server_port = 8082 + +tmpl = """ + + +
+ +mandatory tags: {% for t_tag in db.t_tags | sort %} +{{ t_tag }} +{% endfor %} +
+forbidden tags: {% for t_tag in db.t_tags | sort %} +{{ t_tag }} +{% endfor %} + + + +{% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} +{% if t.visible %} + + + + + + + + +{% endif %} +{% endfor %} +
datearchive?{{ db.today.todos_sum|round(2) }}comment:
weighttitletagstoday?done?day weight
+
+ + + +{% for date, day in db.old_days.items() | sort(reverse=True) %} + +{% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} + +{% endfor %} +{% endfor %} +
{{ date }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
+
+""" + +class Task: + + def __init__(self, title_history={}, tags_history={}, default_weight_history={}): + self.title_history = title_history.copy() + self.tags_history = tags_history.copy() + self.default_weight_history = default_weight_history.copy() + 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]] + + @classmethod + def from_dict(cls, d): + return cls( + d['title_history'], + {k: set(v) for k, v in d['tags_history'].items()}, + d['default_weight_history']) + + def tags_from_joined_string(self, tags_string): + tags = set() + for tag in [tag.strip() for tag in tags_string.split(';') if tag.strip() != '']: + tags.add(tag) + self.set_tags(tags) + + @property + def tags_joined(self): + return ';'.join(sorted(list(self.tags))) + + def set_default_weight(self, default_weight): + self._set_with_history(self.default_weight_history, default_weight) + + @property + def default_weight(self): + return self._last_of_history(self.default_weight_history, 1) + + def set_title(self, title): + self._set_with_history(self.title_history, title) + + @property + def title(self): + return self._last_of_history(self.title_history, '') + + def set_tags(self, tags): + self._set_with_history(self.tags_history, set(tags)) + + @property + def tags(self): + return self._last_of_history(self.tags_history, set()) + + def to_dict(self): + return { + 'title_history': self.title_history, + 'tags_history': {k: list(v) for k,v in self.tags_history.items()}, + 'default_weight_history': self.default_weight_history} + + +class Day: + + def __init__(self, todos, comment=''): + self.todos = todos + self.comment = comment + + @classmethod + def from_dict(cls, d): + todos = {} + comment = d['comment'] if 'comment' in d.keys() else '' + for uuid, todo_dict in d['todos'].items(): + todos[uuid] = Todo.from_dict(todo_dict) + return cls(todos, comment) + + @property + def todos_sum(self): + s = 0 + for todo in [todo for todo in self.todos.values() if (todo.done or todo.day_weight)]: + s += todo.weight + return s + + def to_dict(self): + d = {'comment': self.comment, 'todos': {}} + for task_uuid, todo in self.todos.items(): + d['todos'][task_uuid] = todo.to_dict() + return d + + +class Todo: + + def __init__(self, done=False, day_weight=None): + self.done = done + self.day_weight = day_weight + + @classmethod + def from_dict(cls, d): + return cls(d['done'], d['day_weight']) + + def to_dict(self): + return {'done': self.done, 'day_weight': self.day_weight} + + +class TodoDB(PlomDB): + + def __init__(self, t_filter_and = set(), t_filter_not = set()): + self.t_filter_and = t_filter_and + self.t_filter_not = t_filter_not + self.old_days = {} + self.tasks = {} + self.reset_today() + self.t_tags = set() + super().__init__(db_path) + + def read_db_file(self, f): + d = json.load(f) + self.today = Day.from_dict(d['today']) + self.today_date = d['today_date'] + for uuid, t_dict in d['tasks'].items(): + t = Task.from_dict(t_dict) + self.tasks[uuid] = t + 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 + for tag in t.tags: + self.t_tags.add(tag) + for date, day_dict in d['old_days'].items(): + self.old_days[date] = Day.from_dict(day_dict) + + def to_dict(self): + d = { + 'today': self.today.to_dict(), + 'today_date': self.today_date, + 't_filter_and': list(self.t_filter_and), + 't_filter_not': list(self.t_filter_not), + 'tasks': {}, + 'old_days': {} + } + for uuid, t in self.tasks.items(): + d['tasks'][uuid] = t.to_dict() + for date, day in self.old_days.items(): + d['old_days'][date] = day.to_dict() + return d + + def write(self): + self.write_text_to_db(json.dumps(self.to_dict())) + + def reset_today(self, date=None): + if date: + self.today_date = date + self.today = self.old_days[date] + del self.old_days[date] + else: + self.today_date = str(datetime.now())[:10] + self.today = Day({}) + + +class TodoHandler(PlomHandler): + + def app_init(self, handler): + default_path = '/todo' + handler.add_route('GET', default_path, self.show_db) + handler.add_route('POST', default_path, self.write_db) + return 'todo', default_path + + def do_POST(self): + self.try_do(self.write_db) + + def write_db(self): + from urllib.parse import urlencode + db = TodoDB() + length = int(self.headers['content-length']) + postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + + import pprint + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(postvars) + + db.t_filter_and = set() + db.t_filter_not = set() + if 't_filter_and' in postvars.keys(): + for target in postvars['t_filter_and']: + db.t_filter_and.add(target) + if 't_filter_not' in postvars.keys(): + for target in postvars['t_filter_not']: + db.t_filter_not.add(target) + if 't_uuid' in postvars.keys(): + new_postvars_t_uuid = postvars['t_uuid'].copy() + for i, uuid in enumerate(postvars['t_uuid']): + if len(uuid) < 36 and len(postvars['t_title'][i]) > 0: + t = Task() + new_uuid = str(uuid4()) + db.tasks[new_uuid] = t + new_postvars_t_uuid[i] = new_uuid + for key in [k for k in postvars.keys() if not k == 't_uuid']: + if uuid in postvars[key]: + uuid_index = postvars[key].index(uuid) + postvars[key][uuid_index] = new_uuid + postvars['t_uuid'] = new_postvars_t_uuid + for i, uuid in enumerate(postvars['t_uuid']): + if len(uuid) < 36: + continue + t = db.tasks[uuid] + t.set_title(postvars['t_title'][i]) + t.tags_from_joined_string(postvars['t_tags'][i]) + t.set_default_weight(float(postvars['t_default_weight'][i])) + if uuid in db.today.todos.keys() and ((not 'do_today' in postvars) or uuid not in postvars['do_today']): + del db.today.todos[uuid] + if 'do_today' in postvars.keys(): + for i, uuid in enumerate(postvars['t_uuid']): + if uuid in postvars['do_today']: + done = 'done' in postvars and uuid in postvars['done'] + day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None + db.today.todos[uuid] = Todo(done, day_weight) + db.today_date = postvars['today_date'][0] + db.today.comment = postvars['comment'][0] + switch_edited_day = None + for date in db.old_days.keys(): + if f'edit_{date}' in postvars.keys(): + switch_edited_day = date + break + if 'archive_today' in postvars.keys() or switch_edited_day: + if db.today_date in db.old_days.keys(): + raise PlomException('cannot use same date twice') + db.old_days[db.today_date] = db.today + if switch_edited_day: + db.reset_today(date) + else: + db.reset_today() + db.write() + homepage = self.apps['todo'] if hasattr(self, 'apps') else self.homepage + data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + encoded_params = urlencode(data) + homepage += '?' + encoded_params + self.redirect(homepage) + + def do_GET(self): + self.try_do(self.show_db) + + def show_db(self): + from jinja2 import Template + from urllib.parse import urlparse + parsed_url = urlparse(self.path) + params = parse_qs(parsed_url.query) + t_filter_and = set(params.get('t_and', [])) + t_filter_not = set(params.get('t_not', ['deleted'])) + db = TodoDB(t_filter_and, t_filter_not) + for i in range(10): + db.tasks[f'new{i}'] = Task() + for task_uuid, todo in db.today.todos.items(): + todo.weight = todo.day_weight if todo.day_weight else db.tasks[task_uuid].default_weight + for date, day in db.old_days.items(): + for task_uuid, todo in day.todos.items(): + todo.title = db.tasks[task_uuid].title + todo.weight = todo.day_weight if todo.day_weight else db.tasks[task_uuid].default_weight + page = Template(tmpl).render(db=db) + self.send_HTML(page) + + +if __name__ == "__main__": + run_server(server_port, TodoHandler)