From 811b534b899df7e9185906a3df97c3f4dbcc54d5 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Thu, 25 Sep 2025 15:24:50 -0500 Subject: [PATCH] Getting one to many working... attempt 1. --- crudkit/core/service.py | 65 +--------------------------- crudkit/ui/fragments.py | 14 +++--- inventory/models/work_log.py | 2 +- inventory/routes/entry.py | 23 ++++++++++ inventory/templates/update_list.html | 18 ++++++-- 5 files changed, 48 insertions(+), 74 deletions(-) diff --git a/crudkit/core/service.py b/crudkit/core/service.py index 5732802..653d1f9 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -5,7 +5,7 @@ from flask import current_app from typing import Any, Callable, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast from sqlalchemy import and_, func, inspect, or_, text from sqlalchemy.engine import Engine, Connection -from sqlalchemy.orm import Load, Session, with_polymorphic, Mapper, contains_eager, selectinload +from sqlalchemy.orm import Load, Session, with_polymorphic, Mapper, contains_eager from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.sql import operators from sqlalchemy.sql.elements import UnaryExpression, ColumnElement @@ -80,68 +80,6 @@ def _dedupe_order_by(order_by): out.append(ob) return out -def _hops_from_sort(params: dict | None) -> set[str]: - """Extract first-hop relationship names from a sort spec like 'owner.first_name,-brand.name'.""" - if not params: - return set() - raw = params.get("sort") - tokens: list[str] = [] - if isinstance(raw, str): - tokens = [t.strip() for t in raw.split(",") if t.strip()] - elif isinstance(raw, (list, tuple)): - for item in raw: - if isinstance(item, str): - tokens.extend([t.strip() for t in item.split(",") if t.strip()]) - hops: set[str] = set() - for tok in tokens: - tok = tok.lstrip("+-") - if "." in tok: - hops.add(tok.split(".", 1)[0]) - return hops - -def _belongs_to_alias(col: Any, alias: Any) -> bool: - # Try to detect if a column/expression ultimately comes from this alias. - # Works for most ORM columns; complex expressions may need more. - t = getattr(col, "table", None) - selectable = getattr(alias, "selectable", None) - return t is not None and selectable is not None and t is selectable - -def _paths_needed_for_sql(order_by: Iterable[Any], filters: Iterable[Any], join_paths: tuple) -> set[str]: - hops: set[str] = set() - paths: set[tuple[str, ...]] = set() - # Sort columns - for ob in order_by or []: - col = getattr(ob, "element", ob) # unwrap UnaryExpression - for _path, rel_attr, target_alias in join_paths: - if _belongs_to_alias(col, target_alias): - hops.add(rel_attr.key) - # Filter columns (best-effort) - # Walk simple binary expressions - def _extract_cols(expr: Any) -> Iterable[Any]: - if isinstance(expr, ColumnElement): - yield expr - for ch in getattr(expr, "get_children", lambda: [])(): - yield from _extract_cols(ch) - elif hasattr(expr, "clauses"): - for ch in expr.clauses: - yield from _extract_cols(ch) - - for flt in filters or []: - for col in _extract_cols(flt): - for _path, rel_attr, target_alias in join_paths: - if _belongs_to_alias(col, target_alias): - hops.add(rel_attr.key) - return hops - -def _paths_from_fields(req_fields: list[str]) -> set[str]: - out: set[str] = set() - for f in req_fields: - if "." in f: - parent = f.split(".", 1)[0] - if parent: - out.add(parent) - return out - def _is_truthy(val): return str(val).lower() in ('1', 'true', 'yes', 'on') @@ -492,6 +430,7 @@ class CRUDService(Generic[T]): if params: root_fields, rel_field_names, root_field_names = spec.parse_fields() spec.parse_includes() + join_paths = tuple(spec.get_join_paths()) # Root-column projection (load_only) diff --git a/crudkit/ui/fragments.py b/crudkit/ui/fragments.py index 6c62c45..e60bd1a 100644 --- a/crudkit/ui/fragments.py +++ b/crudkit/ui/fragments.py @@ -1153,15 +1153,15 @@ def render_form( field["wrap"] = _sanitize_attrs(field["wrap"]) fields.append(field) - if submit_attrs: + if submit_attrs: submit_attrs = _sanitize_attrs(submit_attrs) - common_ctx = {"values": values_map, "instance": instance, "model_cls": model_cls, "session": session} - for f in fields: - if f.get("type") == "template": - base = dict(common_ctx) - base.update(f.get("template_ctx") or {}) - f["template_ctx"] = base + common_ctx = {"values": values_map, "instance": instance, "model_cls": model_cls, "session": session} + for f in fields: + if f.get("type") == "template": + base = dict(common_ctx) + base.update(f.get("template_ctx") or {}) + f["template_ctx"] = base for f in fields: # existing FK label resolution diff --git a/inventory/models/work_log.py b/inventory/models/work_log.py index 440ed11..7368472 100644 --- a/inventory/models/work_log.py +++ b/inventory/models/work_log.py @@ -17,7 +17,7 @@ class WorkLog(Base, CRUDMixin): contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') contact_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True) - updates: Mapped[List['WorkNote']] = relationship('WorkNote', back_populates='work_log', cascade='all, delete-orphan') + updates: Mapped[List['WorkNote']] = relationship('WorkNote', back_populates='work_log', cascade='all, delete-orphan', order_by='WorkNote.timestamp.desc()') work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs') work_item_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('inventory.id'), nullable=True, index=True) diff --git a/inventory/routes/entry.py b/inventory/routes/entry.py index 95f270c..986a44f 100644 --- a/inventory/routes/entry.py +++ b/inventory/routes/entry.py @@ -125,6 +125,24 @@ def init_entry_routes(app): # Use the scoped_session proxy so teardown .remove() cleans it up ScopedSession = current_app.extensions["crudkit"]["Session"] + if model == "worklog": + updates_cls = type(obj).updates.property.mapper.class_ + updates_q = (ScopedSession.query(updates_cls) + .filter(updates_cls.work_log_id == obj.id, + updates_cls.is_deleted == False) + .order_by(updates_cls.timestamp.asc())) + all_updates = updates_q.all() + print(all_updates) + + for f in fields_spec: + if f.get("name") == "updates" and f.get("type") == "template": + ctx = dict(f.get("template_ctx") or {}) + ctx["updates"] = all_updates + f["template_ctx"] = ctx + break + + print(fields_spec) + form = render_form( cls, obj.as_dict(), @@ -134,6 +152,11 @@ def init_entry_routes(app): layout=layout, submit_attrs={"class": "btn btn-primary mt-3"}, ) + # sanity log + u = getattr(obj, "updates", None) + print("WORKLOG UPDATES loaded? ", + "None" if u is None else f"len={len(list(u))} ids={[n.id for n in list(u)]}") + return render_template("entry.html", form=form) app.register_blueprint(bp_entry) diff --git a/inventory/templates/update_list.html b/inventory/templates/update_list.html index 29fded1..aff8fed 100644 --- a/inventory/templates/update_list.html +++ b/inventory/templates/update_list.html @@ -1,3 +1,15 @@ -
- UPDATES NOT IMPLEMENTED YET -
\ No newline at end of file +{% set items = (field.template_ctx.instance.updates or []) %} + \ No newline at end of file