diff --git a/crudkit/core/service.py b/crudkit/core/service.py index 38b7e31..db510cd 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -6,7 +6,7 @@ from flask import current_app from typing import Any, Callable, 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, with_polymorphic, Mapper, selectinload +from sqlalchemy.orm import Load, Session, with_polymorphic, Mapper, selectinload, with_loader_criteria from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.sql import operators from sqlalchemy.sql.elements import UnaryExpression, ColumnElement @@ -186,6 +186,31 @@ class CRUDService(Generic[T]): # ---- common building blocks + def _apply_soft_delete_criteria_for_children(self, query, plan: "CRUDService._Plan", params): + # Skip if caller explicitly asked for deleted + if _is_truthy((params or {}).get("include_deleted")): + return query + + seen = set() + for _base_alias, rel_attr, _target_alias in plan.join_paths: + prop = getattr(rel_attr, "property", None) + if not prop: + continue + target_cls = getattr(prop.mapper, "class_", None) + if not target_cls or target_cls in seen: + continue + seen.add(target_cls) + # Only apply to models that support soft delete + if hasattr(target_cls, "is_deleted"): + query = query.options( + with_loader_criteria( + target_cls, + lambda cls: cls.is_deleted == False, + include_aliases=True + ) + ) + return query + def _order_clauses(self, order_spec, invert: bool = False): clauses = [] for c, is_desc in zip(order_spec.cols, order_spec.desc): @@ -442,6 +467,7 @@ class CRUDService(Generic[T]): plan = self._plan(params, root_alias) query = self._apply_projection_load_only(query, root_alias, plan) query = self._apply_firsthop_strategies(query, root_alias, plan) + query = self._apply_soft_delete_criteria_for_children(query, plan, params) if plan.filters: query = query.filter(*plan.filters) @@ -528,6 +554,7 @@ class CRUDService(Generic[T]): plan = self._plan(params, root_alias) query = self._apply_projection_load_only(query, root_alias, plan) query = self._apply_firsthop_strategies(query, root_alias, plan) + query = self._apply_soft_delete_criteria_for_children(query, plan, params) if plan.filters: query = query.filter(*plan.filters) query = query.filter(getattr(root_alias, "id") == id) @@ -548,6 +575,7 @@ class CRUDService(Generic[T]): query = self._apply_not_deleted(query, root_alias, params) query = self._apply_projection_load_only(query, root_alias, plan) query = self._apply_firsthop_strategies(query, root_alias, plan) + query = self._apply_soft_delete_criteria_for_children(query, plan, params) if plan.filters: query = query.filter(*plan.filters) diff --git a/inventory/routes/entry.py b/inventory/routes/entry.py index f434c0f..6b0c912 100644 --- a/inventory/routes/entry.py +++ b/inventory/routes/entry.py @@ -274,7 +274,6 @@ def init_entry_routes(app): for k in ("brand_id", "type_id", "owner_id", "location_id", "image_id"): if payload.get(k) == "": payload[k] = None - # payload["timestamp"] = datetime.now() if model == "worklog": if "contact" in payload and "contact_id" not in payload: @@ -285,13 +284,17 @@ def init_entry_routes(app): # Parent first, no commit yet obj = svc.create(payload, actor="create_entry", commit=False) + # Ensure PK is available for children and relationship auto-FK works + sess.flush() + # Children if model == "worklog" and updates: - note_cls = type(obj).updates.property.mapper.class_ + note_mapper = type(obj).updates.property.mapper + note_cls = note_mapper.class_ for item in updates: - content = (item.get("content") or "").trim() if hasattr(str, 'trim') else (item.get("content") or "").strip() + content = (item.get("content") or "").strip() if content: - sess.add(note_cls(work_log_id=obj.id, content=content)) + obj.updates.append(note_cls(content=content)) sess.commit() return {"status": "success", "id": obj.id} diff --git a/inventory/templates/submit_button.html b/inventory/templates/submit_button.html index 83bc09a..3a4c589 100644 --- a/inventory/templates/submit_button.html +++ b/inventory/templates/submit_button.html @@ -37,12 +37,18 @@ .map(el => Number(el.id.slice(3))) .filter(Number.isFinite); } + function collectEditedUpdates() { const updates = []; - for (const id of collectExistingUpdateIds()) updates.push({ id, content: getMarkdown(id) }); + const deleted = new Set(collectDeletedIds()); + for (const id of collectExistingUpdateIds()) { + if(deleted.has(id)) continue; // skip ones marked for deletion + updates.push({ id, content: getMarkdown(id) }); + } for (const md of (window.newDrafts || [])) if ((md ?? '').trim()) updates.push({ content: md }); return updates; } + function collectDeletedIds() { return (window.deletedIds || []).filter(Number.isFinite); } // much simpler, and correct diff --git a/inventory/templates/update_list.html b/inventory/templates/update_list.html index 92f04b8..0c8537e 100644 --- a/inventory/templates/update_list.html +++ b/inventory/templates/update_list.html @@ -13,42 +13,136 @@ {{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }} +
+ +
+ +
{% else %} -
  • No updates yet.
  • +
  • No updates yet.
  • {% endfor %} +
    + + +
    + + +
    + +
    +
    Pending new updates
    + +
    +
    + + \ No newline at end of file