More entry code and some fixups.
This commit is contained in:
parent
2ae96e5c80
commit
d8e8790987
7 changed files with 233 additions and 57 deletions
|
|
@ -26,6 +26,24 @@ def get_env():
|
||||||
loader=ChoiceLoader([app.jinja_loader, fallback_loader])
|
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]:
|
def _normalize_rows_layout(layout: Optional[List[dict]]) -> Dict[str, dict]:
|
||||||
"""
|
"""
|
||||||
Create node dicts for each row and link parent->children.
|
Create node dicts for each row and link parent->children.
|
||||||
|
|
@ -173,6 +191,69 @@ def _sanitize_attrs(attrs: Any) -> dict[str, Any]:
|
||||||
|
|
||||||
return out
|
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:
|
class _SafeObj:
|
||||||
"""Attribute access that returns '' for missing/None instead of exploding."""
|
"""Attribute access that returns '' for missing/None instead of exploding."""
|
||||||
__slots__ = ("_obj",)
|
__slots__ = ("_obj",)
|
||||||
|
|
@ -181,10 +262,8 @@ class _SafeObj:
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
if self._obj is None:
|
if self._obj is None:
|
||||||
return ""
|
return ""
|
||||||
val = getattr(self._obj, name, None)
|
val = _get_loaded_attr(self._obj, name)
|
||||||
if val is None:
|
return "" if val is None else _SafeObj(val)
|
||||||
return ""
|
|
||||||
return _SafeObj(val)
|
|
||||||
|
|
||||||
def _coerce_fk_value(values: dict | None, instance: Any, base: str):
|
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": spec.get("template"),
|
||||||
"template_name": spec.get("template_name"),
|
"template_name": spec.get("template_name"),
|
||||||
"template_ctx": spec.get("template_ctx"),
|
"template_ctx": spec.get("template_ctx"),
|
||||||
|
"label_spec": spec.get("label_spec")
|
||||||
}
|
}
|
||||||
|
|
||||||
if rel_prop:
|
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
|
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:
|
def _label_from_obj(obj: Any, spec: Any) -> str:
|
||||||
|
if obj is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
if spec is None:
|
if spec is None:
|
||||||
for attr in ("label", "name", "title", "description"):
|
for attr in ("label", "name", "title", "description"):
|
||||||
if hasattr(obj, attr):
|
val = _get_loaded_attr(obj, attr)
|
||||||
val = getattr(obj, attr)
|
if val is not None:
|
||||||
if not callable(val) and val is not None:
|
|
||||||
return str(val)
|
return str(val)
|
||||||
if hasattr(obj, "id"):
|
vid = _get_loaded_attr(obj, "id")
|
||||||
return str(getattr(obj, "id"))
|
return str(vid) if vid is not None else object.__repr__(obj)
|
||||||
return object.__repr__(obj)
|
|
||||||
|
|
||||||
if isinstance(spec, (list, tuple)):
|
if isinstance(spec, (list, tuple)):
|
||||||
parts = []
|
parts = []
|
||||||
for a in spec:
|
for a in spec:
|
||||||
cur = obj
|
cur = obj
|
||||||
for part in str(a).split("."):
|
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:
|
if cur is None:
|
||||||
break
|
break
|
||||||
parts.append("" if cur is None else str(cur))
|
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:
|
for f in fields:
|
||||||
root = f.split(".", 1)[0]
|
root = f.split(".", 1)[0]
|
||||||
if root not in data:
|
if root not in data:
|
||||||
val = getattr(obj, root, None)
|
data[root] = _SafeObj(_get_loaded_attr(obj, root))
|
||||||
data[root] = _SafeObj(val)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return spec.format(**data)
|
return spec.format(**data)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -443,7 +504,7 @@ def _label_from_obj(obj: Any, spec: Any) -> str:
|
||||||
|
|
||||||
cur = obj
|
cur = obj
|
||||||
for part in str(spec).split("."):
|
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:
|
if cur is None:
|
||||||
return ""
|
return ""
|
||||||
return str(cur)
|
return str(cur)
|
||||||
|
|
@ -572,6 +633,28 @@ def _format_value(val: Any, fmt: Optional[str]) -> Any:
|
||||||
return val
|
return val
|
||||||
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]:
|
def _class_for(val: Any, classes: Optional[Dict[str, str]]) -> Optional[str]:
|
||||||
if not classes:
|
if not classes:
|
||||||
return None
|
return None
|
||||||
|
|
@ -642,6 +725,7 @@ def get_crudkit_template(env, name):
|
||||||
|
|
||||||
def render_field(field, value):
|
def render_field(field, value):
|
||||||
env = get_env()
|
env = get_env()
|
||||||
|
print(field)
|
||||||
|
|
||||||
# 1) custom template field
|
# 1) custom template field
|
||||||
field_type = field.get('type', 'text')
|
field_type = field.get('type', 'text')
|
||||||
|
|
@ -668,6 +752,7 @@ def render_field(field, value):
|
||||||
attrs=_sanitize_attrs(field.get('attrs') or {}),
|
attrs=_sanitize_attrs(field.get('attrs') or {}),
|
||||||
label_attrs=_sanitize_attrs(field.get('label_attrs') or {}),
|
label_attrs=_sanitize_attrs(field.get('label_attrs') or {}),
|
||||||
help=field.get('help'),
|
help=field.get('help'),
|
||||||
|
value_label=field.get('value_label'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -825,6 +910,11 @@ def render_form(
|
||||||
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:
|
||||||
|
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)
|
# Build rows (supports nested layout with parents)
|
||||||
rows_map = _normalize_rows_layout(layout)
|
rows_map = _normalize_rows_layout(layout)
|
||||||
rows_tree = _assign_fields_to_rows(fields, rows_map)
|
rows_tree = _assign_fields_to_rows(fields, rows_map)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<textarea name="{{ field_name }}" id="{{ field_name }}"
|
<textarea name="{{ field_name }}" id="{{ field_name }}"
|
||||||
{% if attrs %}{% for k,v in attrs.items() %}
|
{% if attrs %}{% for k,v in attrs.items() %}
|
||||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||||
{% endfor %}{% endif %}>{{ value }}</textarea>
|
{% endfor %}{% endif %}>{{ value if value else "" }}</textarea>
|
||||||
|
|
||||||
{% elif field_type == 'checkbox' %}
|
{% elif field_type == 'checkbox' %}
|
||||||
<input type="checkbox" name="{{ field_name }}" id="{{ field_name }}" value="1"
|
<input type="checkbox" name="{{ field_name }}" id="{{ field_name }}" value="1"
|
||||||
|
|
@ -40,15 +40,15 @@
|
||||||
{% endfor %}{% endif %}>
|
{% endfor %}{% endif %}>
|
||||||
|
|
||||||
{% elif field_type == 'hidden' %}
|
{% 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' %}
|
{% elif field_type == 'display' %}
|
||||||
<div {% if attrs %}{% for k,v in attrs.items() %}
|
<div {% if attrs %}{% for k,v in attrs.items() %}
|
||||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
{{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 %}
|
{% 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() %}
|
{% if attrs %}{% for k,v in attrs.items() %}
|
||||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||||
{% endfor %}{% endif %}>
|
{% endfor %}{% endif %}>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import crudkit
|
||||||
|
|
||||||
from crudkit.integrations.flask import init_app
|
from crudkit.integrations.flask import init_app
|
||||||
|
|
||||||
|
from .debug_pretty import init_pretty
|
||||||
|
|
||||||
from .routes.index import init_index_routes
|
from .routes.index import init_index_routes
|
||||||
from .routes.listing import init_listing_routes
|
from .routes.listing import init_listing_routes
|
||||||
from .routes.entry import init_entry_routes
|
from .routes.entry import init_entry_routes
|
||||||
|
|
@ -13,6 +15,8 @@ from .routes.entry import init_entry_routes
|
||||||
def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
init_pretty(app)
|
||||||
|
|
||||||
runtime = init_app(app, config=crudkit.ProdConfig)
|
runtime = init_app(app, config=crudkit.ProdConfig)
|
||||||
crudkit.init_crud(app)
|
crudkit.init_crud(app)
|
||||||
print(f"Effective DB URL: {str(runtime.engine.url)}")
|
print(f"Effective DB URL: {str(runtime.engine.url)}")
|
||||||
|
|
|
||||||
31
inventory/debug_pretty.py
Normal file
31
inventory/debug_pretty.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from flask import request
|
||||||
|
from werkzeug.wrappers.response import Response
|
||||||
|
|
||||||
|
def init_pretty(app):
|
||||||
|
@app.after_request
|
||||||
|
def _pretty_html(resp: Response):
|
||||||
|
if not app.debug:
|
||||||
|
print("Not debugging.")
|
||||||
|
return resp
|
||||||
|
if resp.mimetype != "text/html":
|
||||||
|
return resp
|
||||||
|
if request.args.get("pretty") != "1":
|
||||||
|
return resp
|
||||||
|
|
||||||
|
html = resp.get_data(as_text=True)
|
||||||
|
try:
|
||||||
|
# Prefer lxml if present; falls back to bs4
|
||||||
|
from lxml import html as lhtml
|
||||||
|
pretty = lhtml.tostring(
|
||||||
|
lhtml.fromstring(html),
|
||||||
|
encoding="unicode",
|
||||||
|
method="html",
|
||||||
|
pretty_print=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
pretty = BeautifulSoup(html, "html.parser").prettify(formatter="html")
|
||||||
|
|
||||||
|
resp.set_data(pretty)
|
||||||
|
resp.headers["Content-Length"] = str(len(pretty.encode("utf-8")))
|
||||||
|
return resp
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import List, Optional, TYPE_CHECKING
|
from typing import List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Integer, ForeignKey, Unicode, func
|
from sqlalchemy import Boolean, Integer, ForeignKey, Unicode, case, func, literal
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from sqlalchemy.sql import expression as sql
|
from sqlalchemy.sql import expression as sql
|
||||||
|
|
@ -41,8 +41,25 @@ class User(Base, CRUDMixin):
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def label(self):
|
def label(self):
|
||||||
return f"{self.first_name} {self.last_name}"
|
out = f"{self.first_name} {self.last_name}"
|
||||||
|
if self.title:
|
||||||
|
out = out + f" ({self.title})"
|
||||||
|
return out
|
||||||
|
|
||||||
@label.expression
|
@label.expression
|
||||||
def label(cls):
|
def label(cls):
|
||||||
return func.concat(cls.first_name, " ", cls.last_name)
|
first = func.coalesce(cls.first_name, "")
|
||||||
|
last = func.coalesce(cls.last_name, "")
|
||||||
|
title = func.coalesce(cls.title, "")
|
||||||
|
|
||||||
|
have_first = func.length(func.trim(first)) > 0
|
||||||
|
have_last = func.length(func.trim(last)) > 0
|
||||||
|
|
||||||
|
space = case((have_first & have_last, literal(" ")), else_=literal(""))
|
||||||
|
|
||||||
|
title_part = case(
|
||||||
|
(func.length(func.trim(title)) > 0, func.concat(" (", title, ")")),
|
||||||
|
else_=literal("")
|
||||||
|
)
|
||||||
|
|
||||||
|
return func.concat(first, space, last, title_part)
|
||||||
|
|
|
||||||
|
|
@ -21,35 +21,65 @@ def init_entry_routes(app):
|
||||||
fields_spec = []
|
fields_spec = []
|
||||||
layout = []
|
layout = []
|
||||||
if model == "inventory":
|
if model == "inventory":
|
||||||
fields["fields"] = ["label", "name", "barcode", "serial"]
|
fields["fields"] = ["label", "name", "serial", "barcode", "brand", "model", "device_type", "owner", "location", "condition", "image"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "label", "label": "", "row": "label", "wrap": {"class": "col"}},
|
{"name": "label", "type": "display", "label": "",
|
||||||
{"name": "name", "label": "Name", "row": "identification", "wrap": {"class": "col"}},
|
"label_attrs": {"class": "display-6 me-2"}, "row": "label",
|
||||||
{"name": "barcode", "label": "Bar Code #", "row": "identification", "wrap": {"class": "col"}},
|
"attrs": {"class": "display-6 mb-3"}},
|
||||||
{"name": "serial", "label": "Serial #", "row": "identification", "wrap": {"class": "col"}},
|
|
||||||
|
{"name": "name", "row": "names", "label": "Name", "wrap": {"class": "col-3"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
{"name": "serial", "row": "names", "label": "Serial #", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
{"name": "barcode", "row": "names", "label": "Barcode #", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
|
{"name": "brand", "label_spec": "{name}", "row": "device", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label": "Brand", "label_attrs": {"class": "form-label"}},
|
||||||
|
{"name": "model", "row": "device", "wrap": {"class": "col"}, "attrs": {"class": "form-control"},
|
||||||
|
"label": "Model #", "label_attrs": {"class": "form-label"}},
|
||||||
|
{"name": "device_type", "label_spec": "{description}", "row": "device", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label": "Device Type", "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
|
{"name": "owner", "row": "status", "label": "Contact", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"},
|
||||||
|
"label_spec": "{first_name} {last_name}"},
|
||||||
|
{"name": "location", "row": "status", "label": "Location", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"},
|
||||||
|
"label_spec": "{name} - {room_function.description}"},
|
||||||
|
{"name": "condition", "row": "status", "label": "Condition", "wrap": {"class": "col"},
|
||||||
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
|
{"name": "image", "label": "", "row": "image", "type": "template", "label_spec": "{filename}",
|
||||||
|
"template": "image_display.html", "attrs": {"class": "img-fluid img-thumbnail"}}
|
||||||
]
|
]
|
||||||
layout = [
|
layout = [
|
||||||
{"name": "label", "order": 10, "attrs": {"class": "row"}},
|
{"name": "label", "order": 5},
|
||||||
{"name": "identification", "order": 20, "attrs": {"class": "row"}},
|
{"name": "kitchen_sink", "order": 6, "attrs": {"class": "row"}},
|
||||||
|
{"name": "everything", "order": 10, "attrs": {"class": "col"}, "parent": "kitchen_sink"},
|
||||||
|
{"name": "names", "order": 20, "attrs": {"class": "row"}, "parent": "everything"},
|
||||||
|
{"name": "device", "order": 30, "attrs": {"class": "row mt-2"}, "parent": "everything"},
|
||||||
|
{"name": "status", "order": 40, "attrs": {"class": "row mt-2"}, "parent": "everything"},
|
||||||
|
{"name": "image", "order": 50, "attrs": {"class": "col-4"}, "parent": "kitchen_sink"}
|
||||||
]
|
]
|
||||||
elif model.lower() == 'user':
|
elif model.lower() == 'user':
|
||||||
fields["fields"] = ["label", "first_name", "last_name", "title", "active", "staff", "location", "supervisor"]
|
fields["fields"] = ["label", "first_name", "last_name", "title", "active", "staff", "location", "supervisor"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "label", "row": "label", "label": "User Record",
|
{"name": "label", "row": "label", "label": "",
|
||||||
"label_attrs": {"class": "display-6"}, "type": "display",
|
"label_attrs": {"class": "display-6 me-2"}, "type": "display",
|
||||||
"attrs": {"class": "display-4 mb-3"}, "wrap": {"class": "text-center"}},
|
"attrs": {"class": "display-6 mb-3"}},
|
||||||
|
|
||||||
{"name": "last_name", "label": "Last Name", "label_attrs": {"class": "form-label"},
|
{"name": "last_name", "label": "Last Name", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "Doe", "class": "form-control"},
|
"attrs": {"placeholder": "Doe", "class": "form-control"},
|
||||||
"row": "name", "wrap": {"class": "col-2"}},
|
"row": "name", "wrap": {"class": "col-3"}},
|
||||||
|
|
||||||
{"name": "first_name", "label": "First Name", "label_attrs": {"class": "form-label"},
|
{"name": "first_name", "label": "First Name", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "John", "class": "form-control"},
|
"attrs": {"placeholder": "John", "class": "form-control"},
|
||||||
"row": "name", "wrap": {"class": "col-2"}},
|
"row": "name", "wrap": {"class": "col-3"}},
|
||||||
|
|
||||||
{"name": "title", "label": "Title", "label_attrs": {"class": "form-label"},
|
{"name": "title", "label": "Title", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "President of the Universe", "class": "form-control"},
|
"attrs": {"placeholder": "President of the Universe", "class": "form-control"},
|
||||||
"row": "name", "wrap": {"class": "col-2"}},
|
"row": "name", "wrap": {"class": "col-3"}},
|
||||||
|
|
||||||
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
||||||
"label_spec": "{first_name} {last_name}", "row": "details", "wrap": {"class": "col-3"},
|
"label_spec": "{first_name} {last_name}", "row": "details", "wrap": {"class": "col-3"},
|
||||||
|
|
@ -67,8 +97,8 @@ def init_entry_routes(app):
|
||||||
]
|
]
|
||||||
layout = [
|
layout = [
|
||||||
{"name": "label", "order": 0},
|
{"name": "label", "order": 0},
|
||||||
{"name": "name", "order": 10, "attrs": {"class": "row mb-3"}},
|
{"name": "name", "order": 10, "attrs": {"class": "row"}},
|
||||||
{"name": "details", "order": 20, "attrs": {"class": "row"}},
|
{"name": "details", "order": 20, "attrs": {"class": "row mt-2"}},
|
||||||
{"name": "checkboxes", "order": 30, "parent": "name", "attrs": {"class": "col d-flex flex-column justify-content-end"}}
|
{"name": "checkboxes", "order": 30, "parent": "name", "attrs": {"class": "col d-flex flex-column justify-content-end"}}
|
||||||
]
|
]
|
||||||
elif model == "worklog":
|
elif model == "worklog":
|
||||||
|
|
@ -85,7 +115,7 @@ def init_entry_routes(app):
|
||||||
instance=obj,
|
instance=obj,
|
||||||
fields_spec=fields_spec,
|
fields_spec=fields_spec,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
submit_attrs={"class": "btn btn-primary"}
|
submit_attrs={"class": "btn btn-primary mt-3"}
|
||||||
)
|
)
|
||||||
return render_template("entry.html", form=form)
|
return render_template("entry.html", form=form)
|
||||||
|
|
||||||
|
|
|
||||||
4
inventory/templates/image_display.html
Normal file
4
inventory/templates/image_display.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<img src="{{ url_for('static', filename=field['value_label']) }}" alt="{{ value }}"
|
||||||
|
{% if field['attrs'] %}{% for k,v in field['attrs'].items() %}
|
||||||
|
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||||
|
{% endfor %}{% endif %}>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue