CRUDkit fixes and changes.

This commit is contained in:
Yaro Kasear 2025-09-10 09:18:31 -05:00
parent 9d36d600bb
commit 7ddfe084ba
5 changed files with 98 additions and 43 deletions

View file

@ -11,26 +11,37 @@ class CRUDMixin:
def as_dict(self, fields: list[str] | None = None):
"""
Serialize mapped columns. Honors projection if either:
- 'fields' is passed explicitly, or
-
Serialize the instance.
- If 'fields' (possibly dotted) is provided, emit exactly those keys.
- Else, if '__crudkit_projection__' is set on the instance, emit those keys.
- Else, fall back to all mapped columns on this class hierarchy.
Always includes 'id' when present unless explicitly excluded.
"""
allowed = None
if fields is None:
fields = getattr(self, "__crudkit_projection__", None)
if fields:
allowed = set(fields)
else:
allowed = getattr(self, "__crudkit_root_fields__", None)
out = {}
if "id" not in fields and hasattr(self, "id"):
out["id"] = getattr(self, "id")
for f in fields:
cur = self
for part in f.split("."):
if cur is None:
break
cur = getattr(cur, part, None)
out[f] = cur
return out
result = {}
for cls in self.__class__.__mro__:
if not hasattr(cls, "__table__"):
continue
for column in cls.__table__.columns:
name = column.name
if allowed is not None and name not in allowed and name != "id":
continue
result[name] = getattr(self, name)
for cls in self.__clas__.__mro__:
if hasattr(cls, "__table__"):
for column in cls.__table__.columns:
name = column.name
result[name] = getattr(self, name)
return result
class Version(Base):
__tablename__ = "versions"

View file

@ -81,8 +81,8 @@ class CRUDService(Generic[T]):
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
query = query.options(eager)
if root_fields or rel_field_names:
query = query.options(Load(root_alias).raiseload("*"))
# if root_fields or rel_field_names:
# query = query.options(Load(root_alias).raiseload("*"))
if filters:
query = query.filter(*filters)
@ -98,26 +98,43 @@ class CRUDService(Generic[T]):
# Only apply offset/limit when not None.
if offset is not None and offset != 0:
query = query.offset(offset)
if limit is not None:
if limit is not None and limit > 0:
query = query.limit(limit)
# return query.all()
rows = query.all()
try:
rf_names = [c.key for c in (root_fields or [])]
except NameError:
rf_names = []
if rf_names:
allow = set(rf_names)
if "id" not in allow and hasattr(self.model, "id"):
allow.add("id")
proj = []
if root_fields:
proj.extend(c.key for c in root_fields)
for path, names in (rel_field_names or {}).items():
prefix = ".".join(path)
for n in names:
proj.append(f"{prefix}.{n}")
if proj and "id" not in proj and hasattr(self.model, "id"):
proj.insert(0, "id")
if proj:
for obj in rows:
try:
setattr(obj, "__crudkit_root_fields__", allow)
setattr(obj, "__crudkit_projection__", tuple(proj))
except Exception:
pass
# try:
# rf_names = [c.key for c in (root_fields or [])]
# except NameError:
# rf_names = []
# if rf_names:
# allow = set(rf_names)
# if "id" not in allow and hasattr(self.model, "id"):
# allow.add("id")
# for obj in rows:
# try:
# setattr(obj, "__crudkit_root_fields__", allow)
# except Exception:
# pass
return rows
def create(self, data: dict, actor=None) -> T:

View file

@ -1,7 +1,9 @@
import os
from flask import current_app
from jinja2 import Environment, FileSystemLoader, ChoiceLoader
from sqlalchemy.orm import class_mapper, RelationshipProperty
from flask import current_app
import os
from typing import List
def get_env():
app_loader = current_app.jinja_loader
@ -32,10 +34,15 @@ def render_field(field, value):
options=field.get('options', None)
)
def render_table(objects):
def render_table(objects, headers: List[str] | None = None):
env = get_env()
template = get_crudkit_template(env, 'table.html')
return template.render(objects=objects)
if not objects:
return template.render(fields=[], rows=[])
proj = getattr(objects[0], "__crudkit_projection__", None)
rows = [obj.as_dict(proj) for obj in objects]
fields = list(rows[0].keys())
return template.render(fields=fields, rows=rows, headers=headers)
def render_form(model_cls, values, session=None):
env = get_env()

View file

@ -1,12 +1,20 @@
<table>
{% if objects %}
{% if rows %}
<thead>
<tr>
{% for field in objects[0].__table__.columns %}<th>{{ field.name }}</th>{% endfor %}
{% if headers %}
{% for header in headers %}<th>{{ header }}</th>{% endfor %}
{% else %}
{% for field in fields if field != "id" %}<th>{{ field }}</th>{% endfor %}
{% endif %}
</tr>
{% for obj in objects %}
<tr>{% for field in obj.__table__.columns %}<td>{{ obj[field.name] }}</td>{% endfor %}</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>{% for _, cell in row.items() if _ != "id" %}<td>{{ cell if cell else "-" }}</td>{% endfor %}</tr>
{% endfor %}
{% else %}
<tr><th>No data.</th></tr>
{% endif %}
{% else %}
<tr><th>No data.</th></tr>
{% endif %}
</tbody>
</table>

View file

@ -16,11 +16,23 @@ def init_index_routes(app):
def index():
session = get_session()
work_log_service = CRUDService(WorkLog, session)
work_logs = work_log_service.list({"complete__ne": 1, "fields": ["start_time"]})
print(work_logs)
logs = render_table(work_logs)
work_logs = work_log_service.list({
"complete__ne": 1,
"fields": [
"start_time",
"contact.last_name",
"work_item.name"
],
"limit": 10
})
headers = [
"Start Time",
"Contact Last Name",
"Work Item"
]
logs = render_table(work_logs, headers)
return render_template("index.html", logs=logs)
return render_template("index.html", logs=logs, headers=headers)
@bp_index.get("/LICENSE")
def license():