X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=ledger.py;h=7771f7f7e458ec4848cf0c19535034c18cda7556;hb=88432b5868d90a30de13a6745f68817d54d40092;hp=c3f0ab9c22d028d92b1c2741e7c411a4d9efa499;hpb=2deeead813fbdf0fec4f94d238a43fd93514f94f;p=misc diff --git a/ledger.py b/ledger.py index c3f0ab9..7771f7f 100755 --- a/ledger.py +++ b/ledger.py @@ -1,6 +1,8 @@ from http.server import BaseHTTPRequestHandler, HTTPServer import sys import os +import html +from urllib.parse import parse_qs, urlparse hostName = "localhost" serverPort = 8082 @@ -32,6 +34,7 @@ def parse_lines(lines): start_line = 0 bookings = [] comments = [] + lines += [''] # to ensure a booking-ending last line for i, line in enumerate(lines): prefix = f"line {i}" # we start with the case of an utterly empty line @@ -73,6 +76,7 @@ def parse_lines(lines): except IndexError: raise HandledException(f"{prefix} bad description: {description}") inside_booking = True + booking_lines += [non_comment] continue # otherwise, read as transfer data toks = non_comment.split() # ignore specification's allowance of single spaces in names @@ -156,7 +160,7 @@ class Booking: prefix = f"booking at line {self.start_line}" sums = {} empty_values = 0 - for line in self.lines: + for line in self.lines[1:]: if line == '': continue _, amount, currency = line @@ -184,7 +188,7 @@ class Booking: account_changes = {} debt = {} sink_account = None - for line in self.lines: + for line in self.lines[1:]: if line == '': continue account, amount, currency = line @@ -205,17 +209,35 @@ class Booking: class Database: - def __init__(self, load_from_file=True): + def __init__(self): db_name = "_ledger" self.db_file = db_name + ".json" self.lock_file = db_name+ ".lock" self.bookings = [] self.comments = [] - if load_from_file and os.path.exists(self.db_file): + self.real_lines = [] + if os.path.exists(self.db_file): with open(self.db_file, "r") as f: - ret = parse_lines(f.readlines()) - self.bookings += ret[0] - self.comments += ret[1] + self.real_lines += f.readlines() + ret = parse_lines(self.real_lines) + self.bookings += ret[0] + self.comments += ret[1] + + def get_lines(self, start, end): + return db.real_lines[start:end] + + def replace(self, start, end, lines): + import shutil + if os.path.exists(self.lock_file): + raise HandledException('Sorry, lock file!') + if os.path.exists(self.db_file): + shutil.copy(self.db_file, self.db_file + ".bak") + f = open(self.lock_file, 'w+') + f.close() + text = ''.join(self.real_lines[:start]) + '\n'.join(lines) + ''.join(self.real_lines[end:]) + with open(self.db_file, 'w') as f: + f.write(text); + os.remove(self.lock_file) def append(self, lines): import shutil @@ -231,25 +253,56 @@ class Database: class MyServer(BaseHTTPRequestHandler): - header = 'ledger balance add
' - footer = '' + header = """ + + +ledger +balance +add free +add structured +
+""" + footer = "\n" def do_POST(self): - from urllib.parse import parse_qs length = int(self.headers['content-length']) postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1) - lines = postvars['booking'][0].splitlines() + parsed_url = urlparse(self.path) + if '/add_structured' == parsed_url.path: + n_lines = int(len(postvars) / 4) + date = postvars['date'][0] + description = postvars['description'][0] + start_comment = postvars['line_0_comment'][0] + lines = [f'{date} {description} ; {start_comment}'] + for i in range(1, n_lines): + 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] + new_main = f'{account} {amount} {currency}' + if '' == new_main.rstrip() == comment.rstrip(): # don't write empty lines + continue + new_line = new_main + if comment.rstrip() != '': + new_line += f' ; {comment}' + lines += [new_line] + elif '/add_free' == parsed_url.path: + lines = postvars['booking'][0].splitlines() + start = int(postvars['start'][0]) + end = int(postvars['end'][0]) try: - bookings, comments = parse_lines(lines) - db.append(lines) - self.send_response(200) + _, _ = parse_lines(lines) + if start == end == 0: + db.append(lines) + else: + db.replace(start, end, lines) + self.send_response(301) + self.send_header('Location', '/') self.end_headers() - page = f"{self.header}Success!{self.footer}" - self.wfile.write(bytes(page, "utf-8")) except HandledException as e: self.send_response(400) self.end_headers() - page = f"{self.header}{e}{self.footer}" + page = f'{self.header}ERROR: {e}{self.footer}' self.wfile.write(bytes(page, "utf-8")) def do_GET(self): @@ -257,11 +310,18 @@ class MyServer(BaseHTTPRequestHandler): self.send_header("Content-type", "text/html") self.end_headers() db = Database() + parsed_url = urlparse(self.path) page = self.header + '' - if self.path == '/balance': + params = parse_qs(parsed_url.query) + start = int(params.get('start', ['0'])[0]) + end = int(params.get('end', ['0'])[0]) + if parsed_url.path == '/balance': page += self.balance_as_html(db) - elif self.path == '/add': - page += self.add() + elif parsed_url.path == '/add_free': + page += self.add_free(db, start, end) + elif parsed_url.path == '/add_structured': + bonus_lines = int(params.get('bonus_lines', ['0'])[0]) + page += self.add_structured(db, start, end) else: page += self.ledger_as_html(db) page += self.footer @@ -332,28 +392,94 @@ class MyServer(BaseHTTPRequestHandler): def ledger_as_html(self, db): lines = [] + line_sep = '
' for comment in db.comments: - lines += [f"; {comment}" if comment != '' else ''] + line = f' ; {comment}' if comment != '' else '' + lines += [line + line_sep] for booking in db.bookings: i = booking.start_line - suffix = f" {lines[i]}" if len(lines[i]) > 0 else "" - lines[i] = f"{booking.date_string} {booking.description}{suffix}" - for booking_line in booking.lines: + suffix = lines[i] + lines[i] = f'

{booking.date_string} {booking.description}{suffix}' + for booking_line in booking.lines[1:]: i += 1 if booking_line == '': continue - suffix = f" {lines[i]}" if len(lines[i]) > 0 else "" - value = f" {booking_line[1]} {booking_line[2]}" if booking_line[1] else "" - lines[i] = f"{booking_line[0]}{value}{suffix}" - content = "\n".join(lines) - return f"

{content}
" + suffix = f' {lines[i]}' if len(lines[i]) > 0 else '' + value = f' {booking_line[1]} {booking_line[2]}' if booking_line[1] else '' + lines[i] = f'{booking_line[0]}{value}{suffix}' + lines[i] = lines[i][:-len(line_sep)] + f"""

+edit: +structured +/ free +
""" + return '\n'.join(lines) + + def header_add_form(self, action): + return f"
\n" + + def footer_add_form(self, start, end): + return f""" + + + +
""" - def add(self): - return '
' + def add_free(self, db, start=0, end=0): + content = html.escape(''.join(db.get_lines(start, end))) + return f'{self.header_add_form("add_free")}{self.footer_add_form(start, end)}' + + def add_structured(self, db, start=0, end=0, bonus_lines=10): + import datetime + lines = db.get_lines(start, end) + bookings, comments = parse_lines(lines) + if len(bookings) > 1: + raise HandledException('can only edit single Booking') + input_lines = '' + last_line = 0 + def inpu(name, val="", datalist=""): + val = val if val is not None else "" + safe_val = html.escape(str(val)) + datalist_string = '' if datalist == '' else f'list="{datalist}"' + return f'' + if len(bookings) == 0: + today = str(datetime.datetime.now())[:10] + input_lines += f'{inpu("date", today)} {inpu("description", "", "descriptions")} ; {inpu("line_0_comment")}
' + last_line = 1 + else: + booking = bookings[0] + last_line = len(comments) + input_lines += f'{inpu("date", booking.date_string)} {inpu("description", booking.description, "descriptions")} ; {inpu("line_0_comment", comments[0])}
' + for i in range(1, len(comments)): + account = amount = currency = '' + if i < len(booking.lines) and booking.lines[i] != '': + account = booking.lines[i][0] + amount = booking.lines[i][1] + currency = booking.lines[i][2] + input_lines += f'{inpu(f"line_{i}_account", account, "accounts")} {inpu(f"line_{i}_amount", amount)} {inpu(f"line_{i}_currency", currency, "currencies")} ; {inpu(f"line_{i}_comment", comments[i])}
' + for j in range(bonus_lines): + i = j + last_line + input_lines += f'{inpu(f"line_{i}_account", "", "accounts")} {inpu(f"line_{i}_amount", "", "amounts")} {inpu(f"line_{i}_currency", "€", "currencies")} ; {inpu(f"line_{i}_comment")}
' + datalist_sets = {'descriptions': set(), 'accounts': set(), 'currencies': set()} + for b in db.bookings: + datalist_sets['descriptions'].add(b.description) + for account, moneys in b.account_changes.items(): + datalist_sets['accounts'].add(account) + for currency in moneys.keys(): + datalist_sets['currencies'].add(currency) + def build_datalist(name): + datalist = f'' + "\n" + for item in datalist_sets[name]: + safe_item = html.escape(item) + datalist += f'' + "\n" + return f"{datalist}\n" + datalists = build_datalist('descriptions') + datalists += build_datalist('accounts') + datalists += build_datalist('currencies') + return f'{self.header_add_form("add_structured")}{input_lines}{datalists}{self.footer_add_form(start, end)}' db = Database() -if __name__ == "__main__": +if __name__ == "__main__": webServer = HTTPServer((hostName, serverPort), MyServer) print(f"Server started http://{hostName}:{serverPort}") try: