Lots of form code done!
This commit is contained in:
parent
25589a79d3
commit
2ae96e5c80
6 changed files with 400 additions and 60 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any, Callable, Dict, Iterable, List, Tuple, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast
|
||||
from sqlalchemy import and_, func, inspect, or_, text
|
||||
from sqlalchemy.engine import Engine, Connection
|
||||
from sqlalchemy.orm import Load, Session, raiseload, selectinload, with_polymorphic, Mapper, RelationshipProperty
|
||||
from sqlalchemy.orm import Load, Session, raiseload, selectinload, with_polymorphic, Mapper, RelationshipProperty, class_mapper
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
from sqlalchemy.orm.util import AliasedClass
|
||||
from sqlalchemy.sql import operators
|
||||
|
|
@ -12,6 +12,35 @@ from crudkit.core.spec import CRUDSpec
|
|||
from crudkit.core.types import OrderSpec, SeekWindow
|
||||
from crudkit.backend import BackendInfo, make_backend_info
|
||||
|
||||
def _loader_options_for_fields(root_alias, model_cls, fields: list[str]) -> list[Load]:
|
||||
"""
|
||||
For bare MANYTOONE names in fields (e.g. "location"), selectinload the relationship
|
||||
and only fetch the related PK. This is enough for preselecting <select> inputs
|
||||
without projecting the FK column on the root model.
|
||||
"""
|
||||
opts: list[Load] = []
|
||||
if not fields:
|
||||
return opts
|
||||
|
||||
mapper = class_mapper(model_cls)
|
||||
for name in fields:
|
||||
prop = mapper.relationships.get(name)
|
||||
if not isinstance(prop, RelationshipProperty):
|
||||
continue
|
||||
if prop.direction.name != "MANYTOONE":
|
||||
continue
|
||||
|
||||
rel_attr = getattr(root_alias, name)
|
||||
target_cls = prop.mapper.class_
|
||||
# load_only PK if present; else just selectinload
|
||||
id_attr = getattr(target_cls, "id", None)
|
||||
if id_attr is not None:
|
||||
opts.append(selectinload(rel_attr).load_only(id_attr))
|
||||
else:
|
||||
opts.append(selectinload(rel_attr))
|
||||
|
||||
return opts
|
||||
|
||||
@runtime_checkable
|
||||
class _HasID(Protocol):
|
||||
id: int
|
||||
|
|
@ -358,6 +387,11 @@ class CRUDService(Generic[T]):
|
|||
if params:
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
|
||||
fields = (params or {}).get("fields") if isinstance(params, dict) else None
|
||||
if fields:
|
||||
for opt in _loader_options_for_fields(root_alias, self.model, fields):
|
||||
query = query.options(opt)
|
||||
|
||||
only_cols = [c for c in root_fields if isinstance(c, InstrumentedAttribute)]
|
||||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
|
|
@ -365,6 +399,11 @@ class CRUDService(Generic[T]):
|
|||
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
query = query.options(eager)
|
||||
|
||||
if params:
|
||||
fields = params.get("fields") or []
|
||||
for opt in _loader_options_for_fields(root_alias, self.model, fields):
|
||||
query = query.options(opt)
|
||||
|
||||
obj = query.first()
|
||||
|
||||
proj = []
|
||||
|
|
@ -422,6 +461,11 @@ class CRUDService(Generic[T]):
|
|||
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
query = query.options(eager)
|
||||
|
||||
if params:
|
||||
fields = params.get("fields") or []
|
||||
for opt in _loader_options_for_fields(root_alias, self.model, fields):
|
||||
query = query.options(opt)
|
||||
|
||||
if filters:
|
||||
query = query.filter(*filters)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from collections import OrderedDict
|
||||
from flask import current_app, url_for
|
||||
from jinja2 import Environment, FileSystemLoader, ChoiceLoader
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.orm import class_mapper, RelationshipProperty, load_only, selectinload
|
||||
from sqlalchemy.orm import Load, RelationshipProperty, class_mapper, load_only, selectinload
|
||||
from sqlalchemy.orm.base import NO_VALUE
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
|
@ -25,6 +26,130 @@ def get_env():
|
|||
loader=ChoiceLoader([app.jinja_loader, fallback_loader])
|
||||
)
|
||||
|
||||
def _normalize_rows_layout(layout: Optional[List[dict]]) -> Dict[str, dict]:
|
||||
"""
|
||||
Create node dicts for each row and link parent->children.
|
||||
Node shape:
|
||||
{
|
||||
'name': str,
|
||||
'legend': Optional[str],
|
||||
'attrs': dict, # sanitized
|
||||
'order': int,
|
||||
'parent': Optional[str],
|
||||
'children': list, # list of node names (we'll expand later)
|
||||
'fields': list, # filled later
|
||||
}
|
||||
Always ensures a 'main' node exists.
|
||||
"""
|
||||
nodes: Dict[str, dict] = {}
|
||||
|
||||
def make_node(name: str) -> dict:
|
||||
node = nodes.get(name)
|
||||
if node is None:
|
||||
node = nodes[name] = {
|
||||
"name": name,
|
||||
"legend": None,
|
||||
"attrs": {},
|
||||
"order": 0,
|
||||
"parent": None,
|
||||
"children": [],
|
||||
"fields": [],
|
||||
}
|
||||
return node
|
||||
|
||||
# seed nodes from layout
|
||||
if isinstance(layout, list):
|
||||
for item in layout:
|
||||
name = item.get("name")
|
||||
if not isinstance(name, str) or not name:
|
||||
continue
|
||||
node = make_node(name)
|
||||
node["legend"] = item.get("legend")
|
||||
node["attrs"] = _sanitize_attrs(item.get("attrs") or {})
|
||||
try:
|
||||
node["order"] = int(item.get("order") or 0)
|
||||
except Exception:
|
||||
node["order"] = 0
|
||||
parent = item.get("parent")
|
||||
node["parent"] = parent if isinstance(parent, str) and parent else None
|
||||
|
||||
# ensure main exists and is early-ordered
|
||||
main = make_node("main")
|
||||
if "order" not in main or main["order"] == 0:
|
||||
main["order"] = -10
|
||||
|
||||
# default any unknown parents to main (except main itself)
|
||||
for n in list(nodes.values()):
|
||||
if n["name"] == "main":
|
||||
n["parent"] = None
|
||||
continue
|
||||
p = n["parent"]
|
||||
if p is None or p not in nodes or p == n["name"]:
|
||||
n["parent"] = "main"
|
||||
|
||||
# detect cycles defensively; break by reparenting to main
|
||||
visiting = set()
|
||||
visited = set()
|
||||
|
||||
def visit(name: str):
|
||||
if name in visited:
|
||||
return
|
||||
if name in visiting:
|
||||
# cycle; break this node to main
|
||||
nodes[name]["parent"] = "main"
|
||||
return
|
||||
visiting.add(name)
|
||||
parent = nodes[name]["parent"]
|
||||
if parent is not None:
|
||||
visit(parent)
|
||||
visiting.remove(name)
|
||||
visited.add(name)
|
||||
|
||||
for nm in list(nodes.keys()):
|
||||
visit(nm)
|
||||
|
||||
# compute children lists
|
||||
for n in nodes.values():
|
||||
n["children"] = []
|
||||
for n in nodes.values():
|
||||
p = n["parent"]
|
||||
if p is not None:
|
||||
nodes[p]["children"].append(n["name"])
|
||||
|
||||
# sort children by (order, name) for deterministic rendering
|
||||
for n in nodes.values():
|
||||
n["children"].sort(key=lambda nm: (nodes[nm]["order"], nodes[nm]["name"]))
|
||||
|
||||
return nodes
|
||||
|
||||
def _assign_fields_to_rows(fields: List[dict], rows: Dict[str, dict]) -> List[dict]:
|
||||
"""
|
||||
Put fields into their target row buckets by name (default 'main'),
|
||||
then return a list of root nodes expanded with nested dicts ready for templates.
|
||||
"""
|
||||
# assign fields
|
||||
for f in fields:
|
||||
row_name = f.get("row") or "main"
|
||||
node = rows.get(row_name) or rows["main"]
|
||||
node["fields"].append(f)
|
||||
|
||||
# expand tree into nested structures
|
||||
def expand(name: str) -> dict:
|
||||
n = rows[name]
|
||||
return {
|
||||
"name": n["name"],
|
||||
"legend": n["legend"],
|
||||
"attrs": n["attrs"],
|
||||
"order": n["order"],
|
||||
"fields": n["fields"],
|
||||
"children": [expand(ch) for ch in n["children"]],
|
||||
}
|
||||
|
||||
# roots are nodes with parent == None
|
||||
roots = [expand(nm) for nm, n in rows.items() if n["parent"] is None]
|
||||
roots.sort(key=lambda r: (r["order"], r["name"]))
|
||||
return roots
|
||||
|
||||
def _sanitize_attrs(attrs: Any) -> dict[str, Any]:
|
||||
"""
|
||||
Whitelist attributes; allow data-* and aria-*; render True as boolean attr.
|
||||
|
|
@ -63,29 +188,37 @@ class _SafeObj:
|
|||
|
||||
def _coerce_fk_value(values: dict | None, instance: Any, base: str):
|
||||
"""
|
||||
Resolve the current selection for relationship 'base':
|
||||
Resolve current selection for relationship 'base':
|
||||
1) values['<base>_id']
|
||||
2) values['<base>']['id'] or values['<base>'] if scalar
|
||||
3) instance.<base> (relationship) if it's already loaded -> use its .id
|
||||
4) instance.<base>_id if it's already loaded (column) and instance is bound
|
||||
Never trigger a lazy load. Never touch the DB.
|
||||
2) values['<base>']['id'] or values['<base>'] if it's an int or numeric string
|
||||
3) instance.<base> (if already loaded) -> use its .id [safe for detached]
|
||||
4) instance.<base>_id (if already loaded and not expired)
|
||||
Never trigger a lazy load.
|
||||
"""
|
||||
# 1) explicit *_id from values
|
||||
if isinstance(values, dict):
|
||||
key = f"{base}_id"
|
||||
if key in values:
|
||||
return values.get(key)
|
||||
rel = values.get(base)
|
||||
if isinstance(rel, dict):
|
||||
return rel.get("id") or rel.get(key)
|
||||
if isinstance(rel, (int, str)):
|
||||
return rel
|
||||
|
||||
# 3) use loaded relationship object (safe for detached instances)
|
||||
rel = values.get(base)
|
||||
# 2a) nested dict with id
|
||||
if isinstance(rel, dict):
|
||||
vid = rel.get("id") or rel.get(key)
|
||||
if vid is not None:
|
||||
return vid
|
||||
# 2b) scalar id
|
||||
if isinstance(rel, int):
|
||||
return rel
|
||||
if isinstance(rel, str):
|
||||
s = rel.strip()
|
||||
if s.isdigit():
|
||||
return s # template compares as strings, so this is fine
|
||||
|
||||
# 3) use loaded relationship object (safe even if instance is detached)
|
||||
if instance is not None:
|
||||
try:
|
||||
state = inspect(instance)
|
||||
# relationship attr present?
|
||||
rel_attr = state.attrs.get(base)
|
||||
if rel_attr is not None and rel_attr.loaded_value is not NO_VALUE:
|
||||
rel_obj = rel_attr.loaded_value
|
||||
|
|
@ -93,7 +226,7 @@ def _coerce_fk_value(values: dict | None, instance: Any, base: str):
|
|||
rid = getattr(rel_obj, "id", None)
|
||||
if rid is not None:
|
||||
return rid
|
||||
# 4) use loaded fk column if the value is present and NOT expired
|
||||
# 4) use loaded fk column if present and not expired
|
||||
id_attr = state.attrs.get(f"{base}_id")
|
||||
if id_attr is not None and id_attr.loaded_value is not NO_VALUE:
|
||||
return id_attr.loaded_value
|
||||
|
|
@ -113,7 +246,7 @@ def _is_many_to_one(mapper, name: str) -> Optional[RelationshipProperty]:
|
|||
|
||||
def _rel_for_id_name(mapper, name: str) -> tuple[Optional[str], Optional[RelationshipProperty]]:
|
||||
if name.endswith("_id"):
|
||||
base = name[":-3"]
|
||||
base = name[:-3]
|
||||
prop = _is_many_to_one(mapper, base)
|
||||
return (base, prop) if prop else (None, None)
|
||||
else:
|
||||
|
|
@ -174,7 +307,13 @@ def _normalize_field_spec(spec, mapper, session, label_specs_model_default):
|
|||
"type": spec.get("type"),
|
||||
"options": spec.get("options"),
|
||||
"attrs": spec.get("attrs"),
|
||||
"label_attrs": spec.get("label_attrs"),
|
||||
"wrap": spec.get("wrap"),
|
||||
"row": spec.get("row"),
|
||||
"help": spec.get("help"),
|
||||
"template": spec.get("template"),
|
||||
"template_name": spec.get("template_name"),
|
||||
"template_ctx": spec.get("template_ctx"),
|
||||
}
|
||||
|
||||
if rel_prop:
|
||||
|
|
@ -503,17 +642,35 @@ def get_crudkit_template(env, name):
|
|||
|
||||
def render_field(field, value):
|
||||
env = get_env()
|
||||
|
||||
# 1) custom template field
|
||||
field_type = field.get('type', 'text')
|
||||
if field_type == 'template':
|
||||
tname = field.get('template') or field.get('template_name')
|
||||
if not tname:
|
||||
return "" # nothing to render
|
||||
t = get_crudkit_template(env, tname)
|
||||
# merge ctx with some sensible defaults
|
||||
ctx = dict(field.get('template_ctx') or {})
|
||||
# make sure templates always see these
|
||||
ctx.setdefault('field', field)
|
||||
ctx.setdefault('value', value)
|
||||
return t.render(**ctx)
|
||||
|
||||
# 2) normal controls
|
||||
template = get_crudkit_template(env, 'field.html')
|
||||
return template.render(
|
||||
field_name=field['name'],
|
||||
field_label=field.get('label', field['name']),
|
||||
value=value,
|
||||
field_type=field.get('type', 'text'),
|
||||
field_type=field_type,
|
||||
options=field.get('options', None),
|
||||
attrs=_sanitize_attrs(field.get('attrs') or {}),
|
||||
help=field.get('help')
|
||||
label_attrs=_sanitize_attrs(field.get('label_attrs') or {}),
|
||||
help=field.get('help'),
|
||||
)
|
||||
|
||||
|
||||
def render_table(objects: List[Any], columns: Optional[List[Dict[str, Any]]] = None, **opts):
|
||||
env = get_env()
|
||||
template = get_crudkit_template(env, 'table.html')
|
||||
|
|
@ -556,7 +713,10 @@ def render_form(
|
|||
label_specs: Optional[Dict[str, Any]] = None,
|
||||
exclude: Optional[set[str]] = None,
|
||||
overrides: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||
instance: Any = None, # NEW: pass the ORM object so we can read *_id
|
||||
instance: Any = None,
|
||||
layout: Optional[list[dict]] = None,
|
||||
submit_attrs: Optional[dict[str, Any]] = None,
|
||||
submit_label: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
fields_spec: list of dicts describing fields in order. Each dict supports:
|
||||
|
|
@ -571,6 +731,8 @@ def render_form(
|
|||
exclude: set of field names to hide.
|
||||
overrides: legacy quick overrides keyed by field name (label/type/etc.)
|
||||
instance: the ORM object backing the form; used to populate *_id values
|
||||
layout: A list of dicts describing layouts for fields.
|
||||
submit_attrs: A dict of attributes to apply to the submit button.
|
||||
"""
|
||||
env = get_env()
|
||||
template = get_crudkit_template(env, "form.html")
|
||||
|
|
@ -649,6 +811,29 @@ def render_form(
|
|||
"type": "checkbox" if getattr(col.type, "python_type", None) is bool else "text",
|
||||
}
|
||||
field = {**base_field, **overrides.get(col.name, {})}
|
||||
if field.get("wrap"):
|
||||
field["wrap"] = _sanitize_attrs(field["wrap"])
|
||||
fields.append(field)
|
||||
|
||||
return template.render(fields=fields, values=values_map, render_field=render_field)
|
||||
if submit_attrs:
|
||||
submit_attrs = _sanitize_attrs(submit_attrs)
|
||||
|
||||
common_ctx = {"values": values_map, "instance": instance, "model_cls": model_cls, "session": session}
|
||||
for f in fields:
|
||||
if f.get("type") == "template":
|
||||
base = dict(common_ctx)
|
||||
base.update(f.get("template_ctx") or {})
|
||||
f["template_ctx"] = base
|
||||
|
||||
# Build rows (supports nested layout with parents)
|
||||
rows_map = _normalize_rows_layout(layout)
|
||||
rows_tree = _assign_fields_to_rows(fields, rows_map)
|
||||
|
||||
return template.render(
|
||||
rows=rows_tree,
|
||||
fields=fields, # keep for backward compatibility
|
||||
values=values_map,
|
||||
render_field=render_field,
|
||||
submit_attrs=submit_attrs,
|
||||
submit_label=submit_label
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
<label for="{{ field_name }}">{{ field_label }}</label>
|
||||
{# show label unless hidden/custom #}
|
||||
{% if field_type != 'hidden' and field_label %}
|
||||
<label for="{{ field_name }}"
|
||||
{% if label_attrs %}{% for k,v in label_attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
{{ field_label }}
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
{% if field_type == 'select' %}
|
||||
<select name="{{ field_name }}" id="{{ field_name }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %} {{k}}-"{{v}}" {% endfor %}{% endif %}
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}
|
||||
{%- if not options %} disabled{% endif %}>
|
||||
{% if options %}
|
||||
<option value="">-- Select --</option>
|
||||
|
|
@ -18,20 +28,32 @@
|
|||
|
||||
{% elif field_type == 'textarea' %}
|
||||
<textarea name="{{ field_name }}" id="{{ field_name }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %} {{k}}="{{v}}"{% endfor %}{% endif %}>{{ value }}</textarea>
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>{{ value }}</textarea>
|
||||
|
||||
{% 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"
|
||||
{% if value %}checked{% endif %}
|
||||
{% if attrs %}{% for k,v in attrs.items() %} {{k}}="{{v}}"{% endfor %}{% endif %}>
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
|
||||
{% elif field_type == 'hidden' %}
|
||||
<input type="hidden" name="{{ field_name }}" id="{{ field_name }}" value="{{ value }}">
|
||||
|
||||
{% 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>
|
||||
|
||||
{% else %}
|
||||
<input type="text" name="{{ field_name }}" id="{{ field_name }}"
|
||||
value="{{ value }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %} {{k}}="{{v}}"{% endfor %}{% endif %}>
|
||||
<input type="text" name="{{ field_name }}" id="{{ field_name }}" value="{{ value }}"
|
||||
{% if attrs %}{% for k,v in attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
{% endif %}
|
||||
|
||||
{% if help %}
|
||||
<div>{{ help }}</div>
|
||||
<div class="form-text">{{ help }}</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,40 @@
|
|||
<form method="POST">
|
||||
{% macro render_row(row) %}
|
||||
<!-- {{ row.name }} -->
|
||||
{% if row.fields or row.children or row.legend %}
|
||||
{% if row.legend %}<legend>{{ row.legend }}</legend>{% endif %}
|
||||
<fieldset
|
||||
{% if row.attrs %}{% for k,v in row.attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
{% for field in row.fields %}
|
||||
<div
|
||||
{% if field.wrap %}{% for k,v in field.wrap.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}>
|
||||
{{ render_field(field, values.get(field.name, '')) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for child in row.children %}
|
||||
{{ render_row(child) }}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if rows %}
|
||||
{% for row in rows %}
|
||||
{{ render_row(row) }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for field in fields %}
|
||||
{{ render_field(field, values.get(field.name, '')) | safe }}
|
||||
{% endfor %}
|
||||
<button type="submit">Create</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit"
|
||||
{% if submit_attrs %}{% for k,v in submit_attrs.items() %}
|
||||
{{k}}{% if v is not sameas true %}="{{ v }}"{% endif %}
|
||||
{% endfor %}{% endif %}
|
||||
>{{ submit_label if label else 'Save' }}</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -14,26 +14,79 @@ def init_entry_routes(app):
|
|||
cls = crudkit.crud.get_model(model)
|
||||
if cls is None:
|
||||
abort(404)
|
||||
obj = crudkit.crud.get_service(cls).get(id, {"fields": ["first_name", "last_name", "title", "active", "staff", "location", "location_id"]})
|
||||
if model not in ["inventory", "worklog", "user"]:
|
||||
abort(404)
|
||||
|
||||
fields = {}
|
||||
fields_spec = []
|
||||
layout = []
|
||||
if model == "inventory":
|
||||
fields["fields"] = ["label", "name", "barcode", "serial"]
|
||||
fields_spec = [
|
||||
{"name": "label", "label": "", "row": "label", "wrap": {"class": "col"}},
|
||||
{"name": "name", "label": "Name", "row": "identification", "wrap": {"class": "col"}},
|
||||
{"name": "barcode", "label": "Bar Code #", "row": "identification", "wrap": {"class": "col"}},
|
||||
{"name": "serial", "label": "Serial #", "row": "identification", "wrap": {"class": "col"}},
|
||||
]
|
||||
layout = [
|
||||
{"name": "label", "order": 10, "attrs": {"class": "row"}},
|
||||
{"name": "identification", "order": 20, "attrs": {"class": "row"}},
|
||||
]
|
||||
elif model.lower() == 'user':
|
||||
fields["fields"] = ["label", "first_name", "last_name", "title", "active", "staff", "location", "supervisor"]
|
||||
fields_spec = [
|
||||
{"name": "label", "row": "label", "label": "User Record",
|
||||
"label_attrs": {"class": "display-6"}, "type": "display",
|
||||
"attrs": {"class": "display-4 mb-3"}, "wrap": {"class": "text-center"}},
|
||||
|
||||
{"name": "last_name", "label": "Last Name", "label_attrs": {"class": "form-label"},
|
||||
"attrs": {"placeholder": "Doe", "class": "form-control"},
|
||||
"row": "name", "wrap": {"class": "col-2"}},
|
||||
|
||||
{"name": "first_name", "label": "First Name", "label_attrs": {"class": "form-label"},
|
||||
"attrs": {"placeholder": "John", "class": "form-control"},
|
||||
"row": "name", "wrap": {"class": "col-2"}},
|
||||
|
||||
{"name": "title", "label": "Title", "label_attrs": {"class": "form-label"},
|
||||
"attrs": {"placeholder": "President of the Universe", "class": "form-control"},
|
||||
"row": "name", "wrap": {"class": "col-2"}},
|
||||
|
||||
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
||||
"label_spec": "{first_name} {last_name}", "row": "details", "wrap": {"class": "col-3"},
|
||||
"attrs": {"class": "form-control"}},
|
||||
|
||||
{"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"}},
|
||||
|
||||
{"name": "active", "label": "Active", "label_attrs": {"class": "form-check-label"},
|
||||
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
|
||||
|
||||
{"name": "staff", "label": "Staff Member", "label_attrs": {"class": "form-check-label"},
|
||||
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
|
||||
]
|
||||
layout = [
|
||||
{"name": "label", "order": 0},
|
||||
{"name": "name", "order": 10, "attrs": {"class": "row mb-3"}},
|
||||
{"name": "details", "order": 20, "attrs": {"class": "row"}},
|
||||
{"name": "checkboxes", "order": 30, "parent": "name", "attrs": {"class": "col d-flex flex-column justify-content-end"}}
|
||||
]
|
||||
elif model == "worklog":
|
||||
pass
|
||||
|
||||
obj = crudkit.crud.get_service(cls).get(id, fields)
|
||||
if obj is None:
|
||||
abort(404)
|
||||
|
||||
fields_spec = [
|
||||
{"name": "last_name", "label": "Last Name", "attrs": {"placeholder": "Doe"}},
|
||||
{"name": "first_name", "label": "First Name", "attrs": {"placeholder": "John"}},
|
||||
{"name": "title", "label": "Title", "attrs": {"placeholder": "President of the Universe"}},
|
||||
{"name": "active", "label": "Active"},
|
||||
{"name": "staff", "label": "Staff Member"},
|
||||
{"name": "location", "label": "Room", "label_spec": "{name} - {room_function.description}"}
|
||||
]
|
||||
form = render_form(
|
||||
cls,
|
||||
obj.as_dict(),
|
||||
crudkit.crud.get_service(cls).session,
|
||||
instance=obj,
|
||||
fields_spec=fields_spec
|
||||
fields_spec=fields_spec,
|
||||
layout=layout,
|
||||
submit_attrs={"class": "btn btn-primary"}
|
||||
)
|
||||
|
||||
return render_template("entry.html", form=form)
|
||||
|
||||
app.register_blueprint(bp_entry)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block main %}
|
||||
{{ form | safe }}
|
||||
<div class="container mt-5">
|
||||
{{ form | safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue