Add filtering for dropdowns.
This commit is contained in:
parent
5234cbdd61
commit
51520da5af
4 changed files with 87 additions and 8 deletions
|
|
@ -415,6 +415,8 @@ class CRUDService(Generic[T]):
|
|||
opt = opt.load_only(*cols)
|
||||
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,
|
||||
# make sure we actually JOIN it (outer) so filters don’t create a cartesian product.
|
||||
if plan.filter_tables:
|
||||
|
|
@ -422,14 +424,19 @@ class CRUDService(Generic[T]):
|
|||
for rel in mapper.relationships:
|
||||
if rel.uselist:
|
||||
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:
|
||||
continue
|
||||
if target_tbl in plan.filter_tables:
|
||||
if rel.key in joined_rel_keys:
|
||||
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)
|
||||
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
info = []
|
||||
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 typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import crudkit
|
||||
|
||||
_ALLOWED_ATTRS = {
|
||||
"class", "placeholder", "autocomplete", "inputmode", "pattern",
|
||||
"min", "max", "step", "maxlength", "minlength",
|
||||
|
|
@ -107,6 +109,53 @@ def register_template_globals(app=None):
|
|||
app.add_template_global(fn, 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):
|
||||
req = getattr(model_cls, "__crudkit_field_requires__", {}) or {}
|
||||
out = set(fields)
|
||||
|
|
@ -647,10 +696,12 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default):
|
|||
if "label_deps" in spec:
|
||||
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 field["type"] is None:
|
||||
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_
|
||||
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 "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
|
||||
|
||||
col = mapper.columns.get(name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue