Implement label rendering functionality and integrate it into worklog template; add label.js for dynamic label updates
This commit is contained in:
parent
a61b56ddf2
commit
1e05ad16ce
6 changed files with 78 additions and 3 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
29
inventory/static/js/label.js
Normal file
29
inventory/static/js/label.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
8
inventory/templates/fragments/_label_fragment.html
Normal file
8
inventory/templates/fragments/_label_fragment.html
Normal 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 %}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue