From 09cfcee8b35abc216a3ec9416d750e022ba7e160 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 20 Aug 2025 14:10:09 -0500 Subject: [PATCH] Add caching functions and enhance item retrieval in templates; implement dynamic field selection for list items --- inventory/models/inventory.py | 17 +++++++++ inventory/routes/__init__.py | 56 +++++++++++++++++++++++++++-- inventory/templates/playground.html | 3 ++ inventory/ui/blueprint.py | 24 ++++++++++--- inventory/ui/defaults.py | 17 ++++++++- 5 files changed, 110 insertions(+), 7 deletions(-) diff --git a/inventory/models/inventory.py b/inventory/models/inventory.py index f190c0f..25c138b 100644 --- a/inventory/models/inventory.py +++ b/inventory/models/inventory.py @@ -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)) + ) diff --git a/inventory/routes/__init__.py b/inventory/routes/__init__.py index 903c5de..1833ee4 100644 --- a/inventory/routes/__init__.py +++ b/inventory/routes/__init__.py @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/inventory/templates/playground.html b/inventory/templates/playground.html index 19273c0..c7814a5 100644 --- a/inventory/templates/playground.html +++ b/inventory/templates/playground.html @@ -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 %} \ No newline at end of file diff --git a/inventory/ui/blueprint.py b/inventory/ui/blueprint.py index 9afe002..fa030cc 100644 --- a/inventory/ui/blueprint.py +++ b/inventory/ui/blueprint.py @@ -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,10 +94,23 @@ def list_items(model_name): except TypeError: rows = [rows_any] - items = [ - (call(Model, "ui_serialize", r, view=view) or default_serialize(Model, r, view=view)) - for r in rows - ] + 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 + ] want_option = (request.args.get("view") == "option") want_list = (request.args.get("view") == "list") diff --git a/inventory/ui/defaults.py b/inventory/ui/defaults.py index 0658ec6..62cdce5 100644 --- a/inventory/ui/defaults.py +++ b/inventory/ui/defaults.py @@ -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)