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)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ def _fields_for_model(model: str):
|
|||
"row": "name", "wrap": {"class": "col-3"}},
|
||||
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
||||
"label_spec": "{label}", "row": "details", "wrap": {"class": "col-3"},
|
||||
"attrs": {"class": "form-control"}, "link": {"endpoint": "entry.entry", "params": {"id": "{supervisor.id}", "model": "user"}}},
|
||||
"attrs": {"class": "form-control"}, "link": {"endpoint": "entry.entry", "params": {"id": "{supervisor.id}", "model": "user"}},
|
||||
"options_params": {"active__eq": True, "staff__eq": True}},
|
||||
{"name": "location", "label": "Room", "label_attrs": {"class": "form-label"},
|
||||
"label_spec": "{name} - {room_function.description}",
|
||||
"row": "details", "wrap": {"class": "col-3"}, "attrs": {"class": "form-control"}},
|
||||
|
|
|
|||
|
|
@ -30,7 +30,21 @@
|
|||
#}
|
||||
{% if options %}
|
||||
{% if value %}
|
||||
{% set sel_label = (options | selectattr('value', 'equalto', value) | first)['label'] %}
|
||||
{% set opts = options or [] %}
|
||||
{% set selected = opts | selectattr('value', 'equalto', value) | list %}
|
||||
{% if not selected %}
|
||||
{# try again with string coercion to handle int vs str mismatch #}
|
||||
{% set selected = opts | selectattr('value', 'equalto', value|string) | list %}
|
||||
{% endif %}
|
||||
{% set sel = selected[0] if selected else none %}
|
||||
|
||||
{% if sel %}
|
||||
{% set sel_label = sel['label'] %}
|
||||
{% elif value_label %}
|
||||
{% set sel_label = value_label %}
|
||||
{% else %}
|
||||
{% set sel_label = "-- Select --" %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set sel_label = "-- Select --" %}
|
||||
{% endif %}
|
||||
|
|
@ -41,11 +55,13 @@
|
|||
id="{{ field_name }}-filter" placeholder="Filter..." oninput="DropDown.utilities.filterList('{{ field_name }}')">
|
||||
{% for opt in options %}
|
||||
<li><a class="dropdown-item{% if opt.value|string == value|string %} active{% endif %}"
|
||||
data-value="{{ opt['value'] }}" onclick="DropDown.utilities.selectItem('{{ field_name }}', '{{ opt['value'] }}')" id="{{ field_name }}-{{ opt['value'] }}">{{ opt['label'] }}</a></li>
|
||||
data-value="{{ opt['value'] }}" onclick="DropDown.utilities.selectItem('{{ field_name }}', '{{ opt['value'] }}')"
|
||||
id="{{ field_name }}-{{ opt['value'] }}">{{ opt['label'] }}</a></li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-dark d-block w-100 text-start dropdown-toggle disabled inventory-dropdown" disabled>-- No selection available --</button>
|
||||
<button class="btn btn-outline-dark d-block w-100 text-start dropdown-toggle disabled inventory-dropdown" disabled>-- No
|
||||
selection available --</button>
|
||||
{% endif %}
|
||||
<input type="hidden" name="{{ field_name }}" id="{{ field_name }}" value="{{ value }}">
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue