home · contact · privacy
Add per-line structured editing of Bookings.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 26 Jan 2025 11:20:45 +0000 (12:20 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 26 Jan 2025 11:20:45 +0000 (12:20 +0100)
ledger.py
templates/_base.tmpl
templates/_macros.tmpl
templates/booking.tmpl [deleted file]
templates/edit_raw.tmpl [new file with mode: 0644]
templates/edit_structured.tmpl [new file with mode: 0644]
templates/index.tmpl [deleted file]
templates/ledger_raw.tmpl [new file with mode: 0644]
templates/ledger_structured.tmpl [new file with mode: 0644]
templates/raw.tmpl [deleted file]

index 016e087fc267085c3ea60622169c66036eff1034..87f58f5cc41353539d8f2e082814064da770fd60 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -221,30 +221,52 @@ class Handler(PlomHttpHandler):
 
     def do_POST(self) -> None:
         # pylint: disable=invalid-name,missing-function-docstring
+        redir_path = Path('/')
+        if self.pagename.startswith('edit_'):
+            id_ = int(self.path_toks[2])
+            redir_path = Path('/').joinpath('booking').joinpath(str(id_))
         if self.pagename == 'file':
             if 'reload' in self.postvars.as_dict:
                 self.server.load()
             elif 'save' in self.postvars.as_dict:
                 self.server.save()
-        elif self.pagename == 'edit':
-            id_ = int(self.path_toks[2])
-            old_booking = self.server.bookings[id_]
-            start_idx = self.server.dat_lines.index(old_booking.dat_lines[0])
-            end_idx = self.server.dat_lines.index(old_booking.dat_lines[-1])
+        elif self.pagename == 'edit_structured':
+            line_keys = self.postvars.keys_prefixed('line_')
+            lineno_to_inputs: dict[int, list[str]] = {}
+            for key in line_keys:
+                toks = key.split('_', maxsplit=2)
+                lineno = int(toks[1])
+                if lineno not in lineno_to_inputs:
+                    lineno_to_inputs[lineno] = []
+                lineno_to_inputs[lineno] += [toks[2]]
+            new_dat_lines = []
+            indent = '  '
+            for lineno, input_names in lineno_to_inputs.items():
+                data = ''
+                comment = self.postvars.first(f'line_{lineno}_comment')
+                for name in input_names:
+                    input_ = self.postvars.first(f'line_{lineno}_{name}')
+                    if name == 'intro':
+                        data = input_
+                    elif name == 'error':
+                        data = f'{indent}{input_}'
+                    elif name == 'account':
+                        data = f'{indent}{input_}'
+                    elif name in {'amount', 'currency'}:
+                        data += f'  {input_}'
+                new_dat_lines += [
+                        DatLine(f'{data} ; {comment}' if comment else data)]
+            self.server.rewrite_booking(id_, new_dat_lines)
+        elif self.pagename == 'edit_raw':
             new_dat_lines = [DatLine(line) for line
                              in self.postvars.first('booking').splitlines()]
-            self.server.dat_lines = (self.server.dat_lines[:start_idx]
-                                     + new_dat_lines
-                                     + self.server.dat_lines[end_idx+1:])
-            self.server.load_bookings()
-            self.redirect(Path('/').joinpath('booking').joinpath(str(id_)))
-            return
-        self.redirect(Path('/'))
+            self.server.rewrite_booking(id_, new_dat_lines)
+        self.redirect(redir_path)
 
     def do_GET(self) -> None:
         # pylint: disable=invalid-name,missing-function-docstring
         ctx = {'tainted': self.server.tainted}
-        if self.pagename in {'booking', 'edit'}:
+        if self.pagename == 'booking' or self.pagename.startswith('edit_'):
             ctx['id'] = int(self.path_toks[2])
             ctx['dat_lines'] = self.server.bookings[ctx['id']].dat_lines
         if self.pagename == 'balance':
@@ -252,22 +274,23 @@ class Handler(PlomHttpHandler):
                     int(self.params.first('cutoff') or '0'))
             self.send_rendered(Path('balance.tmpl'),
                                ctx | {'roots': balance_roots, 'valid': valid})
-        elif self.pagename == 'booking':
-            self.send_rendered(Path('booking.tmpl'), ctx)
-        elif self.pagename == 'edit':
-            self.send_rendered(Path('edit.tmpl'), ctx)
-        elif self.pagename == 'raw':
-            self.send_rendered(Path('raw.tmpl'),
+        elif self.pagename in {'booking', 'edit_structured'}:
+            self.send_rendered(Path('edit_structured.tmpl'), ctx)
+        elif self.pagename == 'edit_raw':
+            self.send_rendered(Path('edit_raw.tmpl'), ctx)
+        elif self.pagename == 'ledger_raw':
+            self.send_rendered(Path('ledger_raw.tmpl'),
                                ctx | {'dat_lines': self.server.dat_lines})
         else:
             self.send_rendered(
-                    Path('index.tmpl'),
+                    Path('ledger_structured.tmpl'),
                     ctx | {'dat_lines': self.server.dat_lines_sans_empty})
 
 
 class Server(PlomHttpServer):
     """Extends parent by loading .dat file into database for Handler."""
     bookings: list[Booking]
+    dat_lines: list[DatLine]
 
     def __init__(self, path_dat: Path, *args, **kwargs) -> None:
         super().__init__(PATH_TEMPLATES, (SERVER_HOST, SERVER_PORT), Handler)
@@ -313,6 +336,15 @@ class Server(PlomHttpServer):
             '\n'.join([line.raw for line in self.dat_lines]), encoding='utf8')
         self.load()
 
+    def rewrite_booking(self, id_: int, new_dat_lines: list[DatLine]) -> None:
+        """Rewrite .dat_lines for Booking of .id_ with new_dat_lines."""
+        old_booking = self.bookings[id_]
+        start_idx = self.dat_lines.index(old_booking.dat_lines[0])
+        end_idx = self.dat_lines.index(old_booking.dat_lines[-1])
+        self.dat_lines = (self.dat_lines[:start_idx]
+                          + new_dat_lines + self.dat_lines[end_idx+1:])
+        self.load_bookings()
+
     @property
     def dat_lines_sans_empty(self) -> list[DatLine]:
         """Return only those .data_lines with .code or .comment."""
index a613a1bfe5381ed47641ece8c7fa3a98927ff946..d6881d24bfb5f3c126f28392cf19d41419285971 100644 (file)
@@ -13,7 +13,7 @@ span.warning, table.warning tbody tr td, tr.warning td { background-color: #ff88
 </head>
 <body>
 <form action="/file" method="POST">
-<a href="/">home</a> · <a href="/raw">raw</a> · <a href="/balance">balance</a> · <input type="submit" name="reload" value="reload" />{% if tainted %} · <span class="warning">unsaved changes: <input type="submit" name="save" value="save"></span>{% endif %}
+ledger <a href="/ledger_structured">structured</a> / <a href="/ledger_raw">raw</a> · <a href="/balance">balance</a> · <input type="submit" name="reload" value="reload" />{% if tainted %} · <span class="warning">unsaved changes: <input type="submit" name="save" value="save"></span>{% endif %}
 </form>
 <hr />
 {% block content %}{% endblock %}
index 850f4c5cfb53d6ee4c3855ee95c1e08b30e0a492..1412c84df2b1c54d23e6417d0d202c7b1e7fa88a 100644 (file)
@@ -7,19 +7,17 @@ td.amt, td.curr { font-family: monospace; font-size: 1.3em; }
 td.invalid, tr.warning td.invalid { background-color: #ff0000; }
 {% endmacro %}
 
-{% macro table_dat_lines(dat_lines, single, raw) %}
+{% macro table_dat_lines(dat_lines, raw) %}
 <table>
 {% for dat_line in dat_lines %}
-  {% if (not (raw or single)) and dat_line.is_intro and loop.index > 1 %}
+  {% if (not raw) and dat_line.is_intro and loop.index > 1 %}
     <tr><td colspan=5>&nbsp;</td></tr>
   {% endif %}
   <tr{% if dat_line.is_questionable %} class="warning"{% endif %}>
-  {% if not single %}
-    {% if dat_line.is_intro %}
-      <td id="{{dat_line.booking_id}}"><a href="#{{dat_line.booking_id}}">#</a>/<a href="/balance?cutoff={{dat_line.booking_id+1}}">b</a></td>
-    {% else %}
-      <td></td>
-    {% endif %}
+  {% if dat_line.is_intro %}
+    <td id="{{dat_line.booking_id}}"><a href="#{{dat_line.booking_id}}">#</a>/<a href="/balance?cutoff={{dat_line.booking_id+1}}">b</a></td>
+  {% else %}
+    <td></td>
   {% endif %}
   {% if raw %}
     <td{% if dat_line.error %} class="invalid"{% endif %}>
@@ -44,9 +42,7 @@ td.invalid, tr.warning td.invalid { background-color: #ff0000; }
   </tr>
   {% if dat_line.error and not raw %}
     <tr class="warning">
-    {% if not single %}
-      <td></td>
-    {% endif %}
+    <td></td>
     <td class="invalid" colspan=3>{{dat_line.error}}</td>
     <td></td>
     </tr>
diff --git a/templates/booking.tmpl b/templates/booking.tmpl
deleted file mode 100644 (file)
index 9470c27..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends '_base.tmpl' %}
-
-
-{% block css %}
-{{ macros.css_td_money() }}
-{{ macros.css_errors() }}
-{% endblock %}
-
-{% block content %}
-<a href="/edit/{{id}}">edit</a>
-<hr />
-{{ macros.table_dat_lines(dat_lines, single=true, raw=false) }}
-{% endblock %}
diff --git a/templates/edit_raw.tmpl b/templates/edit_raw.tmpl
new file mode 100644 (file)
index 0000000..ad344a8
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+{{ macros.css_td_money() }}
+{{ macros.css_errors() }}
+{% endblock %}
+
+{% block content %}
+<a href="/edit_structured/{{id}}">edit structured</a>
+<hr />
+<form action="/edit_raw/{{id}}" method="POST">
+<textarea name="booking" cols=100 rows=100>
+{% for dat_line in dat_lines %}{{ dat_line.raw }}
+{% endfor %}</textarea>
+<input type="submit" value="update" />
+</form>
+{% endblock %}
diff --git a/templates/edit_structured.tmpl b/templates/edit_structured.tmpl
new file mode 100644 (file)
index 0000000..6e13a9a
--- /dev/null
@@ -0,0 +1,37 @@
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+{{ macros.css_td_money() }}
+{{ macros.css_errors() }}
+{% endblock %}
+
+{% block content %}
+<a href="/edit_raw/{{id}}">edit raw</a>
+<hr />
+<form action="/edit_structured/{{id}}" method="POST">
+<table>
+{% for dat_line in dat_lines %}
+  <tr>
+  {% if dat_line.is_intro %}
+    <td{% if dat_line.error %} class="invalid"{% endif %} colspan=3><input name="line_{{loop.index0}}_intro" value="{{dat_line.code}}"/></td>
+  {% elif not dat_line.error %}
+    <td><input name="line_{{loop.index0}}_account" value="{{dat_line.booking_line.account}}" /></td>
+    <td><input name="line_{{loop.index0}}_amount" value="{{dat_line.booking_line.amount or ''}}" /></td>
+    <td><input name="line_{{loop.index0}}_currency" value="{{dat_line.booking_line.currency or ''}}" /></td>
+  {% else %}
+    <td class="invalid" colspan=3><input name="line_{{loop.index0}}_error" value="{{dat_line.code|trim}}" /></td>
+  {% endif %}
+  <td><input name="line_{{loop.index0}}_comment" value="{{dat_line.comment|trim}}" /></td>
+  </tr>
+  {% if dat_line.error %}
+    <tr class="warning">
+    <td class="invalid" colspan=3>{{dat_line.error}}</td>
+    <td></td>
+    </tr>
+  {% endif %}
+{% endfor %}
+</table>
+<input type="submit" value="update" />
+</form>
+{% endblock %}
diff --git a/templates/index.tmpl b/templates/index.tmpl
deleted file mode 100644 (file)
index 886a01c..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends '_base.tmpl' %}
-
-
-{% block css %}
-{{ macros.css_td_money() }}
-{{ macros.css_errors() }}
-{% endblock %}
-
-{% block content %}
-{{ macros.table_dat_lines(dat_lines, single=false, raw=false) }}
-{% endblock %}
diff --git a/templates/ledger_raw.tmpl b/templates/ledger_raw.tmpl
new file mode 100644 (file)
index 0000000..d09e722
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+table { font-family: monospace; }
+{{ macros.css_errors() }}
+{% endblock %}
+
+{% block content %}
+{{ macros.table_dat_lines(dat_lines, raw=true) }}
+{% endblock %}
+
diff --git a/templates/ledger_structured.tmpl b/templates/ledger_structured.tmpl
new file mode 100644 (file)
index 0000000..1669ce9
--- /dev/null
@@ -0,0 +1,11 @@
+{% extends '_base.tmpl' %}
+
+
+{% block css %}
+{{ macros.css_td_money() }}
+{{ macros.css_errors() }}
+{% endblock %}
+
+{% block content %}
+{{ macros.table_dat_lines(dat_lines, raw=false) }}
+{% endblock %}
diff --git a/templates/raw.tmpl b/templates/raw.tmpl
deleted file mode 100644 (file)
index 8b2d621..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{% extends '_base.tmpl' %}
-
-
-{% block css %}
-table { font-family: monospace; }
-{{ macros.css_errors() }}
-{% endblock %}
-
-{% block content %}
-{{ macros.table_dat_lines(dat_lines, single=false, raw=true) }}
-{% endblock %}
-