Add caching functions and enhance item retrieval in templates; implement dynamic field selection for list items

This commit is contained in:
Yaro Kasear 2025-08-20 14:10:09 -05:00
parent 7e24aa0585
commit 09cfcee8b3
5 changed files with 110 additions and 7 deletions

View file

@ -7,13 +7,16 @@ if TYPE_CHECKING:
from .work_log import WorkLog
from .rooms import Room
from .image import Image
from .users import User
from sqlalchemy import Boolean, ForeignKey, Identity, Index, Integer, Unicode, DateTime, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
import datetime
from . import db
from .brands import Brand
from .image import ImageAttachable
from .users import User
class Inventory(db.Model, ImageAttachable):
__tablename__ = 'inventory'
@ -131,3 +134,17 @@ class Inventory(db.Model, ImageAttachable):
def attach_image(self, image: Image) -> None:
self.image = image
@staticmethod
def ui_search(stmt, text: str):
t = f"%{text}%"
return stmt.where(
Inventory.name.ilike(t) |
Inventory.serial.ilike(t) |
Inventory.model.ilike(t) |
Inventory.notes.ilike(t) |
Inventory.barcode.ilike(t) |
Inventory.owner.has(User.first_name.ilike(t)) |
Inventory.owner.has(User.last_name.ilike(t)) |
Inventory.brand.has(Brand.name.ilike(t))
)

View file

@ -1,5 +1,57 @@
from flask import Blueprint
from flask import Blueprint, g
main = Blueprint('main', __name__)
from . import inventory, user, worklog, settings, index, search, hooks
from .. import db
def _cell_cache():
if not hasattr(g, '_cell_cache'):
g._cell_cache = {}
return g._cell_cache
def _rows_cache():
if not hasattr(g, '_rows_cache'):
g._rows_cache = {}
return g._rows_cache
@main.app_template_global()
def cell(model_name: str, id_: int, field: str, default: str = ""):
from ..ui.blueprint import get_model_class, call
from ..ui.defaults import default_value
key = (model_name, int(id_), field)
cache = _cell_cache()
if key in cache:
return cache[key]
try:
Model = get_model_class(model_name)
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:
val = default
if val is None:
val = default
cache[key] = val
return val
@main.app_template_global()
def cells(model_name: str, id_: int, *fields: str):
from ..ui.blueprint import get_model_class, call
from ..ui.defaults import default_values
fields = [f for f in fields if f]
key = (model_name, int(id_), tuple(fields))
cache = _cell_cache()
if key in cache:
return cache[key]
try:
Model = get_model_class(model_name)
data = call(Model, 'ui_values', db.session, id_=int(id_), fields=fields)
if data is None:
data = default_values(db.session, Model, id_=int(id_), fields=fields)
except Exception:
data = {f: None for f in fields}
cache[key] = data
return data

View file

@ -11,4 +11,7 @@
id = 'dropdown',
refresh_url=url_for('ui.list_items', model_name='user')
) }}
{% set vals = cells('user', 8, 'first_name', 'last_name') %}
{{ vals['first_name'] }} {{ vals['last_name'] }}
{% endblock %}

View file

@ -47,6 +47,9 @@ def call(Model: type, name: str, *args: Any, **kwargs: Any) -> Any:
def list_items(model_name):
Model = get_model_class(model_name)
text = (request.args.get("q") or "").strip() or None
fields_raw = (request.args.get("fields") or "").strip()
fields = [f.strip() for f in fields_raw.split(",") if f.strip()]
fields.extend(request.args.getlist("field"))
limit_param = request.args.get("limit")
# 0 / -1 / blank => unlimited (pass 0)
@ -91,6 +94,19 @@ def list_items(model_name):
except TypeError:
rows = [rows_any]
if fields:
items = []
for r in rows:
row = {"id": r.id}
for f in fields:
if '.' in f:
rel, attr = f.split('.', 1)
rel_obj = getattr(r, rel, None)
row[f] = getattr(rel_obj, attr, None) if rel_obj else None
else:
row[f] = getattr(r, f, None)
items.append(row)
else:
items = [
(call(Model, "ui_serialize", r, view=view) or default_serialize(Model, r, view=view))
for r in rows

View file

@ -1,11 +1,21 @@
from sqlalchemy import select, asc as sa_asc, desc as sa_desc
from sqlalchemy import select, asc as sa_asc, desc as sa_desc, or_
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import aliased
from sqlalchemy.sql import Select
from sqlalchemy.sql.sqltypes import String, Unicode, Text
from typing import Any, Optional, cast, Iterable
PREFERRED_LABELS = ("identifier", "name", "first_name", "last_name", "description")
def _columns_for_text_search(Model):
mapper = inspect(Model)
cols = []
for c in mapper.columns:
if isinstance(c.type, (String, Unicode, Text)):
cols.append(getattr(Model, c.key))
return cols
def _mapped_column(Model, attr):
"""Return the mapped column attr on the class (InstrumentedAttribute) or None"""
mapper = inspect(Model)
@ -51,6 +61,11 @@ def default_query(
ui_search = getattr(Model, "ui_search", None)
if callable(ui_search) and text:
stmt = cast(Select[Any], ui_search(stmt, text))
elif text:
t = f"%{text}%"
text_cols = _columns_for_text_search(Model)
if text_cols:
stmt = stmt.where(or_(*(col.ilike(t) for col in text_cols)))
if sort:
ui_sort = getattr(Model, "ui_sort", None)