Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch editor extension #1632

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions frontend/src/svelte-custom-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
import type { SvelteComponent } from "svelte";

import ChartSwitcher from "./charts/ChartSwitcher.svelte";
import SliceEditor from "./editor/SliceEditor.svelte";

const components = new Map<string, typeof SvelteComponent<{ data?: unknown }>>([
const components = new Map<
string,
typeof SvelteComponent<Record<string, unknown>>
>([
["charts", ChartSwitcher],
["slice-editor", SliceEditor],
]);

/**
Expand All @@ -17,7 +22,7 @@ const components = new Map<string, typeof SvelteComponent<{ data?: unknown }>>([
* of the valid values in the Map above.
*/
export class SvelteCustomElement extends HTMLElement {
component?: SvelteComponent<{ data?: unknown }>;
component?: SvelteComponent<Record<string, unknown>>;

connectedCallback(): void {
if (this.component) {
Expand All @@ -31,10 +36,13 @@ export class SvelteCustomElement extends HTMLElement {
if (!Cls) {
throw new Error("Invalid component");
}
const props: { data?: unknown } = {};
const props: Record<string, unknown> = {};
const script = this.querySelector("script");
if (script && script.type === "application/json") {
props.data = JSON.parse(script.innerHTML);
const data: unknown = JSON.parse(script.innerHTML);
if (data instanceof Object) {
Object.assign(props, data);
}
}
this.component = new Cls({ target: this, props });
}
Expand Down
19 changes: 19 additions & 0 deletions src/fava/ext/batch_edit/BatchEdit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default {
async runQuery() {
const queryStr = document.getElementById("batch_edit_query").value;
console.log(queryStr);
if (!queryStr) {
return;
}
let searchParams = new URLSearchParams(window.location.search);
searchParams.set("query", queryStr);
window.location.search = searchParams.toString();
return;
},
onExtensionPageLoad() {
const submitQuery = document.getElementById("batch_query_submit");
submitQuery.addEventListener("click", () => {
this.runQuery();
});
},
};
69 changes: 69 additions & 0 deletions src/fava/ext/batch_edit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Batch editor extension for Fava.

This is a simple batch editor that allows a batch of 20 entries
retrieved with a BQL query to be edited on the same page

There is currently a limitation where each entry needs to be saved
individually
"""
from __future__ import annotations

from typing import TYPE_CHECKING

from fava.beans.abc import Transaction
from fava.beans.funcs import hash_entry
from fava.context import g
from fava.core.file import get_entry_slice
from fava.ext import FavaExtensionBase
from fava.helpers import FavaAPIError

if TYPE_CHECKING: # pragma: no cover
from fava.beans.abc import Directive


class BatchEdit(FavaExtensionBase):
"""Extension page that allows basic batch editing of entries."""

report_title = "Batch Editor"

has_js_module = True

def get_entries(self, entry_hashes: list[str]) -> dict[str, Directive]:
"""Find a set of entries.

Arguments:
entry_hashes: Hashes of the entries.

Returns:
A dictionary of { entry_id: entry } for each given entry hash that is found
"""
entries_set = set(entry_hashes)
hashed_entries = [(hash_entry(e), e) for e in g.filtered.entries]
return {
key: entry for key, entry in hashed_entries if key in entries_set
}

def source_slices(self, query: str) -> list[dict[str, str]]:
contents, _types, rows = self.ledger.query_shell.execute_query(
g.filtered.entries, f"SELECT distinct id WHERE {query}"
)
if contents and "ERROR" in contents:
raise FavaAPIError(contents)

transaction_ids = [row.id for row in rows]
entries = self.get_entries(transaction_ids)
results = []
for tx_id in transaction_ids[:20]:
entry = entries[tx_id]
# Skip generated entries
if isinstance(entry, Transaction) and entry.flag == "S":
continue
source_slice, sha256sum = get_entry_slice(entry)
results.append(
{
"slice": source_slice,
"entry_hash": tx_id,
"sha256sum": sha256sum,
}
)
return results
18 changes: 18 additions & 0 deletions src/fava/ext/batch_edit/templates/BatchEdit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div>
<h3>Query</h3>
<label for="batch_edit_query">SELECT id WHERE </label>
<input id="batch_edit_query" name="batch_edit_query">
<button id="batch_query_submit" class="extension-handler" data-handler-click="this.runQuery()">Run Query</button>
</div>
<br>
{% set query = request.args.get('query') %}
{% if query %}
<div>
{% for slice in extension.source_slices(query) %}
<div>
<span>{{slice["entry_hash"]}}</span>
<svelte-component type="slice-editor"><script type="application/json">{{ slice|tojson }}</script></svelte-component>
</div>
{% endfor %}
</div>
{% endif %}
4 changes: 3 additions & 1 deletion src/fava/help/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Extensions may also contain a report - this is detected when the extension's
class has a `report_title` attribute. The template for the report should be in a
`templates` subdirectory with a report matching the class's name. For example,
check out `fava.ext.portfolio_list` which has its template located at
`fava/ext/portfolio_list/templates/PortfolioList.html`.
`fava/ext/portfolio_list/templates/PortfolioList.html`, or `fava.ext.batch_edit`
which has its template located at
`fava/ext/batch_edit/templates/BatchEdit.html`.

Finally, extensions may contain a Javascript module to be loaded in the
frontend. The module should be in a Javascript file matching the class's name
Expand Down
2 changes: 1 addition & 1 deletion src/fava/templates/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
{% endfor %}
{% endif %}

<svelte-component type="charts"><script type="application/json">{{ chart_list|tojson }}</script></svelte-component>
<svelte-component type="charts"><script type="application/json">{{ {'data': chart_list}|tojson }}</script></svelte-component>

<div class="droptarget" data-account-name="{{ account_name }}">
<div class="headerline">
Expand Down
14 changes: 8 additions & 6 deletions src/fava/templates/balance_sheet.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{% import '_tree_table.html' as tree_table with context %}

<svelte-component type="charts"><script type="application/json">{{
[
chart_api.net_worth(),
chart_api.hierarchy(ledger.options['name_assets']),
chart_api.hierarchy(ledger.options['name_liabilities']),
chart_api.hierarchy(ledger.options['name_equity']),
]|tojson
{
'data': [
chart_api.net_worth(),
chart_api.hierarchy(ledger.options['name_assets']),
chart_api.hierarchy(ledger.options['name_liabilities']),
chart_api.hierarchy(ledger.options['name_equity']),
]
}|tojson
}}</script></svelte-component>

{% set root_tree_closed = g.filtered.root_tree_closed %}
Expand Down
16 changes: 9 additions & 7 deletions src/fava/templates/income_statement.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
{% set invert = ledger.fava_options.invert_income_liabilities_equity %}

<svelte-component type="charts"><script type="application/json">{{
[
chart_api.interval_totals(g.interval, (options['name_income'], options['name_expenses']), label=_('Net Profit'), invert=invert),
chart_api.interval_totals(g.interval, options['name_income'], label='{} ({})'.format(_('Income'), g.interval.label), invert=invert),
chart_api.interval_totals(g.interval, options['name_expenses'], label='{} ({})'.format(_('Expenses'), g.interval.label)),
chart_api.hierarchy(options['name_income']),
chart_api.hierarchy(options['name_expenses']),
]|tojson
{
'data': [
chart_api.interval_totals(g.interval, (options['name_income'], options['name_expenses']), label=_('Net Profit'), invert=invert),
chart_api.interval_totals(g.interval, options['name_income'], label='{} ({})'.format(_('Income'), g.interval.label), invert=invert),
chart_api.interval_totals(g.interval, options['name_expenses'], label='{} ({})'.format(_('Expenses'), g.interval.label)),
chart_api.hierarchy(options['name_income']),
chart_api.hierarchy(options['name_expenses']),
]
}|tojson
}}</script></svelte-component>

<div class="row">
Expand Down
16 changes: 9 additions & 7 deletions src/fava/templates/trial_balance.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{% import '_tree_table.html' as tree_table with context %}

<svelte-component type="charts"><script type="application/json">{{
[
chart_api.hierarchy(ledger.options['name_expenses']),
chart_api.hierarchy(ledger.options['name_income']),
chart_api.hierarchy(ledger.options['name_assets']),
chart_api.hierarchy(ledger.options['name_liabilities']),
chart_api.hierarchy(ledger.options['name_equity']),
]|tojson
{
'data': [
chart_api.hierarchy(ledger.options['name_expenses']),
chart_api.hierarchy(ledger.options['name_income']),
chart_api.hierarchy(ledger.options['name_assets']),
chart_api.hierarchy(ledger.options['name_liabilities']),
chart_api.hierarchy(ledger.options['name_equity']),
]
}|tojson
}}</script></svelte-component>

{{ tree_table.tree(g.filtered.root_tree.get('')) }}