From 389620d37f697f869ed051071db5dead95a152fe Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Thu, 23 Nov 2023 04:37:01 +0100 Subject: [PATCH 01/16] Add script to unify all accounting servers into one. --- calories.py | 26 ++- income_progress_bars.py | 25 ++- ledger.py | 423 +++++++++++++++++++++------------------- plomlib.py | 22 +-- unite.py | 62 ++++++ 5 files changed, 330 insertions(+), 228 deletions(-) create mode 100644 unite.py diff --git a/calories.py b/calories.py index 88165b8..4352567 100644 --- a/calories.py +++ b/calories.py @@ -2,7 +2,7 @@ import os import json import datetime import jinja2 -from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer +from plomlib import PlomDB, PlomException, run_server, PlomHandler db_path = '/home/plom/org/calories_db.json' @@ -16,7 +16,7 @@ td.number { text-align: right; } input[type="number"] { text-align: right; } -
+ @@ -224,9 +224,18 @@ class CaloriesDB(PlomDB): self.write_text_to_db(json.dumps(self.to_dict())) -class CaloriesServer(PlomServer): +class ConsumptionsHandler(PlomHandler): + + def app_init(self, handler): + default_path = '/consumptions' + handler.add_route('GET', default_path, self.show_db) + handler.add_route('POST', default_path, self.write_db) + return 'consumptions', default_path def do_POST(self): + self.write_db() + + def write_db(self): from uuid import uuid4 from urllib.parse import parse_qs length = int(self.headers['content-length']) @@ -283,13 +292,16 @@ class CaloriesServer(PlomServer): break try: db.write() - self.redirect() + homepage = self.apps['consumptions'] if hasattr(self, 'apps') else self.homepage + self.redirect(homepage) except PlomException as e: self.fail_400(e) def do_GET(self): + self.show_db() + + def show_db(self): db = CaloriesDB() - # eatables = "" eatable_rows = [] for k,v in db.eatables.items(): eatable_rows += [{ @@ -320,7 +332,9 @@ class CaloriesServer(PlomServer): 'cals': f'{day.calories:.1f}', 'sugar': f'{day.sugar_g:.1f}', }] + homepage = self.apps['consumptions'] if hasattr(self, 'apps') else self.homepage page = jinja2.Template(tmpl).render( + homepage = homepage, db=db, days=day_rows, consumptions=consumption_rows, @@ -330,4 +344,4 @@ class CaloriesServer(PlomServer): if __name__ == "__main__": - run_server(server_port, CaloriesServer) + run_server(server_port, ConsumptionsHandler) diff --git a/income_progress_bars.py b/income_progress_bars.py index 553627d..60f7104 100644 --- a/income_progress_bars.py +++ b/income_progress_bars.py @@ -1,7 +1,7 @@ import os import json import jinja2 -from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer +from plomlib import PlomDB, PlomException, run_server, PlomHandler server_port = 8083 db_path = '/home/plom/org/income.json' @@ -84,7 +84,7 @@ table { {% endfor %}
eatableunit countunit weight (g)caloriessugar (g)
{{p.diff_goal}}
- + @@ -161,9 +161,18 @@ class ProgressBar: self.success = success_income / time_progress -class IncomeServer(PlomServer): +class IncomeProgressHandler(PlomHandler): + + def app_init(self, handler): + default_path = '/income_progress' + handler.add_route('GET', default_path, self.display_income_progress) + handler.add_route('POST', default_path, self.post_income_update) + return 'income_progress', default_path def do_POST(self): + self.post_income_update() + + def post_income_update(self): from urllib.parse import parse_qs try: length = int(self.headers['content-length']) @@ -188,11 +197,15 @@ class IncomeServer(PlomServer): db.workday_minutes_worked_2 = 0 db.workday_minutes_worked_3 = 0 db.write_db() - self.redirect() + homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage + self.redirect(homepage) except PlomException as e: self.fail_400(e) def do_GET(self): + self.display_income_progress() + + def display_income_progress(self): import datetime import calendar try: @@ -235,7 +248,9 @@ class IncomeServer(PlomServer): ProgressBar("month", month_plus, month_goal, progress_time_month), ProgressBar("week", week_plus, week_goal, progress_time_week), ProgressBar("workday", day_income, workday_goal)] + homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage page = jinja2.Template(tmpl).render( + homepage = homepage, progress_bars = progress_bars, workday_hourly_rate_1 = db.workday_hourly_rate_1, workday_minutes_worked_1 = db.workday_minutes_worked_1, @@ -256,4 +271,4 @@ class IncomeServer(PlomServer): if __name__ == "__main__": - run_server(server_port, IncomeServer) + run_server(server_port, IncomeProgressHandler) diff --git a/ledger.py b/ledger.py index 244a684..87d219e 100755 --- a/ledger.py +++ b/ledger.py @@ -3,7 +3,7 @@ import jinja2 import decimal from datetime import datetime, timedelta from urllib.parse import parse_qs, urlparse -from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer +from plomlib import PlomDB, PlomException, run_server, PlomHandler server_port = 8082 db_path = '/home/plom/org/ledger2023.dat' @@ -20,20 +20,20 @@ input[type=number] { text-align: right; font-family: monospace; } .full_line_comment { display: block; white-space: nowrap; width: 0; } -ledger -balance -add free -add structured +ledger +balance +add free +add structured
""" booking_html = """ -

{{date}} {{desc}} {{head_comment|e}}
-[edit: structured -/ free -| copy:structured -/ free -| move {% if move_up %}up{% else %}up{% endif %}/{% if move_down %}down{% else %}down{% endif %} -| balance after +

{{date}} {{desc}} {{head_comment|e}}
+[edit: structured +/ free +| copy:structured +/ free +| move {% if move_up %}up{% else %}up{% endif %}/{% if move_down %}down{% else %}down{% endif %} +| balance after ]

hourly rateworked today
€ minutes
{% for l in booking_lines %} @@ -333,7 +333,8 @@ class Booking: class LedgerDB(PlomDB): - def __init__(self): + def __init__(self, prefix): + self.prefix = prefix self.bookings = [] self.comments = [] self.real_lines = [] @@ -353,9 +354,11 @@ class LedgerDB(PlomDB): def insert_at_date(self, lines, date): start_at = len(self.real_lines) + print("DEBUG triggered insert_at_date", date, start_at) for b in self.bookings: if b.date_string == date: start_at = b.start_line + print("DEBUG setting start_at to", start_at) break elif b.date_string > date: break @@ -364,6 +367,7 @@ class LedgerDB(PlomDB): return self.write_lines_in_total_lines_at(self.real_lines, start_at, lines) def update(self, start, end, lines, date): + print("DEBUG update", date) total_lines = self.real_lines[:start] + self.real_lines[end:] n_original_lines = end - start start_at = len(total_lines) @@ -512,184 +516,21 @@ class LedgerDB(PlomDB): ret += [f' {acc_buffer} {-final_minus} € ; assume as to earn in year: {acc_buffer} + {12 - months_passed - 1} * this = {year_needed}'] return ret - -class LedgerServer(PlomServer): - - def pre_init(self): - self.html_head += [html_head] - - def do_POST(self): - try: - parsed_url = urlparse(self.path) - length = int(self.headers['content-length']) - postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - start = int(postvars['start'][0]) - end = int(postvars['end'][0]) - db = LedgerDB() - add_empty_line = None - lines = [] - # get inputs - if '/add_structured' == parsed_url.path and not 'revert' in postvars.keys(): - lines, add_empty_line = self.booking_lines_from_postvars(postvars, db) - elif '/add_free' == parsed_url.path and not 'revert' in postvars.keys(): - lines = postvars['booking'][0].splitlines() - # validate where appropriate - if ('save' in postvars.keys()) or ('check' in postvars.keys()): - _, _ = parse_lines(lines) - # if saving, process where to and where to redirect after - if 'save' in postvars.keys(): - last_date = str(datetime.now())[:10] - if len(db.bookings) > 0: - last_date = db.bookings[-1].date_string - target_date = last_date[:] - first_line_tokens = lines[0].split() if len(lines) > 0 else '' - first_token = first_line_tokens[0] if len(first_line_tokens) > 0 else '' - try: - datetime.strptime(first_token, '%Y-%m-%d') - target_date = first_token - except ValueError: - pass - if start == end == 0: - start = db.insert_at_date(lines, target_date) - nth = db.get_nth_for_booking_of_start_line(start) - else: - new_start = db.update(start, end, lines, target_date) - nth = db.get_nth_for_booking_of_start_line(new_start) - if new_start > start: - nth -= 1 - self.redirect( f'/#{nth}') - # otherwise just re-build editing form - else: - if '/add_structured' == parsed_url.path: - edit_content = self.add_structured(db, start, end, temp_lines=lines, add_empty_line=add_empty_line) - else: - edit_content = self.add_free(db, start, end) - self.send_HTML(edit_content) - except PlomException as e: - self.fail_400(e) - - def do_GET(self): - try: - parsed_url = urlparse(self.path) - params = parse_qs(parsed_url.query) - start = int(params.get('start', ['0'])[0]) - end = int(params.get('end', ['0'])[0]) - db = LedgerDB() - if parsed_url.path == '/balance': - stop = params.get('stop', [None])[0] - page = self.balance_as_html(db, stop) - elif parsed_url.path == '/add_free': - page = self.add_free(db, start, end) - elif parsed_url.path == '/add_structured': - page = self.add_structured(db, start, end) - elif parsed_url.path == '/copy_free': - page = self.add_free(db, start, end, copy=True) - elif parsed_url.path == '/copy_structured': - page = self.add_structured(db, start, end, copy=True) - elif parsed_url.path == '/move_up': - nth = self.move_up(db, start, end) - self.redirect(f'/#{nth}') - return - elif parsed_url.path == '/move_down': - nth = self.move_down(db, start, end) - self.redirect(f'/#{nth}') - return - else: - page = self.ledger_as_html(db) - self.send_HTML(page) - except PlomException as e: - self.fail_400(e) - - def booking_lines_from_postvars(self, postvars, db): - add_empty_line = None - date = postvars['date'][0] - description = postvars['description'][0] - start_comment = postvars['line_0_comment'][0] - start_line = f'{date} {description}' - if start_comment.rstrip() != '': - start_line += f' ; {start_comment}' - lines = [start_line] - if 'line_0_add' in postvars.keys(): - add_empty_line = 0 - i = j = 1 - while f'line_{i}_comment' in postvars.keys(): - if f'line_{i}_delete' in postvars.keys(): - i += 1 - continue - elif f'line_{i}_delete_after' in postvars.keys(): - break - elif f'line_{i}_add' in postvars.keys(): - add_empty_line = j - account = postvars[f'line_{i}_account'][0] - amount = postvars[f'line_{i}_amount'][0] - currency = postvars[f'line_{i}_currency'][0] - comment = postvars[f'line_{i}_comment'][0] - i += 1 - new_main = f' {account} {amount}' - if '' == new_main.rstrip() == comment.rstrip(): # don't write empty lines, ignore currency if nothing else set - continue - if len(amount.rstrip()) > 0: - new_main += f' {currency}' - j += 1 - new_line = new_main - if comment.rstrip() != '': - new_line += f' ; {comment}' - lines += [new_line] - if 'add_sink' in postvars.keys(): - temp_lines = lines.copy() + ['_'] - try: - temp_bookings, _ = parse_lines(temp_lines) - for currency in temp_bookings[0].sink: - amount = temp_bookings[0].sink[currency] - lines += [f'Assets {amount:.2f} {currency}'] - except PlomException: - pass - if 'add_taxes' in postvars.keys(): - lines += db.add_taxes(lines, finish=False) - elif 'add_taxes2' in postvars.keys(): - lines += db.add_taxes(lines, finish=True) - return lines, add_empty_line - - def balance_as_html(self, db, until=None): - bookings = db.bookings[:until if until is None else int(until)] - lines = [] - account_tree, account_sums = bookings_to_account_tree(bookings) - def print_subtree(lines, indent, node, subtree, path): - line = f"{indent}{node}" - n_tabs = 5 - (len(line) // 8) - line += n_tabs * "\t" - if "€" in account_sums[path + node].keys(): - amount = account_sums[path + node]["€"] - line += f"{amount:9.2f} €\t" - else: - line += f"\t\t" - for currency, amount in account_sums[path + node].items(): - if currency != '€' and amount > 0: - line += f"{amount:5.2f} {currency}\t" - lines += [line] - indent += " " - for k, v in sorted(subtree.items()): - print_subtree(lines, indent, k, v, path + node + ":") - for k, v in sorted(account_tree.items()): - print_subtree(lines, "", k, v, "") - content = "\n".join(lines) - return f"
{content}
" - - def ledger_as_html(self, db): + def ledger_as_html(self): booking_tmpl = jinja2.Template(booking_html) single_c_tmpl = jinja2.Template('{{c|e}}
') ## elements_to_write = [] last_i = i = 0 ## - for nth, booking in enumerate(db.bookings): - move_up = nth > 0 and db.bookings[nth - 1].date_string == booking.date_string - move_down = nth < len(db.bookings) - 1 and db.bookings[nth + 1].date_string == booking.date_string + for nth, booking in enumerate(self.bookings): + move_up = nth > 0 and self.bookings[nth - 1].date_string == booking.date_string + move_down = nth < len(self.bookings) - 1 and self.bookings[nth + 1].date_string == booking.date_string booking_end = last_i = booking.start_line + len(booking.lines) booking_lines = [] i = booking.start_line ## - elements_to_write += [single_c_tmpl.render(c=c) for c in db.comments[last_i:i] if c != ''] ## + elements_to_write += [single_c_tmpl.render(c=c) for c in self.comments[last_i:i] if c != ''] ## for booking_line in booking.lines[1:]: i += 1 ## - comment = db.comments[i] ## + comment = self.comments[i] ## if booking_line == '': booking_lines += [{'acc': None, 'money': None, 'comment': comment}] ## continue @@ -699,28 +540,54 @@ class LedgerServer(PlomServer): money = f'{booking_line[1]} {booking_line[2]}' booking_lines += [{'acc': booking_line[0], 'money':money, 'comment':comment}] ## elements_to_write += [booking_tmpl.render( + prefix=self.prefix, nth=nth, start=booking.start_line, end=booking_end, date=booking.date_string, desc=booking.description, - head_comment=db.comments[booking.start_line], + head_comment=self.comments[booking.start_line], move_up=move_up, move_down=move_down, booking_lines = booking_lines)] - elements_to_write += [single_c_tmpl.render(c=c) for c in db.comments[last_i:] if c != ''] # + elements_to_write += [single_c_tmpl.render(c=c) for c in self.comments[last_i:] if c != ''] # return '\n'.join(elements_to_write) - def add_free(self, db, start=0, end=0, copy=False): + def balance_as_html(self, until=None): + bookings = self.bookings[:until if until is None else int(until)] + lines = [] + account_tree, account_sums = bookings_to_account_tree(bookings) + def print_subtree(lines, indent, node, subtree, path): + line = f"{indent}{node}" + n_tabs = 5 - (len(line) // 8) + line += n_tabs * "\t" + if "€" in account_sums[path + node].keys(): + amount = account_sums[path + node]["€"] + line += f"{amount:9.2f} €\t" + else: + line += f"\t\t" + for currency, amount in account_sums[path + node].items(): + if currency != '€' and amount > 0: + line += f"{amount:5.2f} {currency}\t" + lines += [line] + indent += " " + for k, v in sorted(subtree.items()): + print_subtree(lines, indent, k, v, path + node + ":") + for k, v in sorted(account_tree.items()): + print_subtree(lines, "", k, v, "") + content = "\n".join(lines) + return f"
{content}
" + + def add_free(self, start=0, end=0, copy=False): tmpl = jinja2.Template(add_form_header + add_free_html + add_form_footer) - lines = db.get_lines(start, end) + lines = self.get_lines(start, end) if copy: start = end = 0 - return tmpl.render(action='add_free', start=start, end=end, lines=lines) + return tmpl.render(action=self.prefix + '/add_free', start=start, end=end, lines=lines) - def add_structured(self, db, start=0, end=0, copy=False, temp_lines=[], add_empty_line=None): + def add_structured(self, start=0, end=0, copy=False, temp_lines=[], add_empty_line=None): tmpl = jinja2.Template(add_form_header + add_structured_html + add_form_footer) - lines = temp_lines if len(''.join(temp_lines)) > 0 else db.get_lines(start, end) + lines = temp_lines if len(''.join(temp_lines)) > 0 else self.get_lines(start, end) bookings, comments = parse_lines(lines, validate_bookings=False) if len(bookings) > 1: raise PlomException('can only structurally edit single Booking') @@ -728,9 +595,9 @@ class LedgerServer(PlomServer): comments = comments[:add_empty_line+1] + [''] + comments[add_empty_line+1:] booking = bookings[0] booking.lines = booking.lines[:add_empty_line+1] + [''] + booking.lines[add_empty_line+1:] - action = 'add_structured' + action = self.prefix + '/add_structured' datalist_sets = {'descriptions': set(), 'accounts': set(), 'currencies': set()} - for b in db.bookings: + for b in self.bookings: datalist_sets['descriptions'].add(b.description) for account, moneys in b.account_changes.items(): datalist_sets['accounts'].add(account) @@ -776,31 +643,183 @@ class LedgerServer(PlomServer): end=end) return content - def move_up(self, db, start, end): + def move_up(self, start, end): prev_booking = None - for redir_nth, b in enumerate(db.bookings): + for redir_nth, b in enumerate(self.bookings): if b.start_line >= start: break prev_booking = b start_at = prev_booking.start_line - self.make_move(db, start, end, start_at) + self.make_move(start, end, start_at) return redir_nth - 1 - def move_down(self, db, start, end): + def move_down(self, start, end): next_booking = None - for redir_nth, b in enumerate(db.bookings): + for redir_nth, b in enumerate(self.bookings): if b.start_line > start: next_booking = b break start_at = next_booking.start_line + len(next_booking.lines) - (end - start) + 1 - self.make_move(db, start, end, start_at) + self.make_move(start, end, start_at) return redir_nth - def make_move(self, db, start, end, start_at): - lines = db.get_lines(start, end) - total_lines = db.real_lines[:start] + db.real_lines[end:] - db.write_lines_in_total_lines_at(total_lines, start_at, lines) + def make_move(self, start, end, start_at): + lines = self.get_lines(start, end) + total_lines = self.real_lines[:start] + self.real_lines[end:] + self.write_lines_in_total_lines_at(total_lines, start_at, lines) + + def booking_lines_from_postvars(self, postvars): + add_empty_line = None + date = postvars['date'][0] + description = postvars['description'][0] + start_comment = postvars['line_0_comment'][0] + start_line = f'{date} {description}' + if start_comment.rstrip() != '': + start_line += f' ; {start_comment}' + lines = [start_line] + if 'line_0_add' in postvars.keys(): + add_empty_line = 0 + i = j = 1 + while f'line_{i}_comment' in postvars.keys(): + if f'line_{i}_delete' in postvars.keys(): + i += 1 + continue + elif f'line_{i}_delete_after' in postvars.keys(): + break + elif f'line_{i}_add' in postvars.keys(): + add_empty_line = j + account = postvars[f'line_{i}_account'][0] + amount = postvars[f'line_{i}_amount'][0] + currency = postvars[f'line_{i}_currency'][0] + comment = postvars[f'line_{i}_comment'][0] + i += 1 + new_main = f' {account} {amount}' + if '' == new_main.rstrip() == comment.rstrip(): # don't write empty lines, ignore currency if nothing else set + continue + if len(amount.rstrip()) > 0: + new_main += f' {currency}' + j += 1 + new_line = new_main + if comment.rstrip() != '': + new_line += f' ; {comment}' + lines += [new_line] + if 'add_sink' in postvars.keys(): + temp_lines = lines.copy() + ['_'] + try: + temp_bookings, _ = parse_lines(temp_lines) + for currency in temp_bookings[0].sink: + amount = temp_bookings[0].sink[currency] + lines += [f'Assets {amount:.2f} {currency}'] + except PlomException: + pass + if 'add_taxes' in postvars.keys(): + lines += self.add_taxes(lines, finish=False) + elif 'add_taxes2' in postvars.keys(): + lines += self.add_taxes(lines, finish=True) + return lines, add_empty_line + + + +class LedgerHandler(PlomHandler): + + def app_init(self, handler): + default_path = '/ledger' + handler.add_route('GET', default_path, self.forward_gets) + handler.add_route('POST', default_path, self.forward_posts) + return 'ledger', default_path + + def do_POST(self): + self.forward_posts() + + def forward_posts(self): + try: + prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' + parsed_url = urlparse(self.path) + length = int(self.headers['content-length']) + postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + start = int(postvars['start'][0]) + end = int(postvars['end'][0]) + db = LedgerDB(prefix) + add_empty_line = None + lines = [] + # get inputs + if prefix + '/add_structured' == parsed_url.path and not 'revert' in postvars.keys(): + lines, add_empty_line = db.booking_lines_from_postvars(postvars) + elif prefix + '/add_free' == parsed_url.path and not 'revert' in postvars.keys(): + lines = postvars['booking'][0].splitlines() + # validate where appropriate + if ('save' in postvars.keys()) or ('check' in postvars.keys()): + _, _ = parse_lines(lines) + # if saving, process where to and where to redirect after + if 'save' in postvars.keys(): + last_date = str(datetime.now())[:10] + if len(db.bookings) > 0: + last_date = db.bookings[-1].date_string + target_date = last_date[:] + first_line_tokens = lines[0].split() if len(lines) > 0 else '' + first_token = first_line_tokens[0] if len(first_line_tokens) > 0 else '' + try: + datetime.strptime(first_token, '%Y-%m-%d') + target_date = first_token + except ValueError: + pass + if start == end == 0: + start = db.insert_at_date(lines, target_date) + nth = db.get_nth_for_booking_of_start_line(start) + else: + new_start = db.update(start, end, lines, target_date) + nth = db.get_nth_for_booking_of_start_line(new_start) + if new_start > start: + nth -= 1 + self.redirect(prefix + f'/#{nth}') + # otherwise just re-build editing form + else: + if prefix + '/add_structured' == parsed_url.path: + edit_content = db.add_structured(db, start, end, temp_lines=lines, add_empty_line=add_empty_line) + else: + edit_content = db.add_free(db, start, end) + self.send_HTML(edit_content) + except PlomException as e: + self.fail_400(e) + + def do_GET(self): + self.forward_gets() + + def forward_gets(self): + try: + prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' + parsed_url = urlparse(self.path) + params = parse_qs(parsed_url.query) + start = int(params.get('start', ['0'])[0]) + end = int(params.get('end', ['0'])[0]) + db = LedgerDB(prefix=prefix) + if parsed_url.path == prefix + '/balance': + stop = params.get('stop', [None])[0] + page = db.balance_as_html(stop) + elif parsed_url.path == prefix + '/add_free': + page = db.add_free(start, end) + elif parsed_url.path == prefix + '/add_structured': + page = db.add_structured(start, end) + elif parsed_url.path == prefix + '/copy_free': + page = db.add_free(start, end, copy=True) + elif parsed_url.path == prefix + '/copy_structured': + page = db.add_structured(start, end, copy=True) + elif parsed_url.path == prefix + '/move_up': + nth = db.move_up(start, end) + self.redirect(prefix + f'/#{nth}') + return + elif parsed_url.path == prefix + '/move_down': + nth = db.move_down(start, end) + self.redirect(prefix + f'/#{nth}') + return + else: + page = db.ledger_as_html() + header = jinja2.Template(html_head).render(prefix=prefix) + self.send_HTML(header+ page) + except PlomException as e: + self.fail_400(e) + if __name__ == "__main__": - run_server(server_port, LedgerServer) + run_server(server_port, LedgerHandler) diff --git a/plomlib.py b/plomlib.py index 92bfb85..6a13f05 100644 --- a/plomlib.py +++ b/plomlib.py @@ -77,25 +77,17 @@ class PlomDB: self.unlock() -class PlomServer(BaseHTTPRequestHandler): +class PlomHandler(BaseHTTPRequestHandler): + homepage = '/' + html_head = '\n\n' + html_foot = '\n' - def __init__(self, *args, **kwargs): - self.html_head = ['\n\n'] - self.html_foot = ['\n'] - self.pre_init() - super().__init__(*args, **kwargs) - - def pre_init(self): - pass - def fail_400(self, e): self.send_HTML(f'ERROR: {e}', 400) def send_HTML(self, html, code=200): self.send_code_and_headers(code, [('Content-type', 'text/html')]) - header = '\n'.join(self.html_head) - footer = '\n'.join(self.html_foot) - self.wfile.write(bytes(f'{header}\n{html}\n{footer}', 'utf-8')) + self.wfile.write(bytes(f'{self.html_head}\n{html}\n{self.html_foot}', 'utf-8')) def send_code_and_headers(self, code, headers=[]): self.send_response(code) @@ -108,9 +100,9 @@ class PlomServer(BaseHTTPRequestHandler): -def run_server(port, server_class): +def run_server(port, handler_class): from http.server import HTTPServer - webServer = HTTPServer(('localhost', port), server_class) + webServer = HTTPServer(('localhost', port), handler_class) print(f"Server started http://localhost:{port}") try: webServer.serve_forever() diff --git a/unite.py b/unite.py new file mode 100644 index 0000000..14a7e3c --- /dev/null +++ b/unite.py @@ -0,0 +1,62 @@ +from plomlib import PlomException, run_server, PlomHandler +from urllib.parse import urlparse +from income_progress_bars import IncomeProgressHandler +from calories import ConsumptionsHandler +from ledger import LedgerHandler +server_port = 8084 + + +class UnitedRequestHandler(PlomHandler): + routes = {'GET':{}, 'POST':{}} + apps = {} + + @classmethod + def register_app(cls, app): + name, data = app.app_init(app, cls) + cls.apps[name] = data + + @classmethod + def add_route(cls, method, path, service): + if not method in cls.routes.keys(): + cls.routes[method] = {} + cls.routes[method][path] = service + + def do_POST(self): + try: + parsed_url = urlparse(self.path) + path_toks = parsed_url.path.split('/') + while len(path_toks) > 0: + target_path = '/'.join(path_toks) + if target_path in self.routes['POST'].keys(): + self.routes['POST'][target_path](self) + return + path_toks.pop() + page = 'nothing to post?' + self.send_HTML(page) + except PlomException as e: + self.fail_400(e) + + def do_GET(self): + try: + parsed_url = urlparse(self.path) + path_toks = parsed_url.path.split('/') + while len(path_toks) > 0: + target_path = '/'.join(path_toks) + print(target_path) + if target_path in self.routes['GET'].keys(): + self.routes['GET'][target_path](self) + return + path_toks.pop() + page = 'hi there!
' + for route in self.routes['GET']: + page += f'{route}
' + self.send_HTML(page) + except PlomException as e: + self.fail_400(e) + + +if __name__ == "__main__": + UnitedRequestHandler.register_app(IncomeProgressHandler) + UnitedRequestHandler.register_app(ConsumptionsHandler) + UnitedRequestHandler.register_app(LedgerHandler) + run_server(server_port, UnitedRequestHandler) -- 2.30.2 From 5a6e56223e59fb628f88070957b39a42d3b7c945 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 27 Nov 2023 18:02:41 +0100 Subject: [PATCH 02/16] Improve /fix accounting scripts. --- calories.py | 3 +-- ledger.py | 12 +++++++----- plomlib.py | 3 +++ unite.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/calories.py b/calories.py index 4352567..28b1a16 100644 --- a/calories.py +++ b/calories.py @@ -251,8 +251,7 @@ class ConsumptionsHandler(PlomHandler): to_delete += [target] i = 0 if 'eatable_uuid' in postvars.keys(): - for uuid_encoded in postvars['eatable_uuid']: - uuid = uuid_encoded + for uuid in postvars['eatable_uuid']: if uuid not in to_delete: e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False)) db.add_eatable(uuid, e) diff --git a/ledger.py b/ledger.py index 87d219e..b6fe064 100755 --- a/ledger.py +++ b/ledger.py @@ -610,7 +610,7 @@ class LedgerDB(PlomDB): start = end = 0 desc = head_comment = '' if len(bookings) == 0: - for i in range(1, 3): + for i in range(1, 8): booking_lines += [{'i': i, 'acc': '', 'amt': '', 'curr': '€', 'comment': ''}] date=today else: @@ -737,6 +737,7 @@ class LedgerHandler(PlomHandler): parsed_url = urlparse(self.path) length = int(self.headers['content-length']) postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + print("DEBUG", postvars['start'], postvars['end']) start = int(postvars['start'][0]) end = int(postvars['end'][0]) db = LedgerDB(prefix) @@ -775,10 +776,11 @@ class LedgerHandler(PlomHandler): # otherwise just re-build editing form else: if prefix + '/add_structured' == parsed_url.path: - edit_content = db.add_structured(db, start, end, temp_lines=lines, add_empty_line=add_empty_line) + edit_content = db.add_structured(start, end, temp_lines=lines, add_empty_line=add_empty_line) else: - edit_content = db.add_free(db, start, end) - self.send_HTML(edit_content) + edit_content = db.add_free(start, end) + header = jinja2.Template(html_head).render(prefix=prefix) + self.send_HTML(header + edit_content) except PlomException as e: self.fail_400(e) @@ -815,7 +817,7 @@ class LedgerHandler(PlomHandler): else: page = db.ledger_as_html() header = jinja2.Template(html_head).render(prefix=prefix) - self.send_HTML(header+ page) + self.send_HTML(header + page) except PlomException as e: self.fail_400(e) diff --git a/plomlib.py b/plomlib.py index 6a13f05..18c2f18 100644 --- a/plomlib.py +++ b/plomlib.py @@ -27,6 +27,9 @@ class PlomDB: def backup(self): import shutil from datetime import datetime, timedelta + if not os.path.exists(self.db_file): + return + # collect modification times of numbered .bak files bak_prefix = f'{self.db_file}.bak.' backup_dates = [] diff --git a/unite.py b/unite.py index 14a7e3c..64a72af 100644 --- a/unite.py +++ b/unite.py @@ -3,7 +3,7 @@ from urllib.parse import urlparse from income_progress_bars import IncomeProgressHandler from calories import ConsumptionsHandler from ledger import LedgerHandler -server_port = 8084 +server_port = 8081 class UnitedRequestHandler(PlomHandler): -- 2.30.2 From 7c77bd5fe82ff15e0e9c6a808ff7eb5a6d859301 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 27 Nov 2023 18:07:03 +0100 Subject: [PATCH 03/16] Fix ledger.py bug. --- ledger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ledger.py b/ledger.py index b6fe064..3071df6 100755 --- a/ledger.py +++ b/ledger.py @@ -354,11 +354,9 @@ class LedgerDB(PlomDB): def insert_at_date(self, lines, date): start_at = len(self.real_lines) - print("DEBUG triggered insert_at_date", date, start_at) for b in self.bookings: - if b.date_string == date: + if b.date_string >= date: start_at = b.start_line - print("DEBUG setting start_at to", start_at) break elif b.date_string > date: break -- 2.30.2 From 71a0c94ce508dcd21165e06a955043d021efb89f Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Nov 2023 07:12:22 +0100 Subject: [PATCH 04/16] Improve accounting scripts. --- calories.py | 13 ++- income_progress_bars.py | 172 +++++++++++++++++++--------------------- ledger.py | 163 ++++++++++++++++++------------------- plomlib.py | 6 ++ unite.py | 60 +++++++------- 5 files changed, 203 insertions(+), 211 deletions(-) diff --git a/calories.py b/calories.py index 28b1a16..9951ce5 100644 --- a/calories.py +++ b/calories.py @@ -233,7 +233,7 @@ class ConsumptionsHandler(PlomHandler): return 'consumptions', default_path def do_POST(self): - self.write_db() + self.try_do(self.write_db) def write_db(self): from uuid import uuid4 @@ -289,15 +289,12 @@ class ConsumptionsHandler(PlomHandler): default_slots -= 1 if (default_slots <= 0): break - try: - db.write() - homepage = self.apps['consumptions'] if hasattr(self, 'apps') else self.homepage - self.redirect(homepage) - except PlomException as e: - self.fail_400(e) + db.write() + homepage = self.apps['consumptions'] if hasattr(self, 'apps') else self.homepage + self.redirect(homepage) def do_GET(self): - self.show_db() + self.try_do(self.show_db) def show_db(self): db = CaloriesDB() diff --git a/income_progress_bars.py b/income_progress_bars.py index 60f7104..5d9a887 100644 --- a/income_progress_bars.py +++ b/income_progress_bars.py @@ -170,104 +170,98 @@ class IncomeProgressHandler(PlomHandler): return 'income_progress', default_path def do_POST(self): - self.post_income_update() + self.try_do(self.post_income_update) def post_income_update(self): from urllib.parse import parse_qs - try: - length = int(self.headers['content-length']) - postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - db = IncomeDB() - db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0]) - db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0]) - db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0]) - db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0]) - db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0]) - db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0]) - db.year_goal = int(postvars['year_goal'][0]) - db.workdays_per_month = int(postvars['workdays_per_month'][0]) - if 'finish' in postvars.keys(): - day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1 - day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2 - day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3 - db.year_income += day_income - db.month_income += day_income - db.week_income += day_income - db.workday_minutes_worked_1 = 0 - db.workday_minutes_worked_2 = 0 - db.workday_minutes_worked_3 = 0 - db.write_db() - homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage - self.redirect(homepage) - except PlomException as e: - self.fail_400(e) + length = int(self.headers['content-length']) + postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + db = IncomeDB() + db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0]) + db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0]) + db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0]) + db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0]) + db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0]) + db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0]) + db.year_goal = int(postvars['year_goal'][0]) + db.workdays_per_month = int(postvars['workdays_per_month'][0]) + if 'finish' in postvars.keys(): + day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1 + day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2 + day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3 + db.year_income += day_income + db.month_income += day_income + db.week_income += day_income + db.workday_minutes_worked_1 = 0 + db.workday_minutes_worked_2 = 0 + db.workday_minutes_worked_3 = 0 + db.write_db() + homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage + self.redirect(homepage) def do_GET(self): - self.display_income_progress() + self.try_do(self.display_income_progress) def display_income_progress(self): import datetime import calendar - try: - db = IncomeDB() - today = datetime.datetime.now() - update_db = False - if today.year != db.timestamp_year: - db.timestamp_year = today.year - db.timestamp_month = today.month - db.year_income = 0 - db.month_income = 0 - update_db = True - if today.month != db.timestamp_month: - db.timestamp_month = today.month - db.month_income = 0 - update_db = True - if today.isocalendar()[1] != db.timestamp_week: - db.timestamp_week = today.isocalendar()[1] - db.week_income = 0 - update_db = True - if update_db: - print("Resetting timestamp") - db.write_db() - day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1 - year_length = 365 + calendar.isleap(today.year) - workday_goal = db.year_goal / 12 / db.workdays_per_month - workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7) - month_goal = db.year_goal / 12 - week_goal = db.year_goal / (year_length / 7) - day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1 - day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2 - day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3 - year_plus = db.year_income + day_income - month_plus = db.month_income + day_income - week_plus = db.week_income + day_income - progress_time_year = day_of_year / year_length - progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1] - progress_time_week = today.weekday() / 7 - progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year), - ProgressBar("month", month_plus, month_goal, progress_time_month), - ProgressBar("week", week_plus, week_goal, progress_time_week), - ProgressBar("workday", day_income, workday_goal)] - homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage - page = jinja2.Template(tmpl).render( - homepage = homepage, - progress_bars = progress_bars, - workday_hourly_rate_1 = db.workday_hourly_rate_1, - workday_minutes_worked_1 = db.workday_minutes_worked_1, - workday_hourly_rate_2 = db.workday_hourly_rate_2, - workday_minutes_worked_2 = db.workday_minutes_worked_2, - workday_hourly_rate_3 = db.workday_hourly_rate_3, - workday_minutes_worked_3 = db.workday_minutes_worked_3, - year_goal = db.year_goal, - month_goal = month_goal, - week_goal = week_goal, - workdays_per_month = db.workdays_per_month, - workday_goal = workday_goal, - workdays_per_week = workdays_per_week, - ) - self.send_HTML(page) - except PlomException as e: - self.fail_400(e) + db = IncomeDB() + today = datetime.datetime.now() + update_db = False + if today.year != db.timestamp_year: + db.timestamp_year = today.year + db.timestamp_month = today.month + db.year_income = 0 + db.month_income = 0 + update_db = True + if today.month != db.timestamp_month: + db.timestamp_month = today.month + db.month_income = 0 + update_db = True + if today.isocalendar()[1] != db.timestamp_week: + db.timestamp_week = today.isocalendar()[1] + db.week_income = 0 + update_db = True + if update_db: + print("Resetting timestamp") + db.write_db() + day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1 + year_length = 365 + calendar.isleap(today.year) + workday_goal = db.year_goal / 12 / db.workdays_per_month + workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7) + month_goal = db.year_goal / 12 + week_goal = db.year_goal / (year_length / 7) + day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1 + day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2 + day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3 + year_plus = db.year_income + day_income + month_plus = db.month_income + day_income + week_plus = db.week_income + day_income + progress_time_year = day_of_year / year_length + progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1] + progress_time_week = today.weekday() / 7 + progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year), + ProgressBar("month", month_plus, month_goal, progress_time_month), + ProgressBar("week", week_plus, week_goal, progress_time_week), + ProgressBar("workday", day_income, workday_goal)] + homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage + page = jinja2.Template(tmpl).render( + homepage = homepage, + progress_bars = progress_bars, + workday_hourly_rate_1 = db.workday_hourly_rate_1, + workday_minutes_worked_1 = db.workday_minutes_worked_1, + workday_hourly_rate_2 = db.workday_hourly_rate_2, + workday_minutes_worked_2 = db.workday_minutes_worked_2, + workday_hourly_rate_3 = db.workday_hourly_rate_3, + workday_minutes_worked_3 = db.workday_minutes_worked_3, + year_goal = db.year_goal, + month_goal = month_goal, + week_goal = week_goal, + workdays_per_month = db.workdays_per_month, + workday_goal = workday_goal, + workdays_per_week = workdays_per_week, + ) + self.send_HTML(page) if __name__ == "__main__": diff --git a/ledger.py b/ledger.py index 3071df6..6653af6 100755 --- a/ledger.py +++ b/ledger.py @@ -365,7 +365,6 @@ class LedgerDB(PlomDB): return self.write_lines_in_total_lines_at(self.real_lines, start_at, lines) def update(self, start, end, lines, date): - print("DEBUG update", date) total_lines = self.real_lines[:start] + self.real_lines[end:] n_original_lines = end - start start_at = len(total_lines) @@ -727,97 +726,91 @@ class LedgerHandler(PlomHandler): return 'ledger', default_path def do_POST(self): - self.forward_posts() + self.try_do(self.forward_posts) def forward_posts(self): - try: - prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' - parsed_url = urlparse(self.path) - length = int(self.headers['content-length']) - postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - print("DEBUG", postvars['start'], postvars['end']) - start = int(postvars['start'][0]) - end = int(postvars['end'][0]) - db = LedgerDB(prefix) - add_empty_line = None - lines = [] - # get inputs - if prefix + '/add_structured' == parsed_url.path and not 'revert' in postvars.keys(): - lines, add_empty_line = db.booking_lines_from_postvars(postvars) - elif prefix + '/add_free' == parsed_url.path and not 'revert' in postvars.keys(): - lines = postvars['booking'][0].splitlines() - # validate where appropriate - if ('save' in postvars.keys()) or ('check' in postvars.keys()): - _, _ = parse_lines(lines) - # if saving, process where to and where to redirect after - if 'save' in postvars.keys(): - last_date = str(datetime.now())[:10] - if len(db.bookings) > 0: - last_date = db.bookings[-1].date_string - target_date = last_date[:] - first_line_tokens = lines[0].split() if len(lines) > 0 else '' - first_token = first_line_tokens[0] if len(first_line_tokens) > 0 else '' - try: - datetime.strptime(first_token, '%Y-%m-%d') - target_date = first_token - except ValueError: - pass - if start == end == 0: - start = db.insert_at_date(lines, target_date) - nth = db.get_nth_for_booking_of_start_line(start) - else: - new_start = db.update(start, end, lines, target_date) - nth = db.get_nth_for_booking_of_start_line(new_start) - if new_start > start: - nth -= 1 - self.redirect(prefix + f'/#{nth}') - # otherwise just re-build editing form + prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' + parsed_url = urlparse(self.path) + length = int(self.headers['content-length']) + postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) + print("DEBUG", postvars['start'], postvars['end']) + start = int(postvars['start'][0]) + end = int(postvars['end'][0]) + db = LedgerDB(prefix) + add_empty_line = None + lines = [] + # get inputs + if prefix + '/add_structured' == parsed_url.path and not 'revert' in postvars.keys(): + lines, add_empty_line = db.booking_lines_from_postvars(postvars) + elif prefix + '/add_free' == parsed_url.path and not 'revert' in postvars.keys(): + lines = postvars['booking'][0].splitlines() + # validate where appropriate + if ('save' in postvars.keys()) or ('check' in postvars.keys()): + _, _ = parse_lines(lines) + # if saving, process where to and where to redirect after + if 'save' in postvars.keys(): + last_date = str(datetime.now())[:10] + if len(db.bookings) > 0: + last_date = db.bookings[-1].date_string + target_date = last_date[:] + first_line_tokens = lines[0].split() if len(lines) > 0 else '' + first_token = first_line_tokens[0] if len(first_line_tokens) > 0 else '' + try: + datetime.strptime(first_token, '%Y-%m-%d') + target_date = first_token + except ValueError: + pass + if start == end == 0: + start = db.insert_at_date(lines, target_date) + nth = db.get_nth_for_booking_of_start_line(start) else: - if prefix + '/add_structured' == parsed_url.path: - edit_content = db.add_structured(start, end, temp_lines=lines, add_empty_line=add_empty_line) - else: - edit_content = db.add_free(start, end) - header = jinja2.Template(html_head).render(prefix=prefix) - self.send_HTML(header + edit_content) - except PlomException as e: - self.fail_400(e) + new_start = db.update(start, end, lines, target_date) + nth = db.get_nth_for_booking_of_start_line(new_start) + if new_start > start: + nth -= 1 + self.redirect(prefix + f'/#{nth}') + # otherwise just re-build editing form + else: + if prefix + '/add_structured' == parsed_url.path: + edit_content = db.add_structured(start, end, temp_lines=lines, add_empty_line=add_empty_line) + else: + edit_content = db.add_free(start, end) + header = jinja2.Template(html_head).render(prefix=prefix) + self.send_HTML(header + edit_content) def do_GET(self): - self.forward_gets() + self.try_do(self.forward_gets) def forward_gets(self): - try: - prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' - parsed_url = urlparse(self.path) - params = parse_qs(parsed_url.query) - start = int(params.get('start', ['0'])[0]) - end = int(params.get('end', ['0'])[0]) - db = LedgerDB(prefix=prefix) - if parsed_url.path == prefix + '/balance': - stop = params.get('stop', [None])[0] - page = db.balance_as_html(stop) - elif parsed_url.path == prefix + '/add_free': - page = db.add_free(start, end) - elif parsed_url.path == prefix + '/add_structured': - page = db.add_structured(start, end) - elif parsed_url.path == prefix + '/copy_free': - page = db.add_free(start, end, copy=True) - elif parsed_url.path == prefix + '/copy_structured': - page = db.add_structured(start, end, copy=True) - elif parsed_url.path == prefix + '/move_up': - nth = db.move_up(start, end) - self.redirect(prefix + f'/#{nth}') - return - elif parsed_url.path == prefix + '/move_down': - nth = db.move_down(start, end) - self.redirect(prefix + f'/#{nth}') - return - else: - page = db.ledger_as_html() - header = jinja2.Template(html_head).render(prefix=prefix) - self.send_HTML(header + page) - except PlomException as e: - self.fail_400(e) + prefix = self.apps['ledger'] if hasattr(self, 'apps') else '' + parsed_url = urlparse(self.path) + params = parse_qs(parsed_url.query) + start = int(params.get('start', ['0'])[0]) + end = int(params.get('end', ['0'])[0]) + db = LedgerDB(prefix=prefix) + if parsed_url.path == prefix + '/balance': + stop = params.get('stop', [None])[0] + page = db.balance_as_html(stop) + elif parsed_url.path == prefix + '/add_free': + page = db.add_free(start, end) + elif parsed_url.path == prefix + '/add_structured': + page = db.add_structured(start, end) + elif parsed_url.path == prefix + '/copy_free': + page = db.add_free(start, end, copy=True) + elif parsed_url.path == prefix + '/copy_structured': + page = db.add_structured(start, end, copy=True) + elif parsed_url.path == prefix + '/move_up': + nth = db.move_up(start, end) + self.redirect(prefix + f'/#{nth}') + return + elif parsed_url.path == prefix + '/move_down': + nth = db.move_down(start, end) + self.redirect(prefix + f'/#{nth}') + return + else: + page = db.ledger_as_html() + header = jinja2.Template(html_head).render(prefix=prefix) + self.send_HTML(header + page) diff --git a/plomlib.py b/plomlib.py index 18c2f18..739b00f 100644 --- a/plomlib.py +++ b/plomlib.py @@ -101,6 +101,12 @@ class PlomHandler(BaseHTTPRequestHandler): def redirect(self, url='/'): self.send_code_and_headers(302, [('Location', url)]) + def try_do(self, do_method): + try: + do_method() + except PlomException as e: + self.fail_400(e) + def run_server(port, handler_class): diff --git a/unite.py b/unite.py index 64a72af..de8be07 100644 --- a/unite.py +++ b/unite.py @@ -3,6 +3,7 @@ from urllib.parse import urlparse from income_progress_bars import IncomeProgressHandler from calories import ConsumptionsHandler from ledger import LedgerHandler +from todo import TodoHandler server_port = 8081 @@ -22,41 +23,42 @@ class UnitedRequestHandler(PlomHandler): cls.routes[method][path] = service def do_POST(self): - try: - parsed_url = urlparse(self.path) - path_toks = parsed_url.path.split('/') - while len(path_toks) > 0: - target_path = '/'.join(path_toks) - if target_path in self.routes['POST'].keys(): - self.routes['POST'][target_path](self) - return - path_toks.pop() - page = 'nothing to post?' - self.send_HTML(page) - except PlomException as e: - self.fail_400(e) + self.try_do(self._do_posts) + + def _do_posts(self): + parsed_url = urlparse(self.path) + path_toks = parsed_url.path.split('/') + while len(path_toks) > 0: + target_path = '/'.join(path_toks) + if target_path in self.routes['POST'].keys(): + self.routes['POST'][target_path](self) + return + path_toks.pop() + page = 'nothing to post?' + self.send_HTML(page) def do_GET(self): - try: - parsed_url = urlparse(self.path) - path_toks = parsed_url.path.split('/') - while len(path_toks) > 0: - target_path = '/'.join(path_toks) - print(target_path) - if target_path in self.routes['GET'].keys(): - self.routes['GET'][target_path](self) - return - path_toks.pop() - page = 'hi there!
' - for route in self.routes['GET']: - page += f'{route}
' - self.send_HTML(page) - except PlomException as e: - self.fail_400(e) + self.try_do(self._do_gets) + + def _do_gets(self): + parsed_url = urlparse(self.path) + path_toks = parsed_url.path.split('/') + while len(path_toks) > 0: + target_path = '/'.join(path_toks) + print(target_path) + if target_path in self.routes['GET'].keys(): + self.routes['GET'][target_path](self) + return + path_toks.pop() + page = 'hi there!
' + for route in self.routes['GET']: + page += f'{route}
' + self.send_HTML(page) if __name__ == "__main__": UnitedRequestHandler.register_app(IncomeProgressHandler) UnitedRequestHandler.register_app(ConsumptionsHandler) UnitedRequestHandler.register_app(LedgerHandler) + UnitedRequestHandler.register_app(TodoHandler) run_server(server_port, UnitedRequestHandler) -- 2.30.2 From 4cfc09fc8f1a60aa266c16eb7d67b13c5e576042 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 28 Nov 2023 07:12:38 +0100 Subject: [PATCH 05/16] Add todo tracker. --- todo.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 todo.json diff --git a/todo.json b/todo.json new file mode 100644 index 0000000..e2a3342 --- /dev/null +++ b/todo.json @@ -0,0 +1 @@ +{"today": {"date": "2023-11-27a", "tasks": {"f9197573-a001-4fbe-adb0-14391cc68101": {"weight": "1", "done": true, "template": "deb8d796-404a-4894-8f0d-ab8e5c4b71e1"}, "d49c78c9-cdf6-4d35-b306-a69a9b03a911": {"weight": "3.5", "done": true, "template": "038c7597-03c2-4008-969a-8d4df3c17e04"}, "e881720e-a629-42a4-98d5-86ac61651f4b": {"weight": "0.2", "done": true, "template": "d36ccc8a-92c6-438d-b680-ac3e077959e1"}, "e3dda9ae-8a56-4170-8a58-4d8ad6518dbd": {"weight": "0.3", "done": true, "template": "25c42212-bced-4745-b268-638e8e373319"}, "95eda507-6dc6-45c1-bec3-40d2e46b7066": {"weight": "0.3", "done": true, "template": "2cb16551-6a93-41fa-8348-11c1d426fffe"}, "0415be6c-4c13-4694-8b12-72efc3d5fc62": {"weight": "1", "done": true, "template": "453c1251-b44b-4845-bac7-e35fb2d586b0"}, "b0bbaa82-4f39-47b7-93ed-25a258895217": {"weight": "0.5", "done": true, "template": "f63e1ca8-00a2-4ebc-a6b2-edae761f969a"}, "34d0be67-aaf0-45f9-9244-89bf10e754ce": {"weight": "0.2", "done": true, "template": "4bea5933-2bdd-49e0-8692-78588e3b7753"}, "725ee070-ceb0-4e7c-b9ca-e71a16ddf5c4": {"weight": "0.2", "done": true, "template": "3e9c535c-2539-423d-9666-4e434b895a46"}}}, "tt_filter": [], "task_templates": [{"uuid": "038c7597-03c2-4008-969a-8d4df3c17e04", "title": "duschen", "weight": "3", "tags": "duschen;bodily", "weight_history": [[1, "2023-11-28"], ["4", "2023-11-28"], ["3", "2023-11-28"]]}, {"uuid": "deb8d796-404a-4894-8f0d-ab8e5c4b71e1", "title": "erstes z\u00e4hneputzen", "weight": "1", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"]]}, {"uuid": "d36ccc8a-92c6-438d-b680-ac3e077959e1", "title": "haare k\u00e4mmen", "weight": "0.2", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "4bea5933-2bdd-49e0-8692-78588e3b7753", "title": "h\u00e4nde waschen", "weight": "0.2", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "3e9c535c-2539-423d-9666-4e434b895a46", "title": "kalorientabelle nachf\u00fcllen", "weight": "0.2", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "25c42212-bced-4745-b268-638e8e373319", "title": "l\u00fcften", "weight": "0.4", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"], ["0.4", "2023-11-28"]]}, {"uuid": "f63e1ca8-00a2-4ebc-a6b2-edae761f969a", "title": "mail-junk aufr\u00e4umen", "weight": "0.5", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "2cb16551-6a93-41fa-8348-11c1d426fffe", "title": "paroxetin nehmen", "weight": "0.3", "tags": "daily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"]]}, {"uuid": "ecdeaa70-676a-4ef0-9b94-93691dadcbd0", "title": "relevante Mails anschauen", "weight": "0.5", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "db1f747c-35e3-4ff4-a3c4-213d9d132d90", "title": "schn\u00fcrsenkelschuhe anziehen", "weight": "0.5", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "ce84225c-6728-45df-a030-2de490b9790f", "title": "schn\u00fcrsenkelschuhe ausziehen", "weight": "0.3", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"]]}, {"uuid": "453c1251-b44b-4845-bac7-e35fb2d586b0", "title": "zweites z\u00e4hneputzen", "weight": "1", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["2", "2023-11-28"], ["1", "2023-11-28"]]}], "days": []} \ No newline at end of file -- 2.30.2 From e58262d6913522cf57d5317fbf0398e2d56b29a0 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 4 Dec 2023 15:16:31 +0100 Subject: [PATCH 06/16] Improve backup algorithm. --- plomlib.py | 60 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/plomlib.py b/plomlib.py index 739b00f..d1b4c4c 100644 --- a/plomlib.py +++ b/plomlib.py @@ -31,46 +31,52 @@ class PlomDB: return # collect modification times of numbered .bak files + print('DEBUG BACKUP') bak_prefix = f'{self.db_file}.bak.' - backup_dates = [] - i = 0 - bak_as = f'{bak_prefix}{i}' - while os.path.exists(bak_as): - mod_time = os.path.getmtime(bak_as) - backup_dates += [str(datetime.fromtimestamp(mod_time))] - i += 1 - bak_as = f'{bak_prefix}{i}' + # backup_dates = [] + mtimes_to_paths = {} + for path in [path for path in os.listdir(os.path.dirname(bak_prefix)) + if path.startswith(os.path.basename(bak_prefix))]: + mod_time = os.path.getmtime(path) + print(f'DEBUG pre-exists: {path} {mod_time}') + mtimes_to_paths[str(datetime.fromtimestamp(mod_time))] = path + # backup_dates += [str(datetime.fromtimestamp(mod_time))] + + for mtime in sorted(mtimes_to_paths.keys()): + print(f'DEBUG mtimes_to_paths: {mtime}:{mtimes_to_paths[mtime]}') # collect what numbered .bak files to save: the older, the fewer; for each # timedelta, keep the newest file that's older ages_to_keep = [timedelta(minutes=4**i) for i in range(0, 8)] + print(f'DEBUG ages_to_keep: {ages_to_keep}') now = datetime.now() - to_save = [] + to_save = {} for age in ages_to_keep: limit = now - age - for i, date in enumerate(reversed(backup_dates)): - if datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f') < limit: - unreversed_i = len(backup_dates) - i - 1 - if unreversed_i not in to_save: - to_save += [unreversed_i] + for mtime in reversed(sorted(mtimes_to_paths.keys())): + print(f'DEBUG checking if {mtime} < {limit} ({now} - {age})') + if datetime.strptime(mtime, '%Y-%m-%d %H:%M:%S.%f') < limit: + print('DEBUG it is, adding!') + to_save[mtime] = mtimes_to_paths[mtime] break - # remove redundant backup files - j = 0 - for i in to_save: - if i != j: - source = f'{bak_prefix}{i}' - target = f'{bak_prefix}{j}' + for path in [path for path in mtimes_to_paths.values() + if path not in to_save.values()]: + print(f'DEBUG removing {path} cause not in to_save') + os.remove(path) + + i = 0 + for mtime in sorted(to_save.keys()): + source = to_save[mtime] + target = f'{bak_prefix}{i}' + print(f'DEBUG to_save {source} -> {target}') + if source != target: shutil.move(source, target) - j += 1 - for i in range(j, len(backup_dates)): - try: - os.remove(f'{bak_prefix}{i}') - except FileNotFoundError: - pass + i += 1 # put copy of current state at end of bak list - shutil.copy(self.db_file, f'{bak_prefix}{j}') + print(f'DEBUG saving current state to {bak_prefix}{i}') + shutil.copy(self.db_file, f'{bak_prefix}{i}') def write_text_to_db(self, text, mode='w'): self.lock() -- 2.30.2 From 4aa69f1601319ceeef640a4c56f9d2b1eb25cd6b Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 4 Dec 2023 16:47:49 +0100 Subject: [PATCH 07/16] Improve accounting scripts. --- ledger.py | 35 ++++++++++++++++++++--------------- plomlib.py | 1 + todo.json | 1 - 3 files changed, 21 insertions(+), 16 deletions(-) delete mode 100644 todo.json diff --git a/ledger.py b/ledger.py index 6653af6..817def6 100755 --- a/ledger.py +++ b/ledger.py @@ -350,21 +350,28 @@ class LedgerDB(PlomDB): return self.real_lines[start:end] def write_db(self, text, mode='w'): + if text[-1] != '\n': + text += '\n' self.write_text_to_db(text) def insert_at_date(self, lines, date): - start_at = len(self.real_lines) - for b in self.bookings: - if b.date_string >= date: - start_at = b.start_line - break - elif b.date_string > date: - break - if start_at == len(self.real_lines): - lines = [''] + lines + print("DEBUG insert_at_date") + start_at = 0 + if len(self.bookings) > 0: + if date >= self.bookings[-1].date_string: + start_at = len(self.real_lines) + lines = [''] + lines + else: + for b in self.bookings: + if b.date_string == date: + start_at = b.start_line + elif b.date_string > date: + start_at = b.start_line + break return self.write_lines_in_total_lines_at(self.real_lines, start_at, lines) def update(self, start, end, lines, date): + print("DEBUG update") total_lines = self.real_lines[:start] + self.real_lines[end:] n_original_lines = end - start start_at = len(total_lines) @@ -607,15 +614,12 @@ class LedgerDB(PlomDB): start = end = 0 desc = head_comment = '' if len(bookings) == 0: - for i in range(1, 8): - booking_lines += [{'i': i, 'acc': '', 'amt': '', 'curr': '€', 'comment': ''}] date=today else: booking = bookings[0] desc = booking.description date = today if copy else booking.date_string head_comment=comments[0] - last_line = len(comments) for i in range(1, len(comments)): account = amount = currency = '' if i < len(booking.lines) and booking.lines[i] != '': @@ -629,6 +633,8 @@ class LedgerDB(PlomDB): 'curr': currency if currency else '€', 'comment': comments[i], 'comm_cols': len(comments[i])}] + for i in range(len(comments), len(comments) + 8): + booking_lines += [{'i': i, 'acc': '', 'amt': '', 'curr': '€', 'comment': ''}] content += tmpl.render( action=action, date=date, @@ -657,12 +663,12 @@ class LedgerDB(PlomDB): next_booking = b break start_at = next_booking.start_line + len(next_booking.lines) - (end - start) + 1 - self.make_move(start, end, start_at) + self.make_move(start, end, start_at-1) return redir_nth def make_move(self, start, end, start_at): lines = self.get_lines(start, end) - total_lines = self.real_lines[:start] + self.real_lines[end:] + total_lines = self.real_lines[:start-1] + self.real_lines[end:] # +1 because we reduce the original position's two empty border lines to in-between line self.write_lines_in_total_lines_at(total_lines, start_at, lines) def booking_lines_from_postvars(self, postvars): @@ -733,7 +739,6 @@ class LedgerHandler(PlomHandler): parsed_url = urlparse(self.path) length = int(self.headers['content-length']) postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - print("DEBUG", postvars['start'], postvars['end']) start = int(postvars['start'][0]) end = int(postvars['end'][0]) db = LedgerDB(prefix) diff --git a/plomlib.py b/plomlib.py index d1b4c4c..d95a35c 100644 --- a/plomlib.py +++ b/plomlib.py @@ -37,6 +37,7 @@ class PlomDB: mtimes_to_paths = {} for path in [path for path in os.listdir(os.path.dirname(bak_prefix)) if path.startswith(os.path.basename(bak_prefix))]: + path = os.path.dirname(bak_prefix) + f'/{path}' mod_time = os.path.getmtime(path) print(f'DEBUG pre-exists: {path} {mod_time}') mtimes_to_paths[str(datetime.fromtimestamp(mod_time))] = path diff --git a/todo.json b/todo.json deleted file mode 100644 index e2a3342..0000000 --- a/todo.json +++ /dev/null @@ -1 +0,0 @@ -{"today": {"date": "2023-11-27a", "tasks": {"f9197573-a001-4fbe-adb0-14391cc68101": {"weight": "1", "done": true, "template": "deb8d796-404a-4894-8f0d-ab8e5c4b71e1"}, "d49c78c9-cdf6-4d35-b306-a69a9b03a911": {"weight": "3.5", "done": true, "template": "038c7597-03c2-4008-969a-8d4df3c17e04"}, "e881720e-a629-42a4-98d5-86ac61651f4b": {"weight": "0.2", "done": true, "template": "d36ccc8a-92c6-438d-b680-ac3e077959e1"}, "e3dda9ae-8a56-4170-8a58-4d8ad6518dbd": {"weight": "0.3", "done": true, "template": "25c42212-bced-4745-b268-638e8e373319"}, "95eda507-6dc6-45c1-bec3-40d2e46b7066": {"weight": "0.3", "done": true, "template": "2cb16551-6a93-41fa-8348-11c1d426fffe"}, "0415be6c-4c13-4694-8b12-72efc3d5fc62": {"weight": "1", "done": true, "template": "453c1251-b44b-4845-bac7-e35fb2d586b0"}, "b0bbaa82-4f39-47b7-93ed-25a258895217": {"weight": "0.5", "done": true, "template": "f63e1ca8-00a2-4ebc-a6b2-edae761f969a"}, "34d0be67-aaf0-45f9-9244-89bf10e754ce": {"weight": "0.2", "done": true, "template": "4bea5933-2bdd-49e0-8692-78588e3b7753"}, "725ee070-ceb0-4e7c-b9ca-e71a16ddf5c4": {"weight": "0.2", "done": true, "template": "3e9c535c-2539-423d-9666-4e434b895a46"}}}, "tt_filter": [], "task_templates": [{"uuid": "038c7597-03c2-4008-969a-8d4df3c17e04", "title": "duschen", "weight": "3", "tags": "duschen;bodily", "weight_history": [[1, "2023-11-28"], ["4", "2023-11-28"], ["3", "2023-11-28"]]}, {"uuid": "deb8d796-404a-4894-8f0d-ab8e5c4b71e1", "title": "erstes z\u00e4hneputzen", "weight": "1", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"]]}, {"uuid": "d36ccc8a-92c6-438d-b680-ac3e077959e1", "title": "haare k\u00e4mmen", "weight": "0.2", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "4bea5933-2bdd-49e0-8692-78588e3b7753", "title": "h\u00e4nde waschen", "weight": "0.2", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "3e9c535c-2539-423d-9666-4e434b895a46", "title": "kalorientabelle nachf\u00fcllen", "weight": "0.2", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["0.2", "2023-11-28"]]}, {"uuid": "25c42212-bced-4745-b268-638e8e373319", "title": "l\u00fcften", "weight": "0.4", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"], ["0.4", "2023-11-28"]]}, {"uuid": "f63e1ca8-00a2-4ebc-a6b2-edae761f969a", "title": "mail-junk aufr\u00e4umen", "weight": "0.5", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "2cb16551-6a93-41fa-8348-11c1d426fffe", "title": "paroxetin nehmen", "weight": "0.3", "tags": "daily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"]]}, {"uuid": "ecdeaa70-676a-4ef0-9b94-93691dadcbd0", "title": "relevante Mails anschauen", "weight": "0.5", "tags": "daily;oncomputer", "weight_history": [[1, "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "db1f747c-35e3-4ff4-a3c4-213d9d132d90", "title": "schn\u00fcrsenkelschuhe anziehen", "weight": "0.5", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.5", "2023-11-28"]]}, {"uuid": "ce84225c-6728-45df-a030-2de490b9790f", "title": "schn\u00fcrsenkelschuhe ausziehen", "weight": "0.3", "tags": "daily;bodily", "weight_history": [[1, "2023-11-28"], ["0.3", "2023-11-28"]]}, {"uuid": "453c1251-b44b-4845-bac7-e35fb2d586b0", "title": "zweites z\u00e4hneputzen", "weight": "1", "tags": "daily;duschen;bodily", "weight_history": [[1, "2023-11-28"], ["1", "2023-11-28"], ["2", "2023-11-28"], ["1", "2023-11-28"]]}], "days": []} \ No newline at end of file -- 2.30.2 From 2960beb644986049fd975eac8e348ca61ab122b8 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 4 Dec 2023 21:16:55 +0100 Subject: [PATCH 08/16] Add todo accounting. --- todo.py | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 todo.py 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) -- 2.30.2 From 3faab835bd9d842cc0a529ba33007b3f85839e12 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 5 Dec 2023 03:03:57 +0100 Subject: [PATCH 09/16] Improve accounting scripts. --- ledger.py | 8 +++++++- todo.py | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/ledger.py b/ledger.py index 817def6..cd38b82 100755 --- a/ledger.py +++ b/ledger.py @@ -65,6 +65,9 @@ add_structured_html = """ + + +
@@ -715,10 +718,13 @@ class LedgerDB(PlomDB): lines += [f'Assets {amount:.2f} {currency}'] except PlomException: pass - if 'add_taxes' in postvars.keys(): + elif 'add_taxes' in postvars.keys(): lines += self.add_taxes(lines, finish=False) elif 'add_taxes2' in postvars.keys(): lines += self.add_taxes(lines, finish=True) + elif 'replace' in postvars.keys(): + for i, line in enumerate(lines): + lines[i] = line.replace(postvars['replace_from'][0], postvars['replace_to'][0]) return lines, add_empty_line diff --git a/todo.py b/todo.py index 08fb8b8..8c7029c 100644 --- a/todo.py +++ b/todo.py @@ -23,7 +23,7 @@ forbidden tags: {% for t_tag in db.t_tags | sort %} {% endfor %} - + {% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} {% if t.visible %} @@ -33,7 +33,7 @@ forbidden tags: {% for t_tag in db.t_tags | sort %} - + {% endif %} {% endfor %} @@ -99,6 +99,14 @@ class Task: def title(self): return self._last_of_history(self.title_history, '') + 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 + def set_tags(self, tags): self._set_with_history(self.tags_history, set(tags)) @@ -130,7 +138,7 @@ class Day: @property def todos_sum(self): s = 0 - for todo in [todo for todo in self.todos.values() if (todo.done or todo.day_weight)]: + for todo in [todo for todo in self.todos.values() if todo.done]: s += todo.weight return s @@ -143,16 +151,16 @@ class Day: class Todo: - def __init__(self, done=False, day_weight=None): + def __init__(self, done=False, weight=None): self.done = done - self.day_weight = day_weight + self.weight = weight @classmethod def from_dict(cls, d): - return cls(d['done'], d['day_weight']) + return cls(d['done'], d['weight']) def to_dict(self): - return {'done': self.done, 'day_weight': self.day_weight} + return {'done': self.done, 'weight': self.weight} class TodoDB(PlomDB): @@ -198,6 +206,14 @@ class TodoDB(PlomDB): def write(self): self.write_text_to_db(json.dumps(self.to_dict())) + def save_today(self): + if self.today_date in self.old_days.keys(): + raise PlomException('cannot use same date twice') + for task_uuid, todo in [(task_uuid, todo) for task_uuid, todo in self.today.todos.items() + if not todo.weight]: + todo.weight = self.tasks[task_uuid].default_weight + self.old_days[self.today_date] = self.today + def reset_today(self, date=None): if date: self.today_date = date @@ -263,8 +279,8 @@ class TodoHandler(PlomHandler): 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) + weight = float(postvars['weight'][i]) if postvars['weight'][i] else None + db.today.todos[uuid] = Todo(done, weight) db.today_date = postvars['today_date'][0] db.today.comment = postvars['comment'][0] switch_edited_day = None @@ -273,9 +289,7 @@ class TodoHandler(PlomHandler): 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 + db.save_today() if switch_edited_day: db.reset_today(date) else: @@ -300,12 +314,9 @@ class TodoHandler(PlomHandler): 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 + todo.title = db.tasks[task_uuid].title_at(date) page = Template(tmpl).render(db=db) self.send_HTML(page) -- 2.30.2 From 92c86eae1c6c758defda091b628a259d84f39b18 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Thu, 7 Dec 2023 17:14:47 +0100 Subject: [PATCH 10/16] To lewdger.py, add in-transaction line mirroring. --- ledger.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ledger.py b/ledger.py index cd38b82..9ff0d3e 100755 --- a/ledger.py +++ b/ledger.py @@ -68,6 +68,7 @@ add_structured_html = """ +
@@ -523,6 +524,14 @@ class LedgerDB(PlomDB): ret += [f' {acc_buffer} {-final_minus} € ; assume as to earn in year: {acc_buffer} + {12 - months_passed - 1} * this = {year_needed}'] return ret + def add_mirror(self, lines): + ret = [] + bookings, _ = parse_lines(lines) + booking = bookings[0] + for line in booking.lines[1:]: + ret += [f'? {-line[1]} {line[2]}'] + return ret + def ledger_as_html(self): booking_tmpl = jinja2.Template(booking_html) single_c_tmpl = jinja2.Template('{{c|e}}
') ## @@ -725,6 +734,8 @@ class LedgerDB(PlomDB): elif 'replace' in postvars.keys(): for i, line in enumerate(lines): lines[i] = line.replace(postvars['replace_from'][0], postvars['replace_to'][0]) + elif 'add_mirror' in postvars.keys(): + lines += self.add_mirror(lines) return lines, add_empty_line -- 2.30.2 From 93756ded23b6f80333ef26e434f611a94bd04764 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 10 Dec 2023 17:54:01 +0100 Subject: [PATCH 11/16] Improve todo accounting. --- todo.py | 241 ++++++++++++++++++++++++++++++++++--------------------- unite.py | 1 - 2 files changed, 151 insertions(+), 91 deletions(-) diff --git a/todo.py b/todo.py index 8c7029c..4449232 100644 --- a/todo.py +++ b/todo.py @@ -3,17 +3,39 @@ import json from uuid import uuid4 from datetime import datetime from urllib.parse import parse_qs +from jinja2 import Template +from urllib.parse import urlparse db_path = '/home/plom/org/todo_new.json' # db_path = '/home/plom/public_repos/misc/todo_new.json' server_port = 8082 -tmpl = """ +html_head = """ -
+all | edit day: +choose +do +
+""" + +form_footer = '\n' +old_days_tmpl = """ +
datearchive?{{ db.today.todos_sum|round(2) }}comment:
weighttitletagstoday?done?day weight
default
weight
titletagstoday?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 }}
+""" + +selected_day_tmpl = """ +
+hiden unchosen:
mandatory tags: {% for t_tag in db.t_tags | sort %} {{ t_tag }} {% endfor %} @@ -22,8 +44,8 @@ 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 %} @@ -31,29 +53,20 @@ forbidden tags: {% for t_tag in db.t_tags | sort %} - - + + + {% endif %} {% endfor %}
datearchive?{{ db.today.todos_sum|round(2) }}comment:
default
weight
titletagstoday?done?day
weight
datearchive?{{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}})comment:
default
weight
titletagschoose?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={}): + def __init__(self, db, title_history={}, tags_history={}, default_weight_history={}): + self.db = db self.title_history = title_history.copy() self.tags_history = tags_history.copy() self.default_weight_history = default_weight_history.copy() @@ -69,8 +82,9 @@ class Task: return default if 0 == len(history) else history[keys[-1]] @classmethod - def from_dict(cls, d): + def from_dict(cls, db, d): return cls( + db, d['title_history'], {k: set(v) for k, v in d['tags_history'].items()}, d['default_weight_history']) @@ -123,24 +137,19 @@ class Task: class Day: - def __init__(self, todos, comment=''): + def __init__(self, db, todos={}, comment=''): + self.db = db self.todos = todos self.comment = comment @classmethod - def from_dict(cls, d): + def from_dict(cls, db, d): todos = {} comment = d['comment'] if 'comment' in d.keys() else '' + day = cls(db, todos, comment) 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]: - s += todo.weight - return s + day.add_todo(uuid, todo_dict) + return day def to_dict(self): d = {'comment': self.comment, 'todos': {}} @@ -148,50 +157,80 @@ class Day: d['todos'][task_uuid] = todo.to_dict() return d + def add_todo(self, id_, dict_source=None): + self.todos[id_] = Todo.from_dict(self, dict_source) if dict_source else Todo(self) + + def _todos_sum(self, include_undone=False): + s = 0 + for todo in [todo for todo in self.todos.values() if todo.done]: + s += todo.weight + if include_undone: + for todo in [todo for todo in self.todos.values() if not todo.done]: + s += todo.day_weight if todo.day_weight else 0 + return s + + @property + def todos_sum(self): + return self._todos_sum() + + @property + def todos_sum2(self): + return self._todos_sum(True) class Todo: - def __init__(self, done=False, weight=None): + def __init__(self, day, done=False, day_weight=None): + self.day = day self.done = done - self.weight = weight + self.day_weight = day_weight @classmethod - def from_dict(cls, d): - return cls(d['done'], d['weight']) + def from_dict(cls, day, d): + return cls(day, d['done'], d['day_weight']) def to_dict(self): - return {'done': self.done, 'weight': self.weight} + return {'done': self.done, 'day_weight': self.day_weight} + + @property + def weight(self): + if self.day_weight: + return self.day_weight + else: + task_uuid = [k for k,v in self.day.todos.items() if v == self][0] + return self.day_weight if self.day_weight else self.day.db.tasks[task_uuid].default_weight class TodoDB(PlomDB): - def __init__(self, t_filter_and = set(), t_filter_not = set()): + def __init__(self, prefix, t_filter_and = set(), t_filter_not = set(), hide_unchosen=False): + self.prefix = prefix self.t_filter_and = t_filter_and self.t_filter_not = t_filter_not + self.hide_unchosen = hide_unchosen self.old_days = {} self.tasks = {} - self.reset_today() + self.reset_day() 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'] + self.selected_day = self.add_day(d['selected_day']) + self.selected_day_date = d['selected_day_date'] for uuid, t_dict in d['tasks'].items(): - t = Task.from_dict(t_dict) - self.tasks[uuid] = t + t = self.add_task(id_=uuid, dict_source=t_dict) 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 + and len([tag for tag in self.t_filter_not if tag in t.tags]) == 0\ + and ((not self.hide_unchosen) or uuid in self.selected_day.todos) 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) + self.old_days[date] = self.add_day(dict_source=day_dict) # Day.from_dict(self, day_dict) def to_dict(self): d = { - 'today': self.today.to_dict(), - 'today_date': self.today_date, + 'selected_day': self.selected_day.to_dict(), + 'selected_day_date': self.selected_day_date, 't_filter_and': list(self.t_filter_and), 't_filter_not': list(self.t_filter_not), 'tasks': {}, @@ -206,22 +245,42 @@ class TodoDB(PlomDB): def write(self): self.write_text_to_db(json.dumps(self.to_dict())) - def save_today(self): - if self.today_date in self.old_days.keys(): + def save_selected_day(self): + if self.selected_day_date in self.old_days.keys(): raise PlomException('cannot use same date twice') - for task_uuid, todo in [(task_uuid, todo) for task_uuid, todo in self.today.todos.items() - if not todo.weight]: - todo.weight = self.tasks[task_uuid].default_weight - self.old_days[self.today_date] = self.today + self.old_days[self.selected_day_date] = self.selected_day - def reset_today(self, date=None): + def reset_day(self, date=None): if date: - self.today_date = date - self.today = self.old_days[date] + self.selected_day_date = date + self.selected_day = self.old_days[date] del self.old_days[date] else: - self.today_date = str(datetime.now())[:10] - self.today = Day({}) + self.selected_day_date = str(datetime.now())[:10] + self.selected_day = self.add_day() + + def add_task(self, id_=None, dict_source=None, return_id=False): + t = Task.from_dict(self, dict_source) if dict_source else Task(self) + id_ = id_ if id_ else str(uuid4()) + self.tasks[id_] = t + if return_id: + return id_, t + else: + return t + + def add_day(self, dict_source=None): + return Day.from_dict(self, dict_source) if dict_source else Day(self) + + def show_all(self): + for i in range(10): + self.add_task(id_=f'new{i}') + for date, day in self.old_days.items(): + for task_uuid, todo in day.todos.items(): + todo.title = self.tasks[task_uuid].title_at(date) + return Template(selected_day_tmpl + old_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all') + + def show_selected_day(self): + return Template(selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') class TodoHandler(PlomHandler): @@ -237,14 +296,13 @@ class TodoHandler(PlomHandler): def write_db(self): from urllib.parse import urlencode - db = TodoDB() + prefix = self.apps['todo'] if hasattr(self, 'apps') else '' + db = TodoDB(prefix) 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) - + # 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(): @@ -253,13 +311,13 @@ class TodoHandler(PlomHandler): if 't_filter_not' in postvars.keys(): for target in postvars['t_filter_not']: db.t_filter_not.add(target) + if 'hide_unchosen' in postvars.keys(): + db.hide_unchosen = True 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_uuid, t = db.add_task(return_id=True) 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]: @@ -273,52 +331,55 @@ class TodoHandler(PlomHandler): 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(): + if uuid in db.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): + del db.selected_day.todos[uuid] + if 'choose' in postvars.keys(): for i, uuid in enumerate(postvars['t_uuid']): - if uuid in postvars['do_today']: + if uuid in postvars['choose']: done = 'done' in postvars and uuid in postvars['done'] - weight = float(postvars['weight'][i]) if postvars['weight'][i] else None - db.today.todos[uuid] = Todo(done, weight) - db.today_date = postvars['today_date'][0] - db.today.comment = postvars['comment'][0] + day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None + db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) + db.selected_day_date = postvars['selected_day_date'][0] + db.selected_day.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: - db.save_today() + if 'archive_day' in postvars.keys() or switch_edited_day: + db.save_selected_day() if switch_edited_day: - db.reset_today(date) + db.reset_day(date) else: - db.reset_today() + db.reset_day() 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] + data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] encoded_params = urlencode(data) - homepage += '?' + encoded_params + parsed_url = urlparse(self.path) + if prefix + '/day' == parsed_url.path: + homepage = f'{prefix}/day?{encoded_params}' + else: + homepage = f'{prefix}/all?{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 + prefix = self.apps['todo'] if hasattr(self, 'apps') else '' 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 date, day in db.old_days.items(): - for task_uuid, todo in day.todos.items(): - todo.title = db.tasks[task_uuid].title_at(date) - page = Template(tmpl).render(db=db) - self.send_HTML(page) + hide_unchosen_params = params.get('hide_unchosen', []) + hide_unchosen = len(hide_unchosen_params) > 0 and hide_unchosen_params[0] != '0' + db = TodoDB(prefix, t_filter_and, t_filter_not, hide_unchosen) + if parsed_url.path == prefix + '/day': + page = db.show_selected_day() + else: + page = db.show_all() + header = Template(html_head).render(prefix=prefix) + self.send_HTML(header + page) if __name__ == "__main__": diff --git a/unite.py b/unite.py index de8be07..8f90e67 100644 --- a/unite.py +++ b/unite.py @@ -45,7 +45,6 @@ class UnitedRequestHandler(PlomHandler): path_toks = parsed_url.path.split('/') while len(path_toks) > 0: target_path = '/'.join(path_toks) - print(target_path) if target_path in self.routes['GET'].keys(): self.routes['GET'][target_path](self) return -- 2.30.2 From 0f70b22c0f0e2983edc0a60309f49636db75da6b Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 10 Dec 2023 18:43:56 +0100 Subject: [PATCH 12/16] Improve todo accounting. --- todo.py | 66 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/todo.py b/todo.py index 4449232..8da5f9e 100644 --- a/todo.py +++ b/todo.py @@ -22,13 +22,15 @@ td { border: 1px solid black; } form_footer = '\n' -old_days_tmpl = """ +archived_days_tmpl = """ -{% for date, day in db.old_days.items() | sort(reverse=True) %} +{% for date, day in db.days.items() | sort(reverse=True) %} +{% if day.archived %} {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} {% endfor %} +{% endif %} {% endfor %}
{{ date }} ({{ day.todos_sum |round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
""" @@ -141,6 +143,7 @@ class Day: self.db = db self.todos = todos self.comment = comment + self.archived = True @classmethod def from_dict(cls, db, d): @@ -207,16 +210,19 @@ class TodoDB(PlomDB): self.t_filter_and = t_filter_and self.t_filter_not = t_filter_not self.hide_unchosen = hide_unchosen - self.old_days = {} + self.days = {} self.tasks = {} - self.reset_day() self.t_tags = set() super().__init__(db_path) + if not hasattr(self, 'selected_day_date'): + self.switch_to_day() def read_db_file(self, f): d = json.load(f) - self.selected_day = self.add_day(d['selected_day']) self.selected_day_date = d['selected_day_date'] + for date, day_dict in d['days'].items(): + self.days[date] = self.add_day(dict_source=day_dict) + self.selected_day.archived = False for uuid, t_dict in d['tasks'].items(): t = self.add_task(id_=uuid, dict_source=t_dict) t.visible = len([tag for tag in self.t_filter_and if not tag in t.tags]) == 0\ @@ -224,40 +230,34 @@ class TodoDB(PlomDB): and ((not self.hide_unchosen) or uuid in self.selected_day.todos) for tag in t.tags: self.t_tags.add(tag) - for date, day_dict in d['old_days'].items(): - self.old_days[date] = self.add_day(dict_source=day_dict) # Day.from_dict(self, day_dict) def to_dict(self): d = { - 'selected_day': self.selected_day.to_dict(), 'selected_day_date': self.selected_day_date, 't_filter_and': list(self.t_filter_and), 't_filter_not': list(self.t_filter_not), 'tasks': {}, - 'old_days': {} + '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() + for date, day in self.days.items(): + d['days'][date] = day.to_dict() return d def write(self): self.write_text_to_db(json.dumps(self.to_dict())) - def save_selected_day(self): - if self.selected_day_date in self.old_days.keys(): - raise PlomException('cannot use same date twice') - self.old_days[self.selected_day_date] = self.selected_day - - def reset_day(self, date=None): + def switch_to_day(self, date=None): + if self.selected_day_date in self.days.keys(): + self.selected_day.archived = True if date: self.selected_day_date = date - self.selected_day = self.old_days[date] - del self.old_days[date] else: self.selected_day_date = str(datetime.now())[:10] - self.selected_day = self.add_day() + if not self.selected_day_date in self.days.keys(): + self.days[self.selected_day_date] = self.add_day() + self.selected_day.archived = False def add_task(self, id_=None, dict_source=None, return_id=False): t = Task.from_dict(self, dict_source) if dict_source else Task(self) @@ -274,10 +274,14 @@ class TodoDB(PlomDB): def show_all(self): for i in range(10): self.add_task(id_=f'new{i}') - for date, day in self.old_days.items(): + for date, day in self.days.items(): for task_uuid, todo in day.todos.items(): todo.title = self.tasks[task_uuid].title_at(date) - return Template(selected_day_tmpl + old_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all') + return Template(selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all') + + @property + def selected_day(self): + return self.days[self.selected_day_date] def show_selected_day(self): return Template(selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') @@ -339,19 +343,27 @@ class TodoHandler(PlomHandler): done = 'done' in postvars and uuid in postvars['done'] day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) - db.selected_day_date = postvars['selected_day_date'][0] + db.selected_day.comment = postvars['comment'][0] + new_selected_day_date = postvars['selected_day_date'][0] + if new_selected_day_date != db.selected_day_date: + if new_selected_day_date in db.days.keys(): + raise PlomException('cannot use same date twice') + else: + db.days[new_selected_day_date] = db.selected_day + del db.days[db.selected_day_date] + db.selected_day_date = new_selected_day_date + switch_edited_day = None - for date in db.old_days.keys(): + for date in db.days.keys(): if f'edit_{date}' in postvars.keys(): switch_edited_day = date break if 'archive_day' in postvars.keys() or switch_edited_day: - db.save_selected_day() if switch_edited_day: - db.reset_day(date) + db.switch_to_day(date) else: - db.reset_day() + db.switch_to_day() db.write() data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] encoded_params = urlencode(data) -- 2.30.2 From 187d7e878767dcd4072a562e834cb7abd4349a06 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Sun, 10 Dec 2023 21:47:42 +0100 Subject: [PATCH 13/16] Improve todo accounting. --- todo.py | 85 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/todo.py b/todo.py index 8da5f9e..a9e1807 100644 --- a/todo.py +++ b/todo.py @@ -17,16 +17,19 @@ td { border: 1px solid black; } all | edit day: choose do +coming
""" - form_footer = '\n' +form_header_tmpl = """ +
+""" archived_days_tmpl = """ -{% for date, day in db.days.items() | sort(reverse=True) %} +{% for date, day in days.items() | sort(reverse=True) %} {% if day.archived %} - + {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} {% endfor %} @@ -34,9 +37,7 @@ archived_days_tmpl = """ {% endfor %}
{{ date }} ({{ day.todos_sum |round(2) }}) {{ day.comment|e }}
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
""" - selected_day_tmpl = """ - hiden unchosen:
mandatory tags: {% for t_tag in db.t_tags | sort %} {{ t_tag }} @@ -245,6 +246,10 @@ class TodoDB(PlomDB): d['days'][date] = day.to_dict() return d + @property + def selected_day(self): + return self.days[self.selected_day_date] + def write(self): self.write_text_to_db(json.dumps(self.to_dict())) @@ -255,8 +260,8 @@ class TodoDB(PlomDB): self.selected_day_date = date else: self.selected_day_date = str(datetime.now())[:10] - if not self.selected_day_date in self.days.keys(): - self.days[self.selected_day_date] = self.add_day() + if not self.selected_day_date in self.days.keys(): + self.days[self.selected_day_date] = self.add_day() self.selected_day.archived = False def add_task(self, id_=None, dict_source=None, return_id=False): @@ -277,14 +282,30 @@ class TodoDB(PlomDB): for date, day in self.days.items(): for task_uuid, todo in day.todos.items(): todo.title = self.tasks[task_uuid].title_at(date) - return Template(selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all') - - @property - def selected_day(self): - return self.days[self.selected_day_date] + return Template(form_header_tmpl + selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all', days=self.days) def show_selected_day(self): - return Template(selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') + return Template(form_header_tmpl + selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') + + def show_coming(self): + from datetime import timedelta + todays_date = str(datetime.now())[:10] + days_to_show = self.days.copy() + for day in days_to_show.values(): + day.archived = False + last_date = sorted(days_to_show.keys())[-1] + start_date = datetime.strptime(todays_date, '%Y-%m-%d') + end_date = datetime.strptime(last_date, '%Y-%m-%d') + for n in range(int((end_date - start_date).days) + 1): + current_date_obj = start_date + timedelta(n) + current_date = current_date_obj.strftime('%Y-%m-%d') + if current_date not in days_to_show.keys(): + days_to_show[current_date] = self.add_day() + days_to_show[current_date].archived = True + days_to_show[current_date].weekday = datetime.strptime(current_date, '%Y-%m-%d').strftime('%A') + for task_uuid, todo in days_to_show[current_date].todos.items(): + todo.title = self.tasks[task_uuid].title_at(current_date) + return Template(form_header_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/day', days=days_to_show) class TodoHandler(PlomHandler): @@ -344,26 +365,30 @@ class TodoHandler(PlomHandler): day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) - db.selected_day.comment = postvars['comment'][0] - new_selected_day_date = postvars['selected_day_date'][0] - if new_selected_day_date != db.selected_day_date: - if new_selected_day_date in db.days.keys(): - raise PlomException('cannot use same date twice') - else: - db.days[new_selected_day_date] = db.selected_day - del db.days[db.selected_day_date] - db.selected_day_date = new_selected_day_date + if 'comment' in postvars.keys(): + db.selected_day.comment = postvars['comment'][0] + if 'selected_day_date' in postvars.keys(): + new_selected_day_date = postvars['selected_day_date'][0] + try: + datetime.strptime(new_selected_day_date, '%Y-%m-%d') + except ValueError: + raise PlomException(f"{prefix} bad date string: {new_selected_day_date}") + if new_selected_day_date != db.selected_day_date: + if new_selected_day_date in db.days.keys(): + raise PlomException('cannot use same date twice') + else: + db.days[new_selected_day_date] = db.selected_day + del db.days[db.selected_day_date] + db.selected_day_date = new_selected_day_date switch_edited_day = None - for date in db.days.keys(): - if f'edit_{date}' in postvars.keys(): - switch_edited_day = date + day_edit_prefix = 'edit_' + for k in postvars.keys(): + if k.startswith(day_edit_prefix): + switch_edited_day = k[len(day_edit_prefix):] break if 'archive_day' in postvars.keys() or switch_edited_day: - if switch_edited_day: - db.switch_to_day(date) - else: - db.switch_to_day() + db.switch_to_day(switch_edited_day) db.write() data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] encoded_params = urlencode(data) @@ -388,6 +413,8 @@ class TodoHandler(PlomHandler): db = TodoDB(prefix, t_filter_and, t_filter_not, hide_unchosen) if parsed_url.path == prefix + '/day': page = db.show_selected_day() + elif parsed_url.path == prefix + '/coming': + page = db.show_coming() else: page = db.show_all() header = Template(html_head).render(prefix=prefix) -- 2.30.2 From cf116472e137d4fcbcf6ce909de05ef75fb984f8 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 11 Dec 2023 05:09:33 +0100 Subject: [PATCH 14/16] Improve todo accounting. --- todo.py | 578 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 412 insertions(+), 166 deletions(-) diff --git a/todo.py b/todo.py index a9e1807..d4ca1e7 100644 --- a/todo.py +++ b/todo.py @@ -1,7 +1,7 @@ from plomlib import PlomDB, run_server, PlomHandler, PlomException import json from uuid import uuid4 -from datetime import datetime +from datetime import datetime, timedelta from urllib.parse import parse_qs from jinja2 import Template from urllib.parse import urlparse @@ -11,13 +11,24 @@ server_port = 8082 html_head = """ -all | edit day: -choose -do -coming +tasks: list add | day: +choose tasks +do tasks +| calendar
""" form_footer = '\n' @@ -25,20 +36,55 @@ form_footer = '\n' form_header_tmpl = """
""" -archived_days_tmpl = """ +calendar_tmpl = """ {% for date, day in days.items() | sort(reverse=True) %} -{% if day.archived %} - + {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} - -{% endfor %} +{% if todo.visible %} + {% endif %} {% endfor %} +{% endfor %} +
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
{% if todo.done %}✓{% else %}  {% endif %}{{ todo.title }}{{ todo.comment|e }}
+""" +todo_tmpl = """ + + + + + + + + +
task{{ todo.task.title|e }}
default weight{{ todo.default_weight }}
day{{ todo.day.date }}
day weight
comment
done
+ """ -selected_day_tmpl = """ -hiden unchosen:
+task_tmpl = """ + + + + + +
title
history
    {% for k,v in task.title_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
default weight
history
    {% for k,v in task.default_weight_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
tags
history
    {% for k,v in task.tags_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
+ +""" +# archived_days_tmpl = """ +# +# {% for date, day in days.items() | sort(reverse=True) %} +# {% if day.archived %} +# +# {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} +# +# {% endfor %} +# {% endif %} +# {% endfor %} +#
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
+# """ +day_tmpl = """ + +hide unchosen:
mandatory tags: {% for t_tag in db.t_tags | sort %} {{ t_tag }} {% endfor %} @@ -46,33 +92,95 @@ mandatory tags: {% for t_tag in db.t_tags | sort %} forbidden tags: {% for t_tag in db.t_tags | sort %} {{ t_tag }} {% endfor %} +

+ +

+ +prev next date: {{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) comment: +

+ - - + {% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} {% if t.visible %} - - - - - - + + + {% endif %} {% endfor %}
datearchive?{{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}})comment:
default
weight
titletagschoose?done?day
weight
taskchoose?done?weightcomment
+ +
] {{ t.current_title|e }}tags: {% for tag in t.tags | sort %}{{ tag }} {% endfor %}
""" +tasks_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 %} +
default
weight
tasktags
{{ t.default_weight }}{{ t.title|e }}{% for tag in t.tags | sort %}{{ tag }} {% endfor %}
+""" +# selected_day_tmpl = """ +#

+# hide unchosen:
+# 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 %} +#

+# +#

+# +# date: {{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) comment: +#

+# +# +# +# {% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} +# {% if t.visible %} +# +# +# +# +# +# +# +# +# {% endif %} +# {% endfor %} +#
default
weight
titletagschoose?done?day
weight
+#
+# +# """ class Task: - def __init__(self, db, title_history={}, tags_history={}, default_weight_history={}): + def __init__(self, db, title_history=None, tags_history=None, default_weight_history=None): self.db = db - self.title_history = title_history.copy() - self.tags_history = tags_history.copy() - self.default_weight_history = default_weight_history.copy() + self.title_history = title_history if title_history else {} + self.tags_history = tags_history if tags_history else {} + self.default_weight_history = default_weight_history if default_weight_history else {} self.visible = True def _set_with_history(self, history, value): @@ -92,30 +200,34 @@ class Task: {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 default_weight(self): + return self._last_of_history(self.default_weight_history, 1) - def set_default_weight(self, default_weight): + @default_weight.setter + def 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 default_weight_at(self, queried_date): + ret = self.default_weight_history[sorted(self.default_weight_history.keys())[0]] + for date_key, default_weight in self.default_weight_history.items(): + if date_key > f'{queried_date} 23:59:59': + break + ret = default_weight + return ret - def set_title(self, title): - self._set_with_history(self.title_history, title) + @property + def current_default_weight(self): + return self.default_weight_at(self.db.selected_date) @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(): @@ -124,25 +236,47 @@ class Task: ret = title return ret - def set_tags(self, tags): - self._set_with_history(self.tags_history, set(tags)) + @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 tags_joined(self): + return ';'.join(sorted(list(self.tags))) + + @tags_joined.setter + def tags_joined(self, tags_string): + tags = set() + for tag in [tag.strip() for tag in tags_string.split(';') if tag.strip() != '']: + tags.add(tag) + self.tags = tags + 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} + @property + def id_(self): + for k, v in self.db.tasks.items(): + if v == self: + return k + class Day: - def __init__(self, db, todos={}, comment=''): + def __init__(self, db, todos=None, comment=''): self.db = db - self.todos = todos + self.todos = todos if todos else {} self.comment = comment self.archived = True @@ -163,6 +297,7 @@ class Day: def add_todo(self, id_, dict_source=None): self.todos[id_] = Todo.from_dict(self, dict_source) if dict_source else Todo(self) + return self.todos[id_] def _todos_sum(self, include_undone=False): s = 0 @@ -181,49 +316,67 @@ class Day: def todos_sum2(self): return self._todos_sum(True) + @property + def date(self): + for k, v in self.db.days.items(): + if v == self: + return k + class Todo: - def __init__(self, day, done=False, day_weight=None): + def __init__(self, day, done=False, day_weight=None, comment=''): self.day = day self.done = done self.day_weight = day_weight + self.comment = comment @classmethod def from_dict(cls, day, d): - return cls(day, d['done'], d['day_weight']) + return cls(day, d['done'], d['day_weight'], d['comment']) def to_dict(self): - return {'done': self.done, 'day_weight': self.day_weight} + return {'done': self.done, 'day_weight': self.day_weight, 'comment': self.comment} + + @property + def default_weight(self): + return self.task.default_weight_at(self.day.date) @property def weight(self): if self.day_weight: return self.day_weight else: - task_uuid = [k for k,v in self.day.todos.items() if v == self][0] - return self.day_weight if self.day_weight else self.day.db.tasks[task_uuid].default_weight + return self.day_weight if self.day_weight else self.default_weight + + @property + def task(self): + for k, v in self.day.todos.items(): + if v == self: + return self.day.db.tasks[k] + + @property + def title(self): + return self.task.title_at(self.day.date) class TodoDB(PlomDB): - def __init__(self, prefix, t_filter_and = set(), t_filter_not = set(), hide_unchosen=False): + def __init__(self, prefix, selected_date=None, t_filter_and = None, t_filter_not = None, hide_unchosen=False): self.prefix = prefix - self.t_filter_and = t_filter_and - self.t_filter_not = t_filter_not + self.selected_date = selected_date if selected_date else str(datetime.now())[:10] + self.t_filter_and = t_filter_and if t_filter_and else set() + self.t_filter_not = t_filter_not if t_filter_not else set() self.hide_unchosen = hide_unchosen self.days = {} self.tasks = {} self.t_tags = set() super().__init__(db_path) - if not hasattr(self, 'selected_day_date'): - self.switch_to_day() + self.switch_to_day() def read_db_file(self, f): d = json.load(f) - self.selected_day_date = d['selected_day_date'] for date, day_dict in d['days'].items(): self.days[date] = self.add_day(dict_source=day_dict) - self.selected_day.archived = False for uuid, t_dict in d['tasks'].items(): t = self.add_task(id_=uuid, dict_source=t_dict) t.visible = len([tag for tag in self.t_filter_and if not tag in t.tags]) == 0\ @@ -234,7 +387,6 @@ class TodoDB(PlomDB): def to_dict(self): d = { - 'selected_day_date': self.selected_day_date, 't_filter_and': list(self.t_filter_and), 't_filter_not': list(self.t_filter_not), 'tasks': {}, @@ -248,21 +400,19 @@ class TodoDB(PlomDB): @property def selected_day(self): - return self.days[self.selected_day_date] + return self.days[self.selected_date] def write(self): self.write_text_to_db(json.dumps(self.to_dict())) def switch_to_day(self, date=None): - if self.selected_day_date in self.days.keys(): - self.selected_day.archived = True if date: - self.selected_day_date = date - else: - self.selected_day_date = str(datetime.now())[:10] - if not self.selected_day_date in self.days.keys(): - self.days[self.selected_day_date] = self.add_day() - self.selected_day.archived = False + self.selected_date = date + # if self.selected_date in self.days.keys(): + # self.selected_day.archived = True + if not self.selected_date in self.days.keys(): + self.days[self.selected_date] = self.add_day() + # self.selected_day.archived = False def add_task(self, id_=None, dict_source=None, return_id=False): t = Task.from_dict(self, dict_source) if dict_source else Task(self) @@ -276,36 +426,69 @@ class TodoDB(PlomDB): def add_day(self, dict_source=None): return Day.from_dict(self, dict_source) if dict_source else Day(self) - def show_all(self): - for i in range(10): - self.add_task(id_=f'new{i}') - for date, day in self.days.items(): - for task_uuid, todo in day.todos.items(): - todo.title = self.tasks[task_uuid].title_at(date) - return Template(form_header_tmpl + selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all', days=self.days) - - def show_selected_day(self): - return Template(form_header_tmpl + selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') - - def show_coming(self): - from datetime import timedelta + # def show_all(self): + # for i in range(10): + # self.add_task(id_=f'new{i}') + # return Template(form_header_tmpl + selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all', days=self.days) + + #def show_selected_day(self): + # return Template(form_header_tmpl + selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') + + def show_day(self): + current_date = datetime.strptime(self.selected_date, '%Y-%m-%d') + prev_date = current_date - timedelta(days=1) + prev_date_str = prev_date.strftime('%Y-%m-%d') + next_date = current_date + timedelta(days=1) + next_date_str = next_date.strftime('%Y-%m-%d') + return Template(form_header_tmpl + day_tmpl + form_footer).render(db=self, action=self.prefix+'/day', prev_date=prev_date_str, next_date=next_date_str) + + def show_calendar(self, start_date_str, end_date_str): + days_to_show = {} + target_start = start_date_str if start_date_str else sorted(self.days.keys())[0] + target_start = str(datetime.now())[:10] if 'today' == target_start else target_start + target_end = end_date_str if end_date_str else sorted(self.days.keys())[-1] todays_date = str(datetime.now())[:10] - days_to_show = self.days.copy() - for day in days_to_show.values(): - day.archived = False - last_date = sorted(days_to_show.keys())[-1] - start_date = datetime.strptime(todays_date, '%Y-%m-%d') - end_date = datetime.strptime(last_date, '%Y-%m-%d') + start_date = datetime.strptime(target_start, '%Y-%m-%d') + end_date = datetime.strptime(target_end, '%Y-%m-%d') for n in range(int((end_date - start_date).days) + 1): current_date_obj = start_date + timedelta(n) current_date = current_date_obj.strftime('%Y-%m-%d') - if current_date not in days_to_show.keys(): + if current_date not in self.days.keys(): days_to_show[current_date] = self.add_day() - days_to_show[current_date].archived = True + else: + days_to_show[current_date] = self.days[current_date] days_to_show[current_date].weekday = datetime.strptime(current_date, '%Y-%m-%d').strftime('%A') for task_uuid, todo in days_to_show[current_date].todos.items(): - todo.title = self.tasks[task_uuid].title_at(current_date) - return Template(form_header_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/day', days=days_to_show) + todo.visible = self.tasks[task_uuid].visible + return Template(calendar_tmpl).render(db=self, days=days_to_show) + + def show_todo(self, task_uuid, selected_date): + todo = self.days[selected_date].todos[task_uuid] + return Template(form_header_tmpl + todo_tmpl + form_footer).render(db=self, todo=todo, action=self.prefix+'/todo') + + def update_todo(self, task_uuid, date, day_weight, done, comment): + 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 = self.days[date].todos[task_uuid] + todo.day_weight = float(day_weight) if len(day_weight) > 0 else None + todo.done = done + todo.comment = comment + + def show_task(self, id_): + task = self.tasks[id_] if id_ else self.add_task() + return Template(form_header_tmpl + task_tmpl + form_footer).render(db=self, task=task, action=self.prefix+'/task') + + def update_task(self, id_, title, default_weight, tags_joined): + task = self.tasks[id_] if id_ in self.tasks.keys() else self.add_task(id_) + task.title = title + task.default_weight = float(default_weight) if len(default_weight) > 0 else None + task.tags_joined = tags_joined + + def show_tasks(self): + return Template(form_header_tmpl + tasks_tmpl + form_footer).render(db=self, action=self.prefix+'/tasks') + class TodoHandler(PlomHandler): @@ -322,82 +505,134 @@ class TodoHandler(PlomHandler): def write_db(self): from urllib.parse import urlencode prefix = self.apps['todo'] if hasattr(self, 'apps') else '' - db = TodoDB(prefix) 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 'hide_unchosen' in postvars.keys(): - db.hide_unchosen = True - 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: - new_uuid, t = db.add_task(return_id=True) - 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.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): - del db.selected_day.todos[uuid] - if 'choose' in postvars.keys(): + parsed_url = urlparse(self.path) + db = TodoDB(prefix=prefix) + if parsed_url.path == prefix + '/todo': + task_uuid = postvars['task_uuid'][0] + date = postvars['date'][0] + db.update_todo(task_uuid, date, postvars['day_weight'][0], 'done' in postvars.keys(), postvars['comment'][0]) + homepage = f'{prefix}/todo?task={task_uuid}&date={date}' + elif parsed_url.path == prefix + '/task': + id_ = postvars['id'][0] + db.update_task(id_, postvars['title'][0], postvars['default_weight'][0], postvars['tags'][0]) + homepage = f'{prefix}/task?id={id_}' + elif parsed_url.path in {prefix + '/tasks', prefix + '/day'}: + 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 'hide_unchosen' in postvars.keys(): + db.hide_unchosen = True + data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + if parsed_url.path == prefix + '/tasks': + encoded_params = urlencode(data) + homepage = f'{prefix}/tasks?{encoded_params}' + elif parsed_url.path == prefix + '/day': + db.switch_to_day(postvars['original_selected_date'][0]) + new_selected_date = postvars['new_selected_date'][0] + try: + datetime.strptime(new_selected_date, '%Y-%m-%d') + except ValueError: + raise PlomException(f"{prefix} bad date string: {new_selected_date}") + if new_selected_date != db.selected_date: + if new_selected_date in db.days.keys(): + raise PlomException('cannot use same date twice') + else: + db.days[new_selected_date] = db.selected_day + del db.days[db.selected_date] + db.selected_date = new_selected_date for i, uuid in enumerate(postvars['t_uuid']): - if uuid in postvars['choose']: - done = 'done' in postvars and uuid in postvars['done'] - day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None - db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) - - if 'comment' in postvars.keys(): - db.selected_day.comment = postvars['comment'][0] - if 'selected_day_date' in postvars.keys(): - new_selected_day_date = postvars['selected_day_date'][0] - try: - datetime.strptime(new_selected_day_date, '%Y-%m-%d') - except ValueError: - raise PlomException(f"{prefix} bad date string: {new_selected_day_date}") - if new_selected_day_date != db.selected_day_date: - if new_selected_day_date in db.days.keys(): - raise PlomException('cannot use same date twice') - else: - db.days[new_selected_day_date] = db.selected_day - del db.days[db.selected_day_date] - db.selected_day_date = new_selected_day_date - - switch_edited_day = None - day_edit_prefix = 'edit_' - for k in postvars.keys(): - if k.startswith(day_edit_prefix): - switch_edited_day = k[len(day_edit_prefix):] - break - if 'archive_day' in postvars.keys() or switch_edited_day: - db.switch_to_day(switch_edited_day) + t = db.tasks[uuid] + if uuid in db.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): + del db.selected_day.todos[uuid] + if 'choose' in postvars.keys(): + for i, uuid in enumerate(postvars['t_uuid']): + if uuid in postvars['choose']: + done = 'done' in postvars and uuid in postvars['done'] + db.update_todo(uuid, db.selected_date, postvars['day_weight'][i], done, postvars['todo_comment'][i]) + if 'comment' in postvars.keys(): + db.selected_day.comment = postvars['comment'][0] + data += [('date', db.selected_date)] + encoded_params = urlencode(data) + homepage = f'{prefix}/day?{encoded_params}' db.write() - data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] - encoded_params = urlencode(data) - parsed_url = urlparse(self.path) - if prefix + '/day' == parsed_url.path: - homepage = f'{prefix}/day?{encoded_params}' - else: - homepage = f'{prefix}/all?{encoded_params}' self.redirect(homepage) + # db = TodoDB(prefix=prefix, selected_date=postvars['original_selected_date'][0]) + # # 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 'hide_unchosen' in postvars.keys(): + # db.hide_unchosen = True + # 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: + # new_uuid, t = db.add_task(return_id=True) + # 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.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): + # del db.selected_day.todos[uuid] + # if 'choose' in postvars.keys(): + # for i, uuid in enumerate(postvars['t_uuid']): + # if uuid in postvars['choose']: + # done = 'done' in postvars and uuid in postvars['done'] + # day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None + # db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) + + # if 'comment' in postvars.keys(): + # db.selected_day.comment = postvars['comment'][0] + # if 'new_selected_date' in postvars.keys(): + # new_selected_date = postvars['new_selected_date'][0] + # try: + # datetime.strptime(new_selected_date, '%Y-%m-%d') + # except ValueError: + # raise PlomException(f"{prefix} bad date string: {new_selected_date}") + # if new_selected_date != db.selected_date: + # if new_selected_date in db.days.keys(): + # raise PlomException('cannot use same date twice') + # else: + # db.days[new_selected_date] = db.selected_day + # del db.days[db.selected_date] + # db.selected_date = new_selected_date + + # switch_edited_day = None + # day_edit_prefix = 'edit_' + # for k in postvars.keys(): + # if k.startswith(day_edit_prefix): + # switch_edited_day = k[len(day_edit_prefix):] + # db.switch_to_day(switch_edited_day) + # break + # db.write() + # data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + [('date', db.selected_date)] + # encoded_params = urlencode(data) + # if prefix + '/day' == parsed_url.path: + # homepage = f'{prefix}/day?{encoded_params}' + # else: + # homepage = f'{prefix}/all?{encoded_params}' + # self.redirect(homepage) def do_GET(self): self.try_do(self.show_db) @@ -406,18 +641,29 @@ class TodoHandler(PlomHandler): prefix = self.apps['todo'] if hasattr(self, 'apps') else '' parsed_url = urlparse(self.path) params = parse_qs(parsed_url.query) + selected_date = params.get('date', [None])[0] t_filter_and = set(params.get('t_and', [])) t_filter_not = set(params.get('t_not', ['deleted'])) hide_unchosen_params = params.get('hide_unchosen', []) hide_unchosen = len(hide_unchosen_params) > 0 and hide_unchosen_params[0] != '0' - db = TodoDB(prefix, t_filter_and, t_filter_not, hide_unchosen) + db = TodoDB(prefix, selected_date, t_filter_and, t_filter_not, hide_unchosen) if parsed_url.path == prefix + '/day': - page = db.show_selected_day() - elif parsed_url.path == prefix + '/coming': - page = db.show_coming() - else: - page = db.show_all() - header = Template(html_head).render(prefix=prefix) + page = db.show_day() + elif parsed_url.path == prefix + '/todo': + task_uuid = params.get('task', [None])[0] + page = db.show_todo(task_uuid, selected_date) + elif parsed_url.path == prefix + '/task': + id_ = params.get('id', [None])[0] + page = db.show_task(id_) + elif parsed_url.path == prefix + '/tasks': + page = db.show_tasks() + elif parsed_url.path == prefix + '/add_task': + page = db.show_task(None) + else: + start_date = params.get('start', [None])[0] + end_date = params.get('end', [None])[0] + page = db.show_calendar(start_date, end_date) + header = Template(html_head).render(db=db, prefix=prefix, date=selected_date) self.send_HTML(header + page) -- 2.30.2 From 5c55641640539a8a2628959d4bbbf3c0a7c8ae19 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 11 Dec 2023 07:14:26 +0100 Subject: [PATCH 15/16] Further improvements to todo accounting. --- todo.py | 333 +++++++++++++++++++++----------------------------------- 1 file changed, 124 insertions(+), 209 deletions(-) diff --git a/todo.py b/todo.py index d4ca1e7..88684ac 100644 --- a/todo.py +++ b/todo.py @@ -8,20 +8,23 @@ from urllib.parse import urlparse db_path = '/home/plom/org/todo_new.json' # db_path = '/home/plom/public_repos/misc/todo_new.json' server_port = 8082 +DATE_FORMAT = '%Y-%m-%d' html_head = """ @@ -32,14 +35,19 @@ tasks: list a
""" form_footer = '\n' - form_header_tmpl = """
""" calendar_tmpl = """ +

+from: +to: + +

-{% for date, day in days.items() | sort(reverse=True) %} - +{% for date, day in days.items() | sort() %} +{% if day.weekday == "Monday" %}{% endif %} + {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} {% if todo.visible %} @@ -70,38 +78,19 @@ task_tmpl = """
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{% if todo.done %}✓{% else %}  {% endif %}{{ todo.title }}{{ todo.comment|e }}
""" -# archived_days_tmpl = """ -# -# {% for date, day in days.items() | sort(reverse=True) %} -# {% if day.archived %} -# -# {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} -# -# {% endfor %} -# {% endif %} -# {% endfor %} -#
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ todo.title }}{% if todo.done %}✓{% endif %}{{ todo.weight }}
-# """ day_tmpl = """ - -hide unchosen:
-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 %} -

-

+ hide unchosen | +prev next | -prev next date: {{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) comment: +date: | +{{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) | +comment:

- +
-{% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} +{% for uuid, t in db.tasks.items() | sort(attribute='1.title') %} {% if t.visible %} @@ -116,19 +105,48 @@ forbidden tags: {% for t_tag in db.t_tags | sort %}
taskchoose?done?weightcomment
""" -tasks_tmpl = """ +tag_filters_tmpl = """ +

+ +

- mandatory tags: {% for t_tag in db.t_tags | sort %} -{{ t_tag }} +mandatory tags: +{% for and_filter in db.t_filter_and %} + +{% endfor %} +
-forbidden tags: {% for t_tag in db.t_tags | sort %} -{{ t_tag }} +forbidden tags: +{% for not_filter in db.t_filter_not %} + +{% endfor %} +

- +""" +tasks_tmpl = """ +
-{% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} +{% for uuid, t in db.tasks.items() | sort(attribute='1.title') %} {% if t.visible %} @@ -138,41 +156,6 @@ forbidden tags: {% for t_tag in db.t_tags | sort %} {% endfor %}
default
weight
tasktags
{{ t.default_weight }}
""" -# selected_day_tmpl = """ -#

-# hide unchosen:
-# 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 %} -#

-# -#

-# -# date: {{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) comment: -#

-# -# -# -# {% for uuid, t in db.tasks.items() | sort(attribute='1.title', reverse=True) %} -# {% if t.visible %} -# -# -# -# -# -# -# -# -# {% endif %} -# {% endfor %} -#
default
weight
titletagschoose?done?day
weight
-#
-# -# """ class Task: @@ -364,14 +347,13 @@ class TodoDB(PlomDB): def __init__(self, prefix, selected_date=None, t_filter_and = None, t_filter_not = None, hide_unchosen=False): self.prefix = prefix self.selected_date = selected_date if selected_date else str(datetime.now())[:10] - self.t_filter_and = t_filter_and if t_filter_and else set() - self.t_filter_not = t_filter_not if t_filter_not else set() + 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.days = {} self.tasks = {} self.t_tags = set() super().__init__(db_path) - self.switch_to_day() def read_db_file(self, f): d = json.load(f) @@ -387,8 +369,8 @@ class TodoDB(PlomDB): def to_dict(self): d = { - 't_filter_and': list(self.t_filter_and), - 't_filter_not': list(self.t_filter_not), + 't_filter_and': self.t_filter_and, + 't_filter_not': self.t_filter_not, 'tasks': {}, 'days': {} } @@ -400,20 +382,27 @@ class TodoDB(PlomDB): @property def selected_day(self): + if not self.selected_date in self.days.keys(): + self.days[self.selected_date] = self.add_day() return self.days[self.selected_date] + def change_selected_days_date(self, new_date): + if new_date in self.days.keys(): + raise PlomException('cannot use same date twice') + else: + self.days[new_date] = self.selected_day + del self.days[self.selected_date] + self.selected_date = new_date + def write(self): + dates_to_purge = [] + for date, day in self.days.items(): + if len(day.todos) == 0 and len(day.comment) == 0: + dates_to_purge += [date] + for date in dates_to_purge: + del self.days[date] self.write_text_to_db(json.dumps(self.to_dict())) - def switch_to_day(self, date=None): - if date: - self.selected_date = date - # if self.selected_date in self.days.keys(): - # self.selected_day.archived = True - if not self.selected_date in self.days.keys(): - self.days[self.selected_date] = self.add_day() - # self.selected_day.archived = False - def add_task(self, id_=None, dict_source=None, return_id=False): t = Task.from_dict(self, dict_source) if dict_source else Task(self) id_ = id_ if id_ else str(uuid4()) @@ -426,41 +415,33 @@ class TodoDB(PlomDB): def add_day(self, dict_source=None): return Day.from_dict(self, dict_source) if dict_source else Day(self) - # def show_all(self): - # for i in range(10): - # self.add_task(id_=f'new{i}') - # return Template(form_header_tmpl + selected_day_tmpl + archived_days_tmpl + form_footer).render(db=self, action=self.prefix+'/all', days=self.days) - - #def show_selected_day(self): - # return Template(form_header_tmpl + selected_day_tmpl + form_footer).render(db=self, action=self.prefix+'/day') - def show_day(self): - current_date = datetime.strptime(self.selected_date, '%Y-%m-%d') + current_date = datetime.strptime(self.selected_date, DATE_FORMAT) prev_date = current_date - timedelta(days=1) - prev_date_str = prev_date.strftime('%Y-%m-%d') + prev_date_str = prev_date.strftime(DATE_FORMAT) next_date = current_date + timedelta(days=1) - next_date_str = next_date.strftime('%Y-%m-%d') - return Template(form_header_tmpl + day_tmpl + form_footer).render(db=self, action=self.prefix+'/day', prev_date=prev_date_str, next_date=next_date_str) + next_date_str = next_date.strftime(DATE_FORMAT) + return Template(form_header_tmpl + tag_filters_tmpl + day_tmpl + form_footer).render(db=self, action=self.prefix+'/day', prev_date=prev_date_str, next_date=next_date_str) def show_calendar(self, start_date_str, end_date_str): days_to_show = {} target_start = start_date_str if start_date_str else sorted(self.days.keys())[0] - target_start = str(datetime.now())[:10] if 'today' == target_start else target_start + # target_start = str(datetime.now())[:10] if 'today' == target_start else target_start target_end = end_date_str if end_date_str else sorted(self.days.keys())[-1] - todays_date = str(datetime.now())[:10] - start_date = datetime.strptime(target_start, '%Y-%m-%d') - end_date = datetime.strptime(target_end, '%Y-%m-%d') + # todays_date = str(datetime.now())[:10] + start_date = datetime.strptime(target_start, DATE_FORMAT) + end_date = datetime.strptime(target_end, DATE_FORMAT) for n in range(int((end_date - start_date).days) + 1): current_date_obj = start_date + timedelta(n) - current_date = current_date_obj.strftime('%Y-%m-%d') + current_date = current_date_obj.strftime(DATE_FORMAT) if current_date not in self.days.keys(): days_to_show[current_date] = self.add_day() else: days_to_show[current_date] = self.days[current_date] - days_to_show[current_date].weekday = datetime.strptime(current_date, '%Y-%m-%d').strftime('%A') + days_to_show[current_date].weekday = datetime.strptime(current_date, DATE_FORMAT).strftime('%A') for task_uuid, todo in days_to_show[current_date].todos.items(): todo.visible = self.tasks[task_uuid].visible - return Template(calendar_tmpl).render(db=self, days=days_to_show) + return Template(form_header_tmpl + calendar_tmpl + form_footer).render(db=self, days=days_to_show, action=self.prefix+'/calendar', today=str(datetime.now())[:10], start_date=start_date_str, end_date=end_date_str) def show_todo(self, task_uuid, selected_date): todo = self.days[selected_date].todos[task_uuid] @@ -471,7 +452,6 @@ class TodoDB(PlomDB): todo = self.days[date].todos[task_uuid] else: todo = self.days[date].add_todo(task_uuid) - # todo = self.days[date].todos[task_uuid] todo.day_weight = float(day_weight) if len(day_weight) > 0 else None todo.done = done todo.comment = comment @@ -487,7 +467,7 @@ class TodoDB(PlomDB): task.tags_joined = tags_joined def show_tasks(self): - return Template(form_header_tmpl + tasks_tmpl + form_footer).render(db=self, action=self.prefix+'/tasks') + return Template(form_header_tmpl + tag_filters_tmpl + tasks_tmpl + form_footer).render(db=self, action=self.prefix+'/tasks') @@ -509,130 +489,65 @@ class TodoHandler(PlomHandler): postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) parsed_url = urlparse(self.path) db = TodoDB(prefix=prefix) - if parsed_url.path == prefix + '/todo': + + if parsed_url.path == prefix + '/calendar': + start = postvars['start'][0] if len(postvars['start'][0]) > 0 else '' + end = postvars['end'][0] if len(postvars['end'][0]) > 0 else '' + homepage = f'{prefix}/calendar?t_and=calendar&start={start}&end={end}' + + elif parsed_url.path == prefix + '/todo': task_uuid = postvars['task_uuid'][0] date = postvars['date'][0] db.update_todo(task_uuid, date, postvars['day_weight'][0], 'done' in postvars.keys(), postvars['comment'][0]) homepage = f'{prefix}/todo?task={task_uuid}&date={date}' + elif parsed_url.path == prefix + '/task': id_ = postvars['id'][0] db.update_task(id_, postvars['title'][0], postvars['default_weight'][0], postvars['tags'][0]) homepage = f'{prefix}/task?id={id_}' + elif parsed_url.path in {prefix + '/tasks', prefix + '/day'}: - 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) + for target in postvars['t_and']: + if not target in db.t_filter_and: + db.t_filter_and += [target] + for target in postvars['t_not']: + if not target in db.t_filter_not: + db.t_filter_not += [target] if 'hide_unchosen' in postvars.keys(): db.hide_unchosen = True data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + if parsed_url.path == prefix + '/tasks': encoded_params = urlencode(data) homepage = f'{prefix}/tasks?{encoded_params}' + elif parsed_url.path == prefix + '/day': - db.switch_to_day(postvars['original_selected_date'][0]) + db.selected_date = postvars['original_selected_date'][0] new_selected_date = postvars['new_selected_date'][0] try: - datetime.strptime(new_selected_date, '%Y-%m-%d') + datetime.strptime(new_selected_date, DATE_FORMAT) except ValueError: raise PlomException(f"{prefix} bad date string: {new_selected_date}") if new_selected_date != db.selected_date: - if new_selected_date in db.days.keys(): - raise PlomException('cannot use same date twice') - else: - db.days[new_selected_date] = db.selected_day - del db.days[db.selected_date] - db.selected_date = new_selected_date - for i, uuid in enumerate(postvars['t_uuid']): - t = db.tasks[uuid] - if uuid in db.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): - del db.selected_day.todos[uuid] - if 'choose' in postvars.keys(): + db.change_selected_days_date(new_selected_date) + if 't_uuid' in postvars.keys(): for i, uuid in enumerate(postvars['t_uuid']): - if uuid in postvars['choose']: - done = 'done' in postvars and uuid in postvars['done'] - db.update_todo(uuid, db.selected_date, postvars['day_weight'][i], done, postvars['todo_comment'][i]) - if 'comment' in postvars.keys(): - db.selected_day.comment = postvars['comment'][0] + t = db.tasks[uuid] + if uuid in db.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): + del db.selected_day.todos[uuid] + if 'choose' in postvars.keys(): + for i, uuid in enumerate(postvars['t_uuid']): + if uuid in postvars['choose']: + done = 'done' in postvars and uuid in postvars['done'] + db.update_todo(uuid, db.selected_date, postvars['day_weight'][i], done, postvars['todo_comment'][i]) + if 'day_comment' in postvars.keys(): + db.selected_day.comment = postvars['day_comment'][0] data += [('date', db.selected_date)] encoded_params = urlencode(data) homepage = f'{prefix}/day?{encoded_params}' + db.write() self.redirect(homepage) - # db = TodoDB(prefix=prefix, selected_date=postvars['original_selected_date'][0]) - # # 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 'hide_unchosen' in postvars.keys(): - # db.hide_unchosen = True - # 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: - # new_uuid, t = db.add_task(return_id=True) - # 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.selected_day.todos.keys() and ((not 'choose' in postvars) or uuid not in postvars['choose']): - # del db.selected_day.todos[uuid] - # if 'choose' in postvars.keys(): - # for i, uuid in enumerate(postvars['t_uuid']): - # if uuid in postvars['choose']: - # done = 'done' in postvars and uuid in postvars['done'] - # day_weight = float(postvars['day_weight'][i]) if postvars['day_weight'][i] else None - # db.selected_day.add_todo(uuid, {'done': done, 'day_weight': day_weight}) - - # if 'comment' in postvars.keys(): - # db.selected_day.comment = postvars['comment'][0] - # if 'new_selected_date' in postvars.keys(): - # new_selected_date = postvars['new_selected_date'][0] - # try: - # datetime.strptime(new_selected_date, '%Y-%m-%d') - # except ValueError: - # raise PlomException(f"{prefix} bad date string: {new_selected_date}") - # if new_selected_date != db.selected_date: - # if new_selected_date in db.days.keys(): - # raise PlomException('cannot use same date twice') - # else: - # db.days[new_selected_date] = db.selected_day - # del db.days[db.selected_date] - # db.selected_date = new_selected_date - - # switch_edited_day = None - # day_edit_prefix = 'edit_' - # for k in postvars.keys(): - # if k.startswith(day_edit_prefix): - # switch_edited_day = k[len(day_edit_prefix):] - # db.switch_to_day(switch_edited_day) - # break - # db.write() - # data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + [('date', db.selected_date)] - # encoded_params = urlencode(data) - # if prefix + '/day' == parsed_url.path: - # homepage = f'{prefix}/day?{encoded_params}' - # else: - # homepage = f'{prefix}/all?{encoded_params}' - # self.redirect(homepage) def do_GET(self): self.try_do(self.show_db) @@ -642,8 +557,8 @@ class TodoHandler(PlomHandler): parsed_url = urlparse(self.path) params = parse_qs(parsed_url.query) selected_date = params.get('date', [None])[0] - t_filter_and = set(params.get('t_and', [])) - t_filter_not = set(params.get('t_not', ['deleted'])) + t_filter_and = params.get('t_and', []) + t_filter_not = params.get('t_not', ['deleted']) hide_unchosen_params = params.get('hide_unchosen', []) hide_unchosen = len(hide_unchosen_params) > 0 and hide_unchosen_params[0] != '0' db = TodoDB(prefix, selected_date, t_filter_and, t_filter_not, hide_unchosen) -- 2.30.2 From 472874343c229fc77136e1cc3bae5a1d8bc543f9 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Tue, 12 Dec 2023 22:32:03 +0100 Subject: [PATCH 16/16] Improve todo accounting. --- todo.py | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/todo.py b/todo.py index 88684ac..42639a1 100644 --- a/todo.py +++ b/todo.py @@ -12,25 +12,28 @@ DATE_FORMAT = '%Y-%m-%d' html_head = """ tasks: list add | day: choose tasks -do tasks +do tasks | calendar
""" @@ -46,8 +49,8 @@ to: {% for date, day in days.items() | sort() %} -{% if day.weekday == "Monday" %}{% endif %} - +{% if day.weekday == "Mo" %}{% endif %} + {% for task, todo in day.todos.items() | sort(attribute='1.title', reverse=True) %} {% if todo.visible %} @@ -63,7 +66,7 @@ todo_tmpl = """ - +
{{ date }} {{ day.weekday }} ({{ day.todos_sum|round(2) }}) {{ day.comment|e }}
{{ day.weekday }} {{ date }} |{{ '%04.1f' % day.todos_sum|round(2) }}| {{ day.comment|e }}
{% if todo.done %}✓{% else %}  {% endif %}{{ todo.title }}{{ todo.comment|e }}
task{{ todo.task.title|e }}
default weight{{ todo.default_weight }}
day{{ todo.day.date }}
day weight
day weight
comment
done
@@ -73,17 +76,17 @@ task_tmpl = """ - +
title
history
    {% for k,v in task.title_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
default weight
history
    {% for k,v in task.default_weight_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
default weight
history
    {% for k,v in task.default_weight_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
tags
history
    {% for k,v in task.tags_history.items() | sort(attribute='0', reverse=True) %}
  • {{ k }}: {{ v|e }}{% endfor %}
""" day_tmpl = """

- hide unchosen | -prev next | + hide unchosen hide done | +prev next | -date: | +date: | {{ db.selected_day.todos_sum|round(2) }} ({{ db.selected_day.todos_sum2|round(2)}}) | comment:

@@ -95,9 +98,9 @@ comment:
] {{ t.current_title|e }}tags: {% for tag in t.tags | sort %}{{ tag }} {% endfor %}
- + - + {% endif %} @@ -149,7 +152,7 @@ tasks_tmpl = """ {% for uuid, t in db.tasks.items() | sort(attribute='1.title') %} {% if t.visible %} -{{ t.default_weight }} +{{ t.default_weight }} {{ t.title|e }} {% for tag in t.tags | sort %}{{ tag }} {% endfor %} {% endif %} @@ -344,12 +347,13 @@ class Todo: class TodoDB(PlomDB): - def __init__(self, prefix, selected_date=None, t_filter_and = None, t_filter_not = None, hide_unchosen=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.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() @@ -363,7 +367,8 @@ class TodoDB(PlomDB): t = self.add_task(id_=uuid, dict_source=t_dict) 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\ - and ((not self.hide_unchosen) or uuid in self.selected_day.todos) + and ((not self.hide_unchosen) or uuid in self.selected_day.todos)\ + and ((not self.hide_done) or (uuid in self.selected_day.todos and not self.selected_day.todos[uuid].done)) for tag in t.tags: self.t_tags.add(tag) @@ -438,7 +443,7 @@ class TodoDB(PlomDB): days_to_show[current_date] = self.add_day() else: days_to_show[current_date] = self.days[current_date] - days_to_show[current_date].weekday = datetime.strptime(current_date, DATE_FORMAT).strftime('%A') + days_to_show[current_date].weekday = datetime.strptime(current_date, DATE_FORMAT).strftime('%A')[:2] for task_uuid, todo in days_to_show[current_date].todos.items(): todo.visible = self.tasks[task_uuid].visible return Template(form_header_tmpl + calendar_tmpl + form_footer).render(db=self, days=days_to_show, action=self.prefix+'/calendar', today=str(datetime.now())[:10], start_date=start_date_str, end_date=end_date_str) @@ -515,7 +520,9 @@ class TodoHandler(PlomHandler): db.t_filter_not += [target] if 'hide_unchosen' in postvars.keys(): db.hide_unchosen = True - data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + if 'hide_done' in postvars.keys(): + db.hide_done = True + data = [('t_and', f) for f in db.t_filter_and] + [('t_not', f) for f in db.t_filter_not] + [('hide_unchosen', int(db.hide_unchosen))] + [('hide_done', int(db.hide_done))] if parsed_url.path == prefix + '/tasks': encoded_params = urlencode(data) @@ -561,7 +568,9 @@ class TodoHandler(PlomHandler): t_filter_not = params.get('t_not', ['deleted']) hide_unchosen_params = params.get('hide_unchosen', []) hide_unchosen = len(hide_unchosen_params) > 0 and hide_unchosen_params[0] != '0' - db = TodoDB(prefix, selected_date, t_filter_and, t_filter_not, hide_unchosen) + hide_done_params = params.get('hide_done', []) + hide_done = len(hide_done_params) > 0 and hide_done_params[0] != '0' + db = TodoDB(prefix, selected_date, t_filter_and, t_filter_not, hide_unchosen, hide_done) if parsed_url.path == prefix + '/day': page = db.show_day() elif parsed_url.path == prefix + '/todo': -- 2.30.2