return
ctx = {'unsaved_changes': self.server.ledger.tainted,
'path': self.path}
- if self.pagename == PAGENAME_BALANCE:
+ if self.pagename.endswith('.js'):
+ self.get_js()
+ elif self.pagename == PAGENAME_BALANCE:
self.get_balance(ctx)
elif self.pagename.startswith(_PREFIX_EDIT):
self.get_edit(ctx, self.pagename == PAGENAME_EDIT_RAW)
else:
self.get_ledger(ctx, False)
+ def get_js(self) -> None:
+ 'Deliver .js module.'
+ ctx = {}
+ if self.pagename == PAGENAME_EDIT_STRUCTURED + '.js':
+ block = self.server.ledger.blocks[int(self.path_toks[2])]
+ ctx['raw_gap_lines'] = [dl.raw for dl in block.gap.lines]
+ ctx['booking_lines'] = (
+ [block.booking.intro_line.as_dict]
+ + [tf_line.as_dict for tf_line in block.booking.transfer_lines]
+ ) if block.booking else []
+ self.send_http(
+ bytes(self.server.jinja.get_template(self.pagename).render(**ctx),
+ encoding='utf8'),
+ [('Content-Type', 'text/javascript')])
+
def get_balance(self, ctx) -> None:
'Display tree of calculated Accounts over blocks up_incl+1.'
id_str = self.params.first('up_incl')
self.last_save_hash = self._hash_dat_lines()
@property
- def _blocks(self) -> list[_DatBlock]:
+ def blocks(self) -> list[_DatBlock]:
'Return blocks chain as list.'
blocks = []
block = self._blocks_start
@property
def _dat_lines(self) -> list[_DatLine]:
- 'From ._blocks build list of current _DatLines.'
+ 'From .blocks build list of current _DatLines.'
lines = []
- for block in self._blocks:
+ for block in self.blocks:
lines += block.lines
return [_DatLine(line.code, line.comment) for line in lines]
for acc_name, desc in dat_line.comment_instructions.items():
ensure_accounts(acc_name)
accounts[acc_name].desc = desc
- for block in [b for b in self._blocks if b.booking]:
+ for block in [b for b in self.blocks if b.booking]:
assert block.booking is not None
for acc_name, wealth in block.booking.diffs_targeted.items():
ensure_accounts(acc_name)
def _blocks_valid_up_incl(self, block_id: int) -> bool:
'Whether nothing questionable about blocks until block_id.'
- for block in self._blocks[:block_id]:
+ for block in self.blocks[:block_id]:
if block.booking:
if block.booking.sink_error:
return False
@property
def _has_redundant_empty_lines(self) -> bool:
'If any gaps have redunant empty lines.'
- return bool([b for b in self._blocks if b.gap.redundant_empty_lines])
+ return bool([b for b in self.blocks if b.gap.redundant_empty_lines])
def remove_redundant_empty_lines(self) -> None:
- 'From all ._blocks remove redundant empty lines.'
- for gap in [b.gap for b in self._blocks
+ 'From all .blocks remove redundant empty lines.'
+ for gap in [b.gap for b in self.blocks
if b.gap.redundant_empty_lines]:
gap.remove_redundant_empty_lines()
def move_block(self, idx_from: int, up: bool) -> int:
'Move _DatBlock of idx_from step up or downwards.'
- block = self._blocks[idx_from]
+ block = self.blocks[idx_from]
block.move(up)
return block.id_
lines_gap_pre_booking += [_GapLine.from_dat(dat_line)]
else:
lines_gap_post_booking += [_GapLine.from_dat(dat_line)]
- old_block = self._blocks[old_id]
+ old_block = self.blocks[old_id]
if not lines_booking:
if old_block.prev:
old_block.prev.gap.add(lines_gap_pre_booking)
return max(0, old_id - 1)
new_block = _DatBlock(_Booking(lines_booking),
_Gap(lines_gap_post_booking))
- self._blocks[old_id].replace_with(new_block)
+ self.blocks[old_id].replace_with(new_block)
if not new_block.prev:
self._blocks_start = _DatBlock(None, _Gap())
self._blocks_start.next = new_block
'Add new _DatBlock of empty _Booking to end of ledger.'
new_block = _DatBlock(
_Booking([_IntroLine(dt_date.today().isoformat(), '?')]))
- self._blocks[-1].next = new_block
+ self.blocks[-1].next = new_block
new_block.fix_position()
return new_block.id_
def copy_block(self, id_: int) -> int:
'Add copy _DatBlock of id_ but with current date.'
- copy = self._blocks[id_].copy_to_current_date()
+ copy = self.blocks[id_].copy_to_current_date()
return copy.id_
def view_ctx_balance(self, id_: int = -1) -> dict[str, Any]:
'All context data relevant for rendering a balance view.'
if id_ < 0:
- id_ = len(self._blocks) - 1
+ id_ = len(self.blocks) - 1
return {'valid': self._blocks_valid_up_incl(id_),
- 'block': self._blocks[id_],
+ 'block': self.blocks[id_],
'roots': sorted([ac for ac in self._calc_accounts().values()
if not ac.parent],
key=lambda root: root.basename)}
def view_ctx_edit(self, id_: int, raw=True) -> dict[str, Any]:
'All context data relevant for rendering an edit view.'
accounts = self._calc_accounts()
- block = self._blocks[id_]
+ block = self.blocks[id_]
roots: list[dict[str, Any]] = []
for full_path in sorted(block.booking.diffs_targeted.keys()
if block.booking else []):
if not raw:
ctx['raw_gap_lines'] = [dl.raw for dl in block.gap.lines]
ctx['all_accounts'] = sorted(accounts.keys())
- ctx['booking_lines'] = (
- [block.booking.intro_line.as_dict]
- + [tf_line.as_dict for tf_line in block.booking.transfer_lines]
- ) if block.booking else []
return ctx
def view_ctx_ledger(self) -> dict[str, Any]:
<html>
<head>
<meta charset="UTF-8">
-<script>
{% block script %}
{% endblock %}
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="{{ path }}" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
-{% macro js_taint() %}
-var suppress_beforeunload = false;
-
-function taint() {
- // activate buttons "apply", "revert"
- Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
- el.disabled = false;
- el.addEventListener('click', function(e) {
- suppress_beforeunload = true;
- });
- });
- // deactivate "disable_on_change" span contents
- function recursive_span_disable(el) {
- old_nodes = Array.from(el.childNodes);
- el.innerHTML = '';
- old_nodes.forEach((node) => {
- if (node.tagName == 'SPAN') {
- recursive_span_disable(node);
- el.appendChild(node);
- } else if (node.tagName == 'INPUT') {
- node.disabled = true;
- el.appendChild(node);
- } else if (node.tagName == 'A') {
- const del = document.createElement('del');
- del.textContent = node.textContent;
- el.appendChild(del);
- } else {
- el.appendChild(node);
- };
- });
- }
- Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
- recursive_span_disable(el);
- });
- // try to catch user closing or reloading window
- window.addEventListener('beforeunload', function(e) {
- if (!suppress_beforeunload) {
- e.preventDefault();
- e.returnValue = true;
- }
- });
- // remove oninput handlers no longer needed (since we only ever go one way)
- Array.from(document.querySelectorAll('*')
- ).filter(el => (el.oninput !== null)
- ).forEach(el => el.oninput = null);
-}
-{% endmacro %}
-
-
-
{% macro edit_bar(block, here, there) %}
<form action="/edit_{{ here }}/{{ block.id_ }}" method="POST">
-<span class="disable_on_change">
+<span class="disable_on_taint">
{{ conditional_block_nav('/blocks/', 'prev', block) -}}
{{ conditional_block_nav('/blocks/', 'next', block) -}}
</span>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="apply"
value="apply"
disabled
/>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="revert"
value="revert"
disabled
/>
-<span class="disable_on_change">
+<span class="disable_on_taint">
<a href="/edit_{{ there }}/{{ block.id_ }}">switch to {{ there }}</a>
·
<a href="/balance?up_incl={{ block.id_ }}">balance after</a>
{% block script %}
-{{ macros.js_taint() }}
+<script type="module" src="/taint.js">
+</script>
{% endblock %}
{% block content %}
{{ macros.edit_bar(block, 'raw', 'structured') }}
<textarea name="raw_lines"
+ class="tainter"
cols=100
rows={{ block.lines|length + 1 }}
- oninput="taint()"
>
{% for dat_line in block.lines %}
{{ dat_line.raw }}
--- /dev/null
+/*
+global
+document
+window
+*/
+/*
+eslint
+"arrow-body-style": [
+ "error",
+ "as-needed"
+],
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"function-paren-newline": "off",
+"line-comment-position": "off",
+"lines-around-comment": [
+ "error",
+ { "beforeBlockComment": false }
+],
+"max-lines": [
+ "error",
+ {"max": 355, "skipBlankLines": true, "skipComments": true}
+],
+"max-lines-per-function": [
+ "error",
+ 238
+],
+"max-params": [
+ "error",
+ 4
+],
+"max-statements": [
+ "error",
+ 38
+],
+"multiline-comment-style": [
+ "error",
+ "bare-block"
+],
+"multiline-ternary": "off",
+"newline-after-var": "off",
+"newline-before-return": "off",
+"no-inline-comments": "off",
+"no-plusplus": "off",
+"no-ternary": "off",
+"one-var": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+import {
+ taint
+} from "/taint.js";
+
+const
+ IDX_LAST = -1,
+ IDX_PAST_INTRO_LINE = 1,
+ IDX_START = 0,
+ LEN_ACCOUNT = 30,
+ LEN_AMOUNT = 12,
+ LEN_COLSPAN_DEFAULT = 1,
+ LEN_COLSPAN_ERROR = 7,
+ LEN_COMMENT = 40,
+ LEN_CURRENCY = 3,
+ LEN_DATE = 10,
+ LEN_EMPTY = 0,
+ LEN_INTRO_LINE = 3,
+ LEN_LINE_STEP = 0,
+ LEN_TARGET = 37,
+ bookingLines = {{ booking_lines|tojson|safe }},
+ rawGapLines = {{ raw_gap_lines|tojson|safe }};
+
+const newBookingLine = (
+ account = "",
+ amount = "None",
+ currency = ""
+) => ({
+ account,
+ amount,
+ "comment": "",
+ currency,
+ "errors": []
+});
+
+const taintAndUpdateForm = () => {
+ taint();
+ updateForm(); // eslint-disable-line no-use-before-define
+};
+
+const updateForm = () => {
+ const
+ table = document.getElementById("booking_lines"),
+ textarea = document.getElementById("gap_lines");
+
+ // empty and redo gapLines, empty bookingLines table
+ textarea.value = "";
+ rawGapLines.forEach((line) => {
+ textarea.value += `${line}\n`;
+ });
+ table.innerHTML = "";
+
+ // basic helpers
+ const addButton = (
+ parentTd,
+ label,
+ disabled,
+ onclick
+ ) => {
+ /* add button to td to run onclick (after updating bookingLines from
+ from inputs, and followed by calling taint and updateForm) */
+ const btn = document.createElement("button");
+ parentTd.appendChild(btn);
+ btn.textContent = label;
+ btn.type = "button"; // otherwise would act as form submit
+ btn.disabled = disabled;
+ btn.onclick = () => {
+ let nRowsSkipped = LEN_EMPTY; // ignore bookingLines-empty rows
+ table.rows.forEach((row, idx) => {
+ if (row.classList.contains("skip")) {
+ nRowsSkipped++;
+ return;
+ }
+ for (const input of row.querySelectorAll("td > input")) {
+ const
+ key = input.name.split("_").at(IDX_LAST),
+ lineToUpdate = bookingLines[idx - nRowsSkipped];
+ lineToUpdate[key] = input.value;
+ }
+ });
+ onclick();
+ taintAndUpdateForm();
+ };
+ };
+ const addTd = (
+ tr,
+ colspan = LEN_COLSPAN_DEFAULT
+ ) => {
+ const td = document.createElement("td");
+ tr.appendChild(td);
+ td.colSpan = colspan;
+ return td;
+ };
+
+ // work through individual booking lines
+ bookingLines.forEach((
+ bookingLine,
+ idx
+ ) => {
+ const tr = document.createElement("tr");
+ table.appendChild(tr);
+
+ // helpers depending on line-specific variables
+ const setupInputTd = (
+ tr_,
+ colspan
+ ) => {
+ const td = addTd(
+ tr_,
+ colspan
+ );
+ if (bookingLine.errors.length > LEN_EMPTY) {
+ td.classList.add("critical");
+ }
+ return td;
+ };
+ const addInput = (
+ td,
+ name,
+ value,
+ size
+ ) => {
+ const input = document.createElement("input");
+ td.appendChild(input);
+ input.name = `line_${idx}_${name}`;
+ input.value = value.trim();
+ input.size = size;
+ input.oninput = taint;
+ return input;
+ };
+ const addTdInput = (
+ name,
+ value,
+ size,
+ colspan = LEN_COLSPAN_DEFAULT
+ ) => addInput(
+ setupInputTd(
+ tr,
+ colspan
+ ),
+ name,
+ value,
+ size
+ );
+
+ // movement buttons
+ const tdBtnsUpdown = addTd(tr);
+ if (idx > IDX_START) {
+ [
+ {
+ "earlierIdx": idx - LEN_LINE_STEP,
+ "enabled": idx > IDX_PAST_INTRO_LINE,
+ "label": "^"
+ },
+ {
+ "earlierIdx": idx,
+ "enabled": idx &&
+ idx + LEN_LINE_STEP < bookingLines.length,
+ "label": "v"
+ }
+ ].forEach((kwargs) => addButton(
+ tdBtnsUpdown,
+ kwargs.label,
+ !kwargs.enabled,
+ () => {
+ const otherLine = bookingLines[kwargs.earlierIdx];
+ bookingLines.splice(
+ kwargs.earlierIdx,
+ LEN_LINE_STEP
+ );
+ bookingLines.splice(
+ kwargs.earlierIdx + LEN_LINE_STEP,
+ LEN_EMPTY,
+ otherLine
+ );
+ }
+ ));
+ }
+
+ // actual input lines
+ if (idx === IDX_START) {
+ const td = setupInputTd(
+ tr,
+ LEN_INTRO_LINE
+ );
+ const dateInput = addInput(
+ td,
+ "date",
+ bookingLine.date,
+ LEN_DATE
+ );
+ dateInput.id = "date_input";
+ addInput(
+ td,
+ "target",
+ bookingLine.target,
+ LEN_TARGET
+ );
+ } else {
+ const accInput = addTdInput(
+ "account",
+ bookingLine.account,
+ LEN_ACCOUNT
+ );
+ accInput.setAttribute(
+ "list",
+ "all_accounts"
+ );
+ accInput.autocomplete = "off";
+ /* not using input[type=number] cuz no minimal step size,
+ therefore regex test instead */
+ const amtInputVal =
+ bookingLine.amount === "None" ? "" : bookingLine.amount;
+ const amtInput = addTdInput(
+ "amount",
+ amtInputVal,
+ LEN_AMOUNT
+ );
+ amtInput.pattern = "^-?[0-9]+(.[0-9]+)?$";
+ amtInput.classList.add("amount");
+ // ensure integer amounts line up with double-digit decimals
+ if (amtInput.value.match("/^-?[0-9]+$/")) {
+ amtInput.value += ".00";
+ }
+ // imply POST handler to set "€" currency if unset, but amount set
+ const currInput = addTdInput(
+ "currency",
+ bookingLine.currency,
+ LEN_CURRENCY
+ );
+ currInput.placeholder = "€";
+ }
+ addTdInput(
+ "comment",
+ bookingLine.comment,
+ LEN_COMMENT
+ );
+
+ // line deletion and addition buttons
+ const tdAddDel = addTd(tr);
+ addButton(
+ tdAddDel,
+ "add new",
+ false,
+ () => bookingLines.splice(
+ idx + LEN_LINE_STEP,
+ LEN_EMPTY,
+ newBookingLine()
+ )
+ );
+ if (idx > IDX_START) {
+ addButton(
+ tdAddDel,
+ "delete",
+ idx <= IDX_START,
+ () => bookingLines.splice(
+ idx,
+ LEN_LINE_STEP
+ )
+ );
+ }
+
+ // add error explanation row if necessary
+ if (bookingLine.errors.length > LEN_EMPTY) {
+ tr.classList.add("critical");
+ const trInfo = document.createElement("tr");
+ trInfo.classList.add("skip");
+ table.appendChild(trInfo);
+ const td = addTd(
+ trInfo,
+ LEN_COLSPAN_ERROR
+ );
+ tr.appendChild(document.createElement("td"));
+ td.textContent = `line bad: ${bookingLine.errors}`;
+ trInfo.classList.add("critical");
+ }
+ });
+};
+
+const replace = () => {
+ const
+ replFrom = document.getElementById("replace_from").value,
+ replTo = document.getElementById("replace_to").value;
+ rawGapLines.forEach((line) => line.replaceAll(
+ replFrom,
+ replTo
+ ));
+ bookingLines.forEach((bookingLine) => {
+ Object.keys(bookingLine).
+ filter((key) => key !== "errors").
+ forEach((key) => {
+ bookingLine[key] = bookingLine[key].replaceAll(
+ replFrom,
+ replTo
+ );
+ });
+ });
+ taintAndUpdateForm();
+};
+
+const mirror = () => {
+ bookingLines.slice(IDX_PAST_INTRO_LINE).forEach((bookingLine) => {
+ let invertedAmount = "None";
+ if (bookingLine.amount !== "None") {
+ invertedAmount = `-${bookingLine.amount}`;
+ const doubleMinus = "--";
+ if (invertedAmount.startsWith(doubleMinus)) {
+ invertedAmount = invertedAmount.slice(doubleMinus.length);
+ }
+ }
+ bookingLines.push(
+ newBookingLine(
+ "?",
+ invertedAmount,
+ bookingLine.currency
+ )
+ );
+ });
+ taintAndUpdateForm();
+};
+
+const fillSink = () => {
+ const
+ sinkAmountsPerCurrency = {},
+ sinkIndices = [],
+ sumPerCurrency = {};
+ let
+ sinkAccount = "",
+ sinkIndicesIdx = IDX_START;
+ bookingLines.forEach((bookingLine, idx) => {
+ if (idx === IDX_START) {
+ return;
+ }
+ const currency = bookingLine.currency || "€";
+ if (bookingLine.amount === "None") {
+ if (sinkAccount === bookingLine.account || !sinkAccount) {
+ if (!sinkAccount) {
+ sinkAccount = bookingLine.account;
+ }
+ sinkIndices.push(idx);
+ }
+ } else {
+ if (!Object.hasOwn(
+ sumPerCurrency,
+ currency
+ )) {
+ sumPerCurrency[currency] = 0;
+ }
+ sumPerCurrency[currency] += parseFloat(bookingLine.amount);
+ }
+ });
+ if (!sinkAccount) {
+ sinkAccount = "?";
+ }
+ for (const [
+ currency,
+ amount
+ ] of Object.entries(sumPerCurrency)) {
+ if (amount !== LEN_EMPTY) {
+ sinkAmountsPerCurrency[currency] = -amount;
+ }
+ }
+ for (
+ let idx = IDX_START;
+ idx < Object.keys(sinkAmountsPerCurrency).length - sinkIndices.length;
+ idx++
+ ) {
+ sinkIndices.push(bookingLines.length);
+ bookingLines.push(newBookingLine(sinkAccount));
+ }
+ for (const [
+ currency,
+ amount
+ ] of Object.entries(sinkAmountsPerCurrency)) {
+ const bookingLine = bookingLines[sinkIndices[sinkIndicesIdx]];
+ sinkIndicesIdx++;
+ bookingLine.currency = currency;
+ bookingLine.amount = amount.toString();
+ }
+ taintAndUpdateForm();
+};
+
+window.onload = () => {
+ document.getElementById("btn_mirror").onclick = mirror;
+ document.getElementById("btn_sink").onclick = fillSink;
+ document.getElementById("btn_replace").onclick = replace;
+ updateForm();
+};
+
{% block script %}
-{{ macros.js_taint() }}
-var raw_gap_lines = {{ raw_gap_lines|tojson|safe }};
-var booking_lines = {{ booking_lines|tojson|safe }};
-
-function new_booking_line(account='', amount='None', currency='') {
- return {
- errors: [],
- comment: '',
- account: account,
- amount: amount,
- currency: currency,
- };
-}
-
-function update_form() {
- // empty and redo gap_lines
- textarea = document.getElementById('gap_lines');
- textarea.value = '';
- raw_gap_lines.forEach((line) => {
- textarea.value += `${line}\n`;
- });
-
- // catch and empty booking lines table
- const table = document.getElementById('booking_lines');
- table.innerHTML = '';
-
- // basic helpers
- function add_button(parent_td, label, disabled, onclick) {
- // add button to td to run onclick (after updating booking_lines from inputs,
- // and followed by calling taint and update_form)
- const btn = document.createElement("button");
- parent_td.appendChild(btn);
- btn.textContent = label;
- btn.type = 'button'; // otherwise would act as form submit
- btn.disabled = disabled;
- btn.onclick = function() {
- let n_rows_skipped = 0; // to ignore table rows not representing booking_lines
- for (let i = 0; i < table.rows.length; i++) {
- const row = table.rows[i];
- if (row.classList.contains('skip')) {
- n_rows_skipped++;
- continue;
- };
- for (const input of table.rows[i].querySelectorAll('td > input')) {
- const line_to_update = booking_lines[i - n_rows_skipped];
- const key = input.name.split('_').at(-1);
- line_to_update[key] = input.value;
- }
- }
- onclick();
- taint();
- update_form();
- };
- }
- function add_td(tr, colspan=1) {
- const td = document.createElement('td');
- tr.appendChild(td);
- td.colSpan = colspan;
- return td;
- }
-
- // work through individual booking lines
- for (let i = 0; i < booking_lines.length; i++) {
- const booking_line = booking_lines[i];
- const tr = document.createElement('tr');
- table.appendChild(tr);
-
- // helpers depending on line-specific variables
- function setup_input_td(tr, colspan) {
- const td = add_td(tr, colspan);
- if (booking_line.errors.length > 0) {
- td.classList.add('critical');
- };
- return td;
- }
- function add_input(td, name, value, size) {
- const input = document.createElement('input');
- td.appendChild(input);
- input.name = `line_${i}_${name}`;
- input.value = value.trim();
- input.size = size;
- input.oninput = taint;
- return input;
- }
- function add_td_input(name, value, size=20, colspan=1) {
- return add_input(setup_input_td(tr, colspan), name, value, size);
- }
-
- // movement buttons
- const td_btns_updown = add_td(tr);
- if (i > 0) {
- [{label: '^', earlier_idx: i-1, enabled: i > 1},
- {label: 'v', earlier_idx: i, enabled: i && i+1 < booking_lines.length}
- ].forEach((kwargs) => {
- add_button(td_btns_updown, kwargs.label, ! kwargs.enabled, function() {
- const other_line = booking_lines[kwargs.earlier_idx];
- booking_lines.splice(kwargs.earlier_idx, 1);
- booking_lines.splice(kwargs.earlier_idx + 1, 0, other_line);
- });
- });
- }
-
- // actual input lines
- if (i == 0) {
- const td = setup_input_td(tr, 3);
- const date_input = add_input(td, 'date', booking_line.date, 10)
- date_input.id = 'date_input';
- add_input(td, 'target', booking_line.target, 37)
- } else {
- const acc_input = add_td_input('account', booking_line.account, 30);
- acc_input.setAttribute ('list', 'all_accounts');
- acc_input.autocomplete = 'off';
- // not using input[type=number] cuz no minimal step size, therefore regex test instead
- const amt_input_val = booking_line.amount == 'None' ? '' : booking_line.amount;
- const amt_input = add_td_input('amount', amt_input_val, 12);
- amt_input.pattern = '^-?[0-9]+(\.[0-9]+)?$';
- amt_input.classList.add('amount');
- // ensure integer amounts at least line up with double-digit decimals
- if (amt_input.value.match(/^-?[0-9]+$/)) {
- amt_input.value += '.00';
- }
- // imply that POST handler will set '€' currency if unset, but amount set
- const curr_input = add_td_input('currency', booking_line.currency, 3);
- curr_input.placeholder = '€';
- }
- add_td_input('comment', booking_line.comment, 40);
-
- // line deletion and addition buttons
- td_add_del = add_td(tr);
- add_button(td_add_del, 'add new', false, function() {
- booking_lines.splice(i + 1, 0, new_booking_line());
- });
- if (i > 0) {
- add_button(td_add_del, 'delete', i > 0 ? false : true, function() {
- booking_lines.splice(i, 1);
- });
- }
-
- // add error explanation row if necessary
- if (booking_line.errors.length > 0) {
- tr.classList.add('critical');
- const tr_info = document.createElement('tr');
- tr_info.classList.add('skip');
- table.appendChild(tr_info);
- const td = add_td(tr_info, 7);
- tr.appendChild(document.createElement('td'));
- td.textContent = 'line bad:' + booking_line.errors;
- tr_info.classList.add('critical');
- }
- }
-}
-
-function replace() {
- const from = document.getElementById('replace_from').value;
- const to = document.getElementById('replace_to').value;
- raw_gap_lines.forEach((line) => {
- line.replaceAll(from, to);
- });
- booking_lines.forEach((booking_line) => {
- Object.keys(booking_line).forEach((key) => {
- if (key != 'errors') {
- booking_line[key] = booking_line[key].replaceAll(from, to);
- }
- });
- });
- taint();
- update_form();
-}
-
-function mirror() {
- booking_lines.slice(1).forEach((booking_line) => {
- let inverted_amount = 'None';
- if (booking_line.amount != 'None') {
- inverted_amount = `-${booking_line.amount}`;
- if (inverted_amount.startsWith('--')) {
- inverted_amount = inverted_amount.slice(2);
- }
- }
- booking_lines.push(new_booking_line('?', inverted_amount, booking_line.currency));
- })
- taint();
- update_form();
-}
-
-function fill_sink() {
- let sink_account = '';
- let sink_indices = [];
- const sum_per_currency = {};
- for (let i = 1; i < booking_lines.length; i++) {
- const booking_line = booking_lines[i];
- const currency = booking_line.currency || '€';
- if (booking_line.amount == 'None') {
- if (sink_account == booking_line.account || !sink_account) {
- if (!sink_account) {
- sink_account = booking_line.account;
- }
- sink_indices.push(i);
- }
- } else {
- if (!Object.hasOwn(sum_per_currency, currency)) {
- sum_per_currency[currency] = 0;
- }
- sum_per_currency[currency] += parseFloat(booking_line.amount);
- }
- }
- if (!sink_account) {
- sink_account = '?';
- }
- const sink_amounts_per_currency = {};
- for (const [currency, amount] of Object.entries(sum_per_currency)) {
- if (amount != 0) {
- sink_amounts_per_currency[currency] = -amount;
- }
- }
- for (i = 0; i < Object.keys(sink_amounts_per_currency).length - sink_indices.length; i++) {
- sink_indices.push(booking_lines.length);
- booking_lines.push(new_booking_line(sink_account));
- }
- let sink_indices_index = 0;
- for (const [currency, amount] of Object.entries(sink_amounts_per_currency)) {
- const booking_line = booking_lines[sink_indices[sink_indices_index]];
- sink_indices_index++;
- booking_line.currency = currency;
- booking_line.amount = amount.toString();
- }
- taint();
- update_form();
-}
-
-window.onload = update_form;
+<script type="module" src="/edit_structured.js/{{ block.id_ }}">
+</script>
{% endblock %}
{% block content %}
{{ macros.edit_bar(block, 'structured', 'raw') }}
-<span class="disable_on_change">
-<input type="button" onclick="mirror()" value="mirror">
-<input type="button" onclick="fill_sink()" value="fill sink">
+<span class="disable_on_taint">
+<input id="btn_mirror" type="button" value="mirror">
+<input id="btn_sink" type="button" value="fill sink">
|
-<input type="button" onclick="replace()" value="replace string">
+<input id="btn_replace" type="button" value="replace string">
from
<input id="replace_from" />
to
name="raw_lines"
cols=100
rows={{ raw_gap_lines|length + 1 }}
- oninput="taint()"
+ class="tainter"
>
</textarea>
</form>
--- /dev/null
+/*
+global
+document
+window
+*/
+/*
+eslint
+"brace-style": [
+ "error",
+ "1tbs",
+ { "allowSingleLine": true }
+],
+"capitalized-comments": [
+ "error",
+ "never"
+],
+"function-paren-newline": "off",
+"max-lines-per-function": [
+ "error",
+ 46
+],
+max-statements-per-line: [
+ "error",
+ { "max": 2 }
+],
+"multiline-comment-style": [
+ "error",
+ "bare-block"
+],
+"newline-after-var": "off",
+"padded-blocks": [
+ "error",
+ "never"
+],
+*/
+let suppressBeforeUnload = false;
+
+export const taint = () => {
+ // activate buttons "apply", "revert"
+ Array.from(document.getElementsByClassName("enable_on_taint")).forEach(
+ (el) => {
+ el.disabled = false;
+ el.onclick = () => { suppressBeforeUnload = true; };
+ }
+ );
+ // deactivate "disable_on_taint" span contents
+ const recursiveSpanDisable = (el) => {
+ const oldNodes = Array.from(el.childNodes);
+ el.innerHTML = "";
+ oldNodes.forEach((node) => {
+ if (node.tagName === "SPAN") {
+ recursiveSpanDisable(node);
+ el.appendChild(node);
+ } else if (node.tagName === "INPUT") {
+ node.disabled = true;
+ el.appendChild(node);
+ } else if (node.tagName === "A") {
+ const del = document.createElement("del");
+ del.textContent = node.textContent;
+ el.appendChild(del);
+ } else {
+ el.appendChild(node);
+ }
+ });
+ };
+ Array.from(document.getElementsByClassName("disable_on_taint")).forEach(
+ (el) => recursiveSpanDisable(el)
+ );
+ // try to catch user closing or reloading window
+ window.addEventListener(
+ "beforeunload",
+ (evt) => {
+ if (!suppressBeforeUnload) {
+ evt.preventDefault();
+ evt.returnValue = true;
+ }
+ }
+ );
+ // remove oninput handlers no longer needed (since we only ever go one way)
+ Array.from(document.querySelectorAll("*")).
+ filter((el) => el.oninput !== null).
+ forEach((el) => { el.oninput = null; });
+};
+
+window.onload = () => {
+ Array.from(document.getElementsByClassName("tainter")).forEach(
+ (el) => { el.oninput = taint; }
+ );
+};
+
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
<html>
<head>
<meta charset="UTF-8">
-<script>
-var suppress_beforeunload = false;
-
-function taint() {
- // activate buttons "apply", "revert"
- Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
- el.disabled = false;
- el.addEventListener('click', function(e) {
- suppress_beforeunload = true;
- });
- });
- // deactivate "disable_on_change" span contents
- function recursive_span_disable(el) {
- old_nodes = Array.from(el.childNodes);
- el.innerHTML = '';
- old_nodes.forEach((node) => {
- if (node.tagName == 'SPAN') {
- recursive_span_disable(node);
- el.appendChild(node);
- } else if (node.tagName == 'INPUT') {
- node.disabled = true;
- el.appendChild(node);
- } else if (node.tagName == 'A') {
- const del = document.createElement('del');
- del.textContent = node.textContent;
- el.appendChild(del);
- } else {
- el.appendChild(node);
- };
- });
- }
- Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
- recursive_span_disable(el);
- });
- // try to catch user closing or reloading window
- window.addEventListener('beforeunload', function(e) {
- if (!suppress_beforeunload) {
- e.preventDefault();
- e.returnValue = true;
- }
- });
- // remove oninput handlers no longer needed (since we only ever go one way)
- Array.from(document.querySelectorAll('*')
- ).filter(el => (el.oninput !== null)
- ).forEach(el => el.oninput = null);
-}
-
+<script type="module" src="/taint.js">
</script>
<style>
html {
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
</form>
</div>
<form action="/edit_raw/1" method="POST">
-<span class="disable_on_change">
+<span class="disable_on_taint">
<a href="/blocks/0">prev</a>
<a href="/blocks/2">next</a>
</span>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="apply"
value="apply"
disabled
/>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="revert"
value="revert"
disabled
/>
-<span class="disable_on_change">
+<span class="disable_on_taint">
<a href="/edit_structured/1">switch to structured</a>
·
<a href="/balance?up_incl=1">balance after</a>
<hr />
<textarea name="raw_lines"
+ class="tainter"
cols=100
rows=5
- oninput="taint()"
>
2001-01-01 test ; foo
foo 10 €
<html>
<head>
<meta charset="UTF-8">
-<script>
-var suppress_beforeunload = false;
-
-function taint() {
- // activate buttons "apply", "revert"
- Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
- el.disabled = false;
- el.addEventListener('click', function(e) {
- suppress_beforeunload = true;
- });
- });
- // deactivate "disable_on_change" span contents
- function recursive_span_disable(el) {
- old_nodes = Array.from(el.childNodes);
- el.innerHTML = '';
- old_nodes.forEach((node) => {
- if (node.tagName == 'SPAN') {
- recursive_span_disable(node);
- el.appendChild(node);
- } else if (node.tagName == 'INPUT') {
- node.disabled = true;
- el.appendChild(node);
- } else if (node.tagName == 'A') {
- const del = document.createElement('del');
- del.textContent = node.textContent;
- el.appendChild(del);
- } else {
- el.appendChild(node);
- };
- });
- }
- Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
- recursive_span_disable(el);
- });
- // try to catch user closing or reloading window
- window.addEventListener('beforeunload', function(e) {
- if (!suppress_beforeunload) {
- e.preventDefault();
- e.returnValue = true;
- }
- });
- // remove oninput handlers no longer needed (since we only ever go one way)
- Array.from(document.querySelectorAll('*')
- ).filter(el => (el.oninput !== null)
- ).forEach(el => el.oninput = null);
-}
-
+<script type="module" src="/taint.js">
</script>
<style>
html {
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
</form>
</div>
<form action="/edit_raw/4" method="POST">
-<span class="disable_on_change">
+<span class="disable_on_taint">
<a href="/blocks/3">prev</a>
<del>next</del>
</span>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="apply"
value="apply"
disabled
/>
-<input class="enable_on_change"
+<input class="enable_on_taint"
type="submit"
name="revert"
value="revert"
disabled
/>
-<span class="disable_on_change">
+<span class="disable_on_taint">
<a href="/edit_structured/4">switch to structured</a>
·
<a href="/balance?up_incl=4">balance after</a>
<hr />
<textarea name="raw_lines"
+ class="tainter"
cols=100
rows=7
- oninput="taint()"
>
2001-01-01 test
foo:x 10 €
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>
<html>
<head>
<meta charset="UTF-8">
-<script>
-</script>
<style>
html {
scroll-padding-top: 2em;
<body>
<div id="header">
<form action="" method="POST">
- <span class="disable_on_change">
+ <span class="disable_on_taint">
ledger <a href="/ledger_structured">structured</a>
/ <a href="/ledger_raw">raw</a>
· <a href="/balance">balance</a>