Implement label rendering functionality and integrate it into worklog template; add label.js for dynamic label updates

This commit is contained in:
Yaro Kasear 2025-08-21 09:27:38 -05:00
parent a61b56ddf2
commit 1e05ad16ce
6 changed files with 78 additions and 3 deletions

View file

@ -1,15 +1,28 @@
import logging
from flask import Blueprint, g from flask import Blueprint, g
from sqlalchemy.sql import Select
from sqlalchemy.engine import ScalarResult from sqlalchemy.engine import ScalarResult
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import Select
from typing import Iterable, Any, cast from typing import Iterable, Any, cast
main = Blueprint('main', __name__) main = Blueprint('main', __name__)
log = logging.getLogger(__name__)
from . import inventory, user, worklog, settings, index, search, hooks from . import inventory, user, worklog, settings, index, search, hooks
from .. import db from .. import db
from ..ui.blueprint import get_model_class, call from ..ui.blueprint import get_model_class, call
from ..ui.defaults import default_query 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(): def _cell_cache():
if not hasattr(g, '_cell_cache'): if not hasattr(g, '_cell_cache'):
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]: def _project_row(obj: Any, fields: Iterable[str]) -> dict[str, Any]:
out = {"id": obj.id} out = {"id": obj.id}
Model = type(obj)
allow = getattr(Model, "ui_value_allow", None)
for f in fields: for f in fields:
if allow and f not in allow:
out[f] = None
continue
if "." in f: if "." in f:
rel, attr = f.split(".", 1) rel, attr = f.split(".", 1)
relobj = getattr(obj, rel, None) 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) val = call(Model, 'ui_value', db.session, id_=id_, field=field)
if val is None: if val is None:
val = default_value(db.session, Model, id_=id_, field=field) 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 val = default
if val is None: if val is None:
val = default val = default
@ -106,6 +125,16 @@ def table(model_name: str, fields: Iterable[str], *,
Model = get_model_class(model_name) Model = get_model_class(model_name)
qkwargs = dict(text=(q or None), limit=int(limit), offset=int(offset), qkwargs = dict(text=(q or None), limit=int(limit), offset=int(offset),
sort=(sort or None), direction=(direction or "asc").lower()) 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) rows_any: Any = call(Model, "ui_query", db.session, **qkwargs)
if rows_any is None: if rows_any is None:

View file

@ -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;
}
}
};
}

View file

@ -0,0 +1,8 @@
{% macro render_label(id, text=none, refresh_url=none, field_name=none, record_id=none) %}
<span id="label-{{ id }}" x-data='Label({
id: "{{ id }}",
refreshUrl: {{ refresh_url|tojson if refresh_url else "null" }},
fieldName: {{ field_name|tojson if field_name else "null" }},
recordId: {{ record_id|tojson if record_id else "null" }}
})' x-ref="label" hx-preserve>{{ text if text else '' }}</span>
{% endmacro %}

View file

@ -5,6 +5,7 @@
{% import "fragments/_editor_fragment.html" as editor %} {% import "fragments/_editor_fragment.html" as editor %}
{% import "fragments/_icon_fragment.html" as icons %} {% import "fragments/_icon_fragment.html" as icons %}
{% import "fragments/_image_fragment.html" as images %} {% import "fragments/_image_fragment.html" as images %}
{% import "fragments/_label_fragment.html" as labels %}
{% import "fragments/_link_fragment.html" as links %} {% import "fragments/_link_fragment.html" as links %}
{% import "fragments/_table_fragment.html" as tables %} {% import "fragments/_table_fragment.html" as tables %}
{% import "fragments/_toolbar_fragment.html" as toolbars %} {% import "fragments/_toolbar_fragment.html" as toolbars %}
@ -70,6 +71,7 @@
<script src="{{ url_for('static', filename='js/dropdown.js') }}"></script> <script src="{{ url_for('static', filename='js/dropdown.js') }}"></script>
<script src="{{ url_for('static', filename='js/editor.js') }}" defer></script> <script src="{{ url_for('static', filename='js/editor.js') }}" defer></script>
<script src="{{ url_for('static', filename='js/image.js') }}"></script> <script src="{{ url_for('static', filename='js/image.js') }}"></script>
<script src="{{ url_for('static', filename='js/label.js') }}"></script>
<script src="{{ url_for('static', filename='js/toast.js') }}" defer></script> <script src="{{ url_for('static', filename='js/toast.js') }}" defer></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"
integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO"

View file

@ -14,4 +14,6 @@
{% set vals = cells('user', 8, 'first_name', 'last_name') %} {% set vals = cells('user', 8, 'first_name', 'last_name') %}
{{ vals['first_name'] }} {{ vals['last_name'] }} {{ vals['first_name'] }} {{ vals['last_name'] }}
{{ table('inventory', ['name', 'barcode', 'serial'], limit=0, q='BH0298') | tojson }}
{% endblock %} {% endblock %}

View file

@ -231,7 +231,12 @@
{% for update in log.updates %} {% for update in log.updates %}
{{ editor.render_editor( {{ editor.render_editor(
id = update.id, id = update.id,
title = update.timestamp.strftime('%Y-%m-%d %H:%M:%S'), title = labels.render_label(
id = update.id,
refresh_url = url_for('ui.get_value', model_name='work_note'),
field_name = 'timestamp',
record_id = update.id
),
mode = 'view', mode = 'view',
enabled = not log.complete, enabled = not log.complete,
refresh_url = url_for('ui.get_value', model_name='work_note'), refresh_url = url_for('ui.get_value', model_name='work_note'),