Getting one to many working... attempt 1.
This commit is contained in:
parent
ea8e8a9df7
commit
811b534b89
5 changed files with 48 additions and 74 deletions
|
|
@ -5,7 +5,7 @@ from flask import current_app
|
||||||
from typing import Any, Callable, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast
|
from typing import Any, Callable, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast
|
||||||
from sqlalchemy import and_, func, inspect, or_, text
|
from sqlalchemy import and_, func, inspect, or_, text
|
||||||
from sqlalchemy.engine import Engine, Connection
|
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.orm.attributes import InstrumentedAttribute
|
||||||
from sqlalchemy.sql import operators
|
from sqlalchemy.sql import operators
|
||||||
from sqlalchemy.sql.elements import UnaryExpression, ColumnElement
|
from sqlalchemy.sql.elements import UnaryExpression, ColumnElement
|
||||||
|
|
@ -80,68 +80,6 @@ def _dedupe_order_by(order_by):
|
||||||
out.append(ob)
|
out.append(ob)
|
||||||
return out
|
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):
|
def _is_truthy(val):
|
||||||
return str(val).lower() in ('1', 'true', 'yes', 'on')
|
return str(val).lower() in ('1', 'true', 'yes', 'on')
|
||||||
|
|
||||||
|
|
@ -492,6 +430,7 @@ class CRUDService(Generic[T]):
|
||||||
if params:
|
if params:
|
||||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||||
spec.parse_includes()
|
spec.parse_includes()
|
||||||
|
|
||||||
join_paths = tuple(spec.get_join_paths())
|
join_paths = tuple(spec.get_join_paths())
|
||||||
|
|
||||||
# Root-column projection (load_only)
|
# Root-column projection (load_only)
|
||||||
|
|
|
||||||
|
|
@ -1153,15 +1153,15 @@ def render_form(
|
||||||
field["wrap"] = _sanitize_attrs(field["wrap"])
|
field["wrap"] = _sanitize_attrs(field["wrap"])
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
|
|
||||||
if submit_attrs:
|
if submit_attrs:
|
||||||
submit_attrs = _sanitize_attrs(submit_attrs)
|
submit_attrs = _sanitize_attrs(submit_attrs)
|
||||||
|
|
||||||
common_ctx = {"values": values_map, "instance": instance, "model_cls": model_cls, "session": session}
|
common_ctx = {"values": values_map, "instance": instance, "model_cls": model_cls, "session": session}
|
||||||
for f in fields:
|
for f in fields:
|
||||||
if f.get("type") == "template":
|
if f.get("type") == "template":
|
||||||
base = dict(common_ctx)
|
base = dict(common_ctx)
|
||||||
base.update(f.get("template_ctx") or {})
|
base.update(f.get("template_ctx") or {})
|
||||||
f["template_ctx"] = base
|
f["template_ctx"] = base
|
||||||
|
|
||||||
for f in fields:
|
for f in fields:
|
||||||
# existing FK label resolution
|
# existing FK label resolution
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class WorkLog(Base, CRUDMixin):
|
||||||
contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs')
|
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)
|
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: 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)
|
work_item_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('inventory.id'), nullable=True, index=True)
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,24 @@ def init_entry_routes(app):
|
||||||
# Use the scoped_session proxy so teardown .remove() cleans it up
|
# Use the scoped_session proxy so teardown .remove() cleans it up
|
||||||
ScopedSession = current_app.extensions["crudkit"]["Session"]
|
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(
|
form = render_form(
|
||||||
cls,
|
cls,
|
||||||
obj.as_dict(),
|
obj.as_dict(),
|
||||||
|
|
@ -134,6 +152,11 @@ def init_entry_routes(app):
|
||||||
layout=layout,
|
layout=layout,
|
||||||
submit_attrs={"class": "btn btn-primary mt-3"},
|
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)
|
return render_template("entry.html", form=form)
|
||||||
|
|
||||||
app.register_blueprint(bp_entry)
|
app.register_blueprint(bp_entry)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
<div class="col mt-3">
|
{% set items = (field.template_ctx.instance.updates or []) %}
|
||||||
UPDATES NOT IMPLEMENTED YET
|
<ul class="list-group mt-3">
|
||||||
</div>
|
{% for n in items %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="me-3" style="white-space: pre-wrap;">{{ n.content }}</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item text-muted">No updates yet.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue