Downstream patch.
This commit is contained in:
parent
ec82ca2394
commit
87f7108f2d
2 changed files with 66 additions and 4 deletions
|
|
@ -415,6 +415,8 @@ class CRUDService(Generic[T]):
|
||||||
opt = opt.load_only(*cols)
|
opt = opt.load_only(*cols)
|
||||||
query = query.options(opt)
|
query = query.options(opt)
|
||||||
|
|
||||||
|
# inside CRUDService._apply_firsthop_strategies
|
||||||
|
# ...
|
||||||
# NEW: if a first-hop to-one relationship’s target table is present in filter expressions,
|
# NEW: if a first-hop to-one relationship’s target table is present in filter expressions,
|
||||||
# make sure we actually JOIN it (outer) so filters don’t create a cartesian product.
|
# make sure we actually JOIN it (outer) so filters don’t create a cartesian product.
|
||||||
if plan.filter_tables:
|
if plan.filter_tables:
|
||||||
|
|
@ -422,14 +424,19 @@ class CRUDService(Generic[T]):
|
||||||
for rel in mapper.relationships:
|
for rel in mapper.relationships:
|
||||||
if rel.uselist:
|
if rel.uselist:
|
||||||
continue # only first-hop to-one here
|
continue # only first-hop to-one here
|
||||||
target_tbl = getattr(rel.mapper.class_, "__table__", None)
|
target_cls = rel.mapper.class_
|
||||||
|
target_tbl = getattr(target_cls, "__table__", None)
|
||||||
if target_tbl is None:
|
if target_tbl is None:
|
||||||
continue
|
continue
|
||||||
if target_tbl in plan.filter_tables:
|
if target_tbl in plan.filter_tables:
|
||||||
if rel.key in joined_rel_keys:
|
if rel.key in joined_rel_keys:
|
||||||
continue # already joined via join_paths
|
continue # already joined via join_paths
|
||||||
query = query.join(getattr(root_alias, rel.key), isouter=True)
|
|
||||||
|
# alias when joining same-entity relationships (User->User supervisor)
|
||||||
|
ta = aliased(target_cls) if target_cls is self.model else target_cls
|
||||||
|
query = query.join(getattr(root_alias, rel.key).of_type(ta), isouter=True)
|
||||||
joined_rel_keys.add(rel.key)
|
joined_rel_keys.add(rel.key)
|
||||||
|
|
||||||
if log.isEnabledFor(logging.DEBUG):
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
info = []
|
info = []
|
||||||
for base_alias, rel_attr, target_alias in plan.join_paths:
|
for base_alias, rel_attr, target_alias in plan.join_paths:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ from sqlalchemy.orm.base import NO_VALUE
|
||||||
from sqlalchemy.orm.properties import ColumnProperty, RelationshipProperty
|
from sqlalchemy.orm.properties import ColumnProperty, RelationshipProperty
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import crudkit
|
||||||
|
|
||||||
_ALLOWED_ATTRS = {
|
_ALLOWED_ATTRS = {
|
||||||
"class", "placeholder", "autocomplete", "inputmode", "pattern",
|
"class", "placeholder", "autocomplete", "inputmode", "pattern",
|
||||||
"min", "max", "step", "maxlength", "minlength",
|
"min", "max", "step", "maxlength", "minlength",
|
||||||
|
|
@ -107,6 +109,53 @@ def register_template_globals(app=None):
|
||||||
app.add_template_global(fn, name)
|
app.add_template_global(fn, name)
|
||||||
installed.add(name)
|
installed.add(name)
|
||||||
|
|
||||||
|
def _fields_for_label_params(label_spec, related_model):
|
||||||
|
"""
|
||||||
|
Build a 'fields' list suitable for CRUDService.list() so labels render
|
||||||
|
without triggering lazy loads. Always includes 'id'.
|
||||||
|
"""
|
||||||
|
simple_cols, rel_paths = _extract_label_requirements(label_spec, related_model)
|
||||||
|
fields = set(["id"])
|
||||||
|
for c in simple_cols:
|
||||||
|
fields.add(c)
|
||||||
|
for rel_name, col_name in rel_paths:
|
||||||
|
if col_name == "__all__":
|
||||||
|
# just ensure relationship object is present; ask for rel.id
|
||||||
|
fields.add(f"{rel_name}.id")
|
||||||
|
else:
|
||||||
|
fields.add(f"{rel_name}.{col_name}")
|
||||||
|
return list(fields)
|
||||||
|
|
||||||
|
def _fk_options_via_service(related_model, label_spec, *, options_params: dict | None = None):
|
||||||
|
svc = crudkit.crud.get_service(related_model)
|
||||||
|
|
||||||
|
# default to unlimited results for dropdowns
|
||||||
|
params = {"limit": 0}
|
||||||
|
if options_params:
|
||||||
|
params.update(options_params) # caller can override limit if needed
|
||||||
|
|
||||||
|
# ensure fields needed to render the label are present (avoid lazy loads)
|
||||||
|
fields = _fields_for_label_params(label_spec, related_model)
|
||||||
|
if fields:
|
||||||
|
existing = params.get("fields")
|
||||||
|
if isinstance(existing, str):
|
||||||
|
existing = [s.strip() for s in existing.split(",") if s.strip()]
|
||||||
|
if isinstance(existing, (list, tuple)):
|
||||||
|
params["fields"] = list(dict.fromkeys(list(existing) + fields))
|
||||||
|
else:
|
||||||
|
params["fields"] = fields
|
||||||
|
|
||||||
|
# only set a default sort if caller didn’t supply one
|
||||||
|
if "sort" not in params:
|
||||||
|
simple_cols, _ = _extract_label_requirements(label_spec, related_model)
|
||||||
|
params["sort"] = (simple_cols[0] if simple_cols else "id")
|
||||||
|
|
||||||
|
rows = svc.list(params)
|
||||||
|
return [
|
||||||
|
{"value": str(r.id), "label": _label_from_obj(r, label_spec)}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
def expand_projection(model_cls, fields):
|
def expand_projection(model_cls, fields):
|
||||||
req = getattr(model_cls, "__crudkit_field_requires__", {}) or {}
|
req = getattr(model_cls, "__crudkit_field_requires__", {}) or {}
|
||||||
out = set(fields)
|
out = set(fields)
|
||||||
|
|
@ -647,10 +696,12 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default):
|
||||||
if "label_deps" in spec:
|
if "label_deps" in spec:
|
||||||
field["label_deps"] = spec["label_deps"]
|
field["label_deps"] = spec["label_deps"]
|
||||||
|
|
||||||
|
opts_params = spec.get("options_params") or spec.get("options_filter") or spec.get("options_where")
|
||||||
|
|
||||||
if rel_prop:
|
if rel_prop:
|
||||||
if field["type"] is None:
|
if field["type"] is None:
|
||||||
field["type"] = "select"
|
field["type"] = "select"
|
||||||
if field["type"] == "select" and field.get("options") is None and session is not None:
|
if field["type"] == "select" and field.get("options") is None:
|
||||||
related_model = rel_prop.mapper.class_
|
related_model = rel_prop.mapper.class_
|
||||||
label_spec = (
|
label_spec = (
|
||||||
spec.get("label_spec")
|
spec.get("label_spec")
|
||||||
|
|
@ -658,7 +709,11 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default):
|
||||||
or getattr(related_model, "__crud_label__", None)
|
or getattr(related_model, "__crud_label__", None)
|
||||||
or "id"
|
or "id"
|
||||||
)
|
)
|
||||||
field["options"] = _fk_options(session, related_model, label_spec)
|
field["options"] = _fk_options_via_service(
|
||||||
|
related_model,
|
||||||
|
label_spec,
|
||||||
|
options_params=opts_params
|
||||||
|
)
|
||||||
return field
|
return field
|
||||||
|
|
||||||
col = mapper.columns.get(name)
|
col = mapper.columns.get(name)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue