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): def as_dict(self, fields: list[str] | None = None):
""" """
Serialize mapped columns. Honors projection if either: Serialize the instance.
- 'fields' is passed explicitly, or - 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: if fields:
allowed = set(fields) out = {}
else: if "id" not in fields and hasattr(self, "id"):
allowed = getattr(self, "__crudkit_root_fields__", None) 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 = {} result = {}
for cls in self.__class__.__mro__: for cls in self.__clas__.__mro__:
if not hasattr(cls, "__table__"): if hasattr(cls, "__table__"):
continue for column in cls.__table__.columns:
for column in cls.__table__.columns: name = column.name
name = column.name result[name] = getattr(self, name)
if allowed is not None and name not in allowed and name != "id":
continue
result[name] = getattr(self, name)
return result return result
class Version(Base): class Version(Base):
__tablename__ = "versions" __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): for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
query = query.options(eager) query = query.options(eager)
if root_fields or rel_field_names: # if root_fields or rel_field_names:
query = query.options(Load(root_alias).raiseload("*")) # query = query.options(Load(root_alias).raiseload("*"))
if filters: if filters:
query = query.filter(*filters) query = query.filter(*filters)
@ -98,26 +98,43 @@ class CRUDService(Generic[T]):
# Only apply offset/limit when not None. # Only apply offset/limit when not None.
if offset is not None and offset != 0: if offset is not None and offset != 0:
query = query.offset(offset) query = query.offset(offset)
if limit is not None: if limit is not None and limit > 0:
query = query.limit(limit) query = query.limit(limit)
# return query.all()
rows = query.all() rows = query.all()
try: proj = []
rf_names = [c.key for c in (root_fields or [])] if root_fields:
except NameError: proj.extend(c.key for c in root_fields)
rf_names = [] for path, names in (rel_field_names or {}).items():
if rf_names: prefix = ".".join(path)
allow = set(rf_names) for n in names:
if "id" not in allow and hasattr(self.model, "id"): proj.append(f"{prefix}.{n}")
allow.add("id")
if proj and "id" not in proj and hasattr(self.model, "id"):
proj.insert(0, "id")
if proj:
for obj in rows: for obj in rows:
try: try:
setattr(obj, "__crudkit_root_fields__", allow) setattr(obj, "__crudkit_projection__", tuple(proj))
except Exception: except Exception:
pass 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 return rows
def create(self, data: dict, actor=None) -> T: 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 jinja2 import Environment, FileSystemLoader, ChoiceLoader
from sqlalchemy.orm import class_mapper, RelationshipProperty from sqlalchemy.orm import class_mapper, RelationshipProperty
from flask import current_app from typing import List
import os
def get_env(): def get_env():
app_loader = current_app.jinja_loader app_loader = current_app.jinja_loader
@ -32,10 +34,15 @@ def render_field(field, value):
options=field.get('options', None) options=field.get('options', None)
) )
def render_table(objects): def render_table(objects, headers: List[str] | None = None):
env = get_env() env = get_env()
template = get_crudkit_template(env, 'table.html') 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): def render_form(model_cls, values, session=None):
env = get_env() env = get_env()

View file

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

View file

@ -16,11 +16,23 @@ def init_index_routes(app):
def index(): def index():
session = get_session() session = get_session()
work_log_service = CRUDService(WorkLog, session) work_log_service = CRUDService(WorkLog, session)
work_logs = work_log_service.list({"complete__ne": 1, "fields": ["start_time"]}) work_logs = work_log_service.list({
print(work_logs) "complete__ne": 1,
logs = render_table(work_logs) "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") @bp_index.get("/LICENSE")
def license(): def license():