More field and form rendering logic.
This commit is contained in:
parent
2d837210c1
commit
6b56251d33
2 changed files with 128 additions and 38 deletions
|
|
@ -26,6 +26,24 @@ def get_env():
|
|||
loader=ChoiceLoader([app.jinja_loader, fallback_loader])
|
||||
)
|
||||
|
||||
def _get_loaded_attr(obj: Any, name: str) -> Any:
|
||||
"""
|
||||
Return obj.<name> only if it is already loaded.
|
||||
Never triggers a lazy load. Returns None if missing/unloaded.
|
||||
Works for both column and relationship attributes.
|
||||
"""
|
||||
try:
|
||||
st = inspect(obj)
|
||||
attr = st.attrs.get(name)
|
||||
if attr is not None:
|
||||
val = attr.loaded_value
|
||||
return None if val is NO_VALUE else val
|
||||
if name in st.dict:
|
||||
return st.dict.get(name)
|
||||
return getattr(obj, name, None)
|
||||
except Exception:
|
||||
return getattr(obj, name, None)
|
||||
|
||||
def _normalize_rows_layout(layout: Optional[List[dict]]) -> Dict[str, dict]:
|
||||
"""
|
||||
Create node dicts for each row and link parent->children.
|
||||
|
|
@ -173,6 +191,69 @@ def _sanitize_attrs(attrs: Any) -> dict[str, Any]:
|
|||
|
||||
return out
|
||||
|
||||
def _resolve_rel_obj(values: dict, instance, base: str):
|
||||
rel = None
|
||||
if isinstance(values, dict) and base in values:
|
||||
rel = values[base]
|
||||
if isinstance(rel, dict):
|
||||
class _DictObj:
|
||||
def __init__(self, d): self._d = d
|
||||
def __getattr__(self, k): return self._d.get(k)
|
||||
rel = _DictObj(rel)
|
||||
|
||||
if rel is None and instance is not None:
|
||||
try:
|
||||
st = inspect(instance)
|
||||
ra = st.attrs.get(base)
|
||||
if ra is not None and ra.loaded_value is not NO_VALUE:
|
||||
rel = ra.loaded_value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return rel
|
||||
|
||||
def _value_label_for_field(field: dict, mapper, values_map: dict, instance, session):
|
||||
"""
|
||||
If field targets a MANYTOONE (foo or foo_id), compute a human-readable label.
|
||||
No lazy loads. Optional single-row lean fetch if we only have the id.
|
||||
"""
|
||||
base, rel_prop = _rel_for_id_name(mapper, field["name"])
|
||||
if not rel_prop:
|
||||
return None
|
||||
|
||||
rid = _coerce_fk_value(values_map, instance, base)
|
||||
rel_obj = _resolve_rel_obj(values_map, instance, base)
|
||||
|
||||
label_spec = (
|
||||
field.get("label_spec")
|
||||
or getattr(rel_prop.mapper.class_, "__crud_label__", None)
|
||||
or "id"
|
||||
)
|
||||
|
||||
if rel_obj is not None and not _has_label_bits_loaded(rel_obj, label_spec) and session is not None and rid is not None:
|
||||
mdl = rel_prop.mapper.class_
|
||||
simple_cols, rel_paths = _extract_label_requirements(label_spec)
|
||||
q = session.query(mdl)
|
||||
cols = [getattr(mdl, "id")]
|
||||
for c in simple_cols:
|
||||
if hasattr(mdl, c):
|
||||
cols.append(getattr(mdl, c))
|
||||
if cols:
|
||||
q = q.options(load_only(*cols))
|
||||
for rel_name, col_name in rel_paths:
|
||||
try:
|
||||
t_rel = mdl.__mapper__.relationships[rel_name]
|
||||
t_cls = t_rel.mapper.class_
|
||||
col_attr = getattr(t_cls, col_name, None)
|
||||
opt = selectinload(getattr(mdl, rel_name))
|
||||
q = q.options(opt.load_only(col_attr) if col_attr is not None else opt)
|
||||
except Exception:
|
||||
q = q.options(selectinload(getattr(mdl, rel_name)))
|
||||
rel_obj = q.get(rid)
|
||||
if rel_obj is not None:
|
||||
return _label_from_obj(rel_obj, label_spec)
|
||||
return str(rid) if rid is not None else None
|
||||
|
||||
class _SafeObj:
|
||||
"""Attribute access that returns '' for missing/None instead of exploding."""
|
||||
__slots__ = ("_obj",)
|
||||
|
|
@ -181,10 +262,8 @@ class _SafeObj:
|
|||
def __getattr__(self, name):
|
||||
if self._obj is None:
|
||||
return ""
|
||||
val = getattr(self._obj, name, None)
|
||||
if val is None:
|
||||
return ""
|
||||
return _SafeObj(val)
|
||||
val = _get_loaded_attr(self._obj, name)
|
||||
return "" if val is None else _SafeObj(val)
|
||||
|
||||
def _coerce_fk_value(values: dict | None, instance: Any, base: str):
|
||||
"""
|
||||
|
|
@ -314,6 +393,7 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default):
|
|||
"template": spec.get("template"),
|
||||
"template_name": spec.get("template_name"),
|
||||
"template_ctx": spec.get("template_ctx"),
|
||||
"label_spec": spec.get("label_spec")
|
||||
}
|
||||
|
||||
if rel_prop:
|
||||
|
|
@ -387,41 +467,24 @@ def _extract_label_requirements(spec: Any) -> tuple[list[str], list[tuple[str, s
|
|||
|
||||
return simple_cols, rel_paths
|
||||
|
||||
def _attrs_from_label_spec(spec: Any) -> list[str]:
|
||||
"""
|
||||
Return a list of attribute names needed from the related model to compute the label.
|
||||
Only simple attribute names are returned; dotted paths return just the first segment.
|
||||
"""
|
||||
if spec is None:
|
||||
return []
|
||||
if callable(spec):
|
||||
return []
|
||||
if isinstance(spec, (list, tuple)):
|
||||
return [str(a).split(".", 1)[0] for a in spec]
|
||||
if isinstance(spec, str):
|
||||
if "{" in spec and "}" in spec:
|
||||
names = re.findall(r"{\s*([^}:\s]+)", spec)
|
||||
return [n.split(".", 1)[0] for n in names]
|
||||
return [spec.split(".", 1)[0]]
|
||||
return []
|
||||
|
||||
def _label_from_obj(obj: Any, spec: Any) -> str:
|
||||
if obj is None:
|
||||
return ""
|
||||
|
||||
if spec is None:
|
||||
for attr in ("label", "name", "title", "description"):
|
||||
if hasattr(obj, attr):
|
||||
val = getattr(obj, attr)
|
||||
if not callable(val) and val is not None:
|
||||
val = _get_loaded_attr(obj, attr)
|
||||
if val is not None:
|
||||
return str(val)
|
||||
if hasattr(obj, "id"):
|
||||
return str(getattr(obj, "id"))
|
||||
return object.__repr__(obj)
|
||||
vid = _get_loaded_attr(obj, "id")
|
||||
return str(vid) if vid is not None else object.__repr__(obj)
|
||||
|
||||
if isinstance(spec, (list, tuple)):
|
||||
parts = []
|
||||
for a in spec:
|
||||
cur = obj
|
||||
for part in str(a).split("."):
|
||||
cur = getattr(cur, part, None)
|
||||
cur = _get_loaded_attr(cur, part) if cur is not None else None
|
||||
if cur is None:
|
||||
break
|
||||
parts.append("" if cur is None else str(cur))
|
||||
|
|
@ -433,9 +496,7 @@ def _label_from_obj(obj: Any, spec: Any) -> str:
|
|||
for f in fields:
|
||||
root = f.split(".", 1)[0]
|
||||
if root not in data:
|
||||
val = getattr(obj, root, None)
|
||||
data[root] = _SafeObj(val)
|
||||
|
||||
data[root] = _SafeObj(_get_loaded_attr(obj, root))
|
||||
try:
|
||||
return spec.format(**data)
|
||||
except Exception:
|
||||
|
|
@ -443,7 +504,7 @@ def _label_from_obj(obj: Any, spec: Any) -> str:
|
|||
|
||||
cur = obj
|
||||
for part in str(spec).split("."):
|
||||
cur = getattr(cur, part, None)
|
||||
cur = _get_loaded_attr(cur, part) if cur is not None else None
|
||||
if cur is None:
|
||||
return ""
|
||||
return str(cur)
|
||||
|
|
@ -572,6 +633,28 @@ def _format_value(val: Any, fmt: Optional[str]) -> Any:
|
|||
return val
|
||||
return val
|
||||
|
||||
def _has_label_bits_loaded(obj: Any, label_spec: Any) -> bool:
|
||||
try:
|
||||
st = inspect(obj)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
simple_cols, rel_paths = _extract_label_requirements(label_spec)
|
||||
for c in simple_cols:
|
||||
if c not in st.dict:
|
||||
return False
|
||||
for rel, col in rel_paths:
|
||||
ra = st.attrs.get(rel)
|
||||
if ra is None or ra.loaded_value is NO_VALUE or ra.loaded_value is None:
|
||||
return False
|
||||
try:
|
||||
t_st = inspect(ra.loaded_value)
|
||||
if col not in t_st.dict:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _class_for(val: Any, classes: Optional[Dict[str, str]]) -> Optional[str]:
|
||||
if not classes:
|
||||
return None
|
||||
|
|
@ -642,6 +725,7 @@ def get_crudkit_template(env, name):
|
|||
|
||||
def render_field(field, value):
|
||||
env = get_env()
|
||||
print(field)
|
||||
|
||||
# 1) custom template field
|
||||
field_type = field.get('type', 'text')
|
||||
|
|
@ -668,6 +752,7 @@ def render_field(field, value):
|
|||
attrs=_sanitize_attrs(field.get('attrs') or {}),
|
||||
label_attrs=_sanitize_attrs(field.get('label_attrs') or {}),
|
||||
help=field.get('help'),
|
||||
value_label=field.get('value_label'),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -825,6 +910,11 @@ def render_form(
|
|||
base.update(f.get("template_ctx") or {})
|
||||
f["template_ctx"] = base
|
||||
|
||||
for f in fields:
|
||||
vl = _value_label_for_field(f, mapper, values_map, instance, session)
|
||||
if vl is not None:
|
||||
f["value_label"] = vl
|
||||
|
||||
# Build rows (supports nested layout with parents)
|
||||
rows_map = _normalize_rows_layout(layout)
|
||||
rows_tree = _assign_fields_to_rows(fields, rows_map)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
<textarea name="{{ field_name }}" id="{{ field_name }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>{{ value }}</textarea>
|
||||
{% endfor %}{% endif %}>{{ value if value else "" }}</textarea>
|
||||
|
||||
{% elif field_type == 'checkbox' %}
|
||||
<input type="checkbox" name="{{ field_name }}" id="{{ field_name }}" value="1"
|
||||
|
|
@ -40,15 +40,15 @@
|
|||
{% endfor %}{% endif %}>
|
||||
|
||||
{% elif field_type == 'hidden' %}
|
||||
<input type="hidden" name="{{ field_name }}" id="{{ field_name }}" value="{{ value }}">
|
||||
<input type="hidden" name="{{ field_name }}" id="{{ field_name }}" value="{{ value if value else "" }}">
|
||||
|
||||
{% elif field_type == 'display' %}
|
||||
<div {% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>{{ value }}</div>
|
||||
{% endfor %}{% endif %}>{{ value_label if value_label else (value if value else "") }}</div>
|
||||
|
||||
{% else %}
|
||||
<input type="text" name="{{ field_name }}" id="{{ field_name }}" value="{{ value }}"
|
||||
<input type="text" name="{{ field_name }}" id="{{ field_name }}" value="{{ value if value else "" }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue