diff --git a/crudkit/core/base.py b/crudkit/core/base.py index c66aaf3..2204778 100644 --- a/crudkit/core/base.py +++ b/crudkit/core/base.py @@ -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" diff --git a/crudkit/core/service.py b/crudkit/core/service.py index 39226d3..f84d276 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -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: diff --git a/crudkit/ui/fragments.py b/crudkit/ui/fragments.py index d30e24b..cef0f3c 100644 --- a/crudkit/ui/fragments.py +++ b/crudkit/ui/fragments.py @@ -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() diff --git a/crudkit/ui/templates/table.html b/crudkit/ui/templates/table.html index b4abd80..4005c0c 100644 --- a/crudkit/ui/templates/table.html +++ b/crudkit/ui/templates/table.html @@ -1,12 +1,20 @@ - {% if objects %} + {% if rows %} + - {% for field in objects[0].__table__.columns %}{% endfor %} + {% if headers %} + {% for header in headers %}{% endfor %} + {% else %} + {% for field in fields if field != "id" %}{% endfor %} + {% endif %} - {% for obj in objects %} - {% for field in obj.__table__.columns %}{% endfor %} + + + {% for row in rows %} + {% for _, cell in row.items() if _ != "id" %}{% endfor %} {% endfor %} - {% else %} - - {% endif %} + {% else %} + + {% endif %} +
{{ field.name }}{{ header }}{{ field }}
{{ obj[field.name] }}
{{ cell if cell else "-" }}
No data.
No data.
\ No newline at end of file diff --git a/inventory/routes/index.py b/inventory/routes/index.py index d189140..fd02776 100644 --- a/inventory/routes/index.py +++ b/inventory/routes/index.py @@ -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():