diff --git a/inventory/routes/__init__.py b/inventory/routes/__init__.py index 601df49..da18176 100644 --- a/inventory/routes/__init__.py +++ b/inventory/routes/__init__.py @@ -1,15 +1,28 @@ +import logging + from flask import Blueprint, g -from sqlalchemy.sql import Select from sqlalchemy.engine import ScalarResult +from sqlalchemy.orm import joinedload +from sqlalchemy.sql import Select from typing import Iterable, Any, cast main = Blueprint('main', __name__) +log = logging.getLogger(__name__) from . import inventory, user, worklog, settings, index, search, hooks from .. import db from ..ui.blueprint import get_model_class, call from ..ui.defaults import default_query +def _eager_from_fields(Model, fields: Iterable[str]): + rels = {f.split(".", 1)[0] for f in fields if "." in f} + opts = [] + for r in rels: + rel_attr = getattr(Model, r, None) + if getattr(rel_attr, "property", None) is not None: + opts.append(joinedload(rel_attr)) + return opts + def _cell_cache(): if not hasattr(g, '_cell_cache'): g._cell_cache = {} @@ -22,7 +35,12 @@ def _tmpl_cache(name: str): def _project_row(obj: Any, fields: Iterable[str]) -> dict[str, Any]: out = {"id": obj.id} + Model = type(obj) + allow = getattr(Model, "ui_value_allow", None) for f in fields: + if allow and f not in allow: + out[f] = None + continue if "." in f: rel, attr = f.split(".", 1) relobj = getattr(obj, rel, None) @@ -45,7 +63,8 @@ def cell(model_name: str, id_: int, field: str, default: str = ""): val = call(Model, 'ui_value', db.session, id_=id_, field=field) if val is None: val = default_value(db.session, Model, id_=id_, field=field) - except Exception: + except Exception as e: + log.warning(f"cell() error for {model_name} {id_} {field}: {e}") val = default if val is None: val = default @@ -106,6 +125,16 @@ def table(model_name: str, fields: Iterable[str], *, Model = get_model_class(model_name) qkwargs = dict(text=(q or None), limit=int(limit), offset=int(offset), sort=(sort or None), direction=(direction or "asc").lower()) + extra_opts = _eager_from_fields(Model, fields) + if extra_opts: + if isinstance(rows_any, Select): + rows_any = rows_any.options(*extra_opts) + elif rows_any is None: + original = getattr(Model, 'ui_eagerload', ()) + def dyn_opts(): + base = original() if callable(original) else original + return tuple(base) + tuple(extra_opts) + setattr(Model, 'ui_eagerload', dyn_opts) rows_any: Any = call(Model, "ui_query", db.session, **qkwargs) if rows_any is None: diff --git a/inventory/static/js/label.js b/inventory/static/js/label.js new file mode 100644 index 0000000..be3649d --- /dev/null +++ b/inventory/static/js/label.js @@ -0,0 +1,29 @@ +function Label(cfg) { + return { + id: cfg.id, + refreshUrl: cfg.refreshUrl, + fieldName: cfg.fieldName, + recordId: cfg.recordId, + + init() { + if (this.refreshUrl) this.refresh(); + }, + + buildRefreshUrl() { + if (!this.refreshUrl) return null; + const u = new URL(this.refreshUrl, window.location.origin); + u.search = new URLSearchParams({ field: this.fieldName, id: this.recordId }).toString(); + return u.toString(); + }, + + async refresh() { + const url = this.buildRefreshUrl(); + if (!url) return; + const res = await fetch(url, { headers: { 'HX-Request': 'true' } }); + const text = await res.text(); + if (this.$refs.label) { + this.$refs.label.innerHTML = text; + } + } + }; +} \ No newline at end of file diff --git a/inventory/templates/fragments/_label_fragment.html b/inventory/templates/fragments/_label_fragment.html new file mode 100644 index 0000000..699969e --- /dev/null +++ b/inventory/templates/fragments/_label_fragment.html @@ -0,0 +1,8 @@ +{% macro render_label(id, text=none, refresh_url=none, field_name=none, record_id=none) %} +{{ text if text else '' }} +{% endmacro %} \ No newline at end of file diff --git a/inventory/templates/layout.html b/inventory/templates/layout.html index 67feb6b..16f01d7 100644 --- a/inventory/templates/layout.html +++ b/inventory/templates/layout.html @@ -5,6 +5,7 @@ {% import "fragments/_editor_fragment.html" as editor %} {% import "fragments/_icon_fragment.html" as icons %} {% import "fragments/_image_fragment.html" as images %} +{% import "fragments/_label_fragment.html" as labels %} {% import "fragments/_link_fragment.html" as links %} {% import "fragments/_table_fragment.html" as tables %} {% import "fragments/_toolbar_fragment.html" as toolbars %} @@ -70,6 +71,7 @@ +