From eff6da0a3b4d299880aaecd0f60a8cf80c70a771 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Tue, 7 Oct 2025 13:15:30 -0500 Subject: [PATCH] Fix soft delete not being recognized. --- crudkit/core/service.py | 30 +++++++++++- inventory/templates/submit_button.html | 8 ++- inventory/templates/update_list.html | 68 ++++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 5 deletions(-) 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/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 eda6293..0c8537e 100644 --- a/inventory/templates/update_list.html +++ b/inventory/templates/update_list.html @@ -13,16 +13,20 @@ {{ n.timestamp.strftime("%Y-%m-%d %H:%M") if n.timestamp else "" }} +
- +
+ +
+ +
{% else %} -
  • No updates yet.
  • +
  • No updates yet.
  • {% endfor %} @@ -77,6 +81,64 @@ ta.value = ''; } + function renderNewDrafts() { + const wrap = document.getElementById('newDraftsList'); + const ul = document.getElementById('newDraftsUl'); + ul.innerHTML = ''; + const drafts = window.newDrafts || []; + if (!drafts.length) { + wrap.classList.add('d-none'); + return; + } + wrap.classList.remove('d-none'); + drafts.forEach((md, idx) => { + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-start'; + const left = document.createElement('div'); + left.className = 'w-100 markdown-body'; + left.innerHTML = DOMPurify.sanitize(marked.parse(md || '')); + for (const a of left.querySelectorAll('a[href]')) { + a.setAttribute('target','_blank'); + a.setAttribute('rel','noopener noreferrer nofollow'); + } + const right = document.createElement('div'); + right.className = 'ms-3 d-flex flex-column align-items-end'; + right.innerHTML = `"`; + li.appendChild(left); + li.appendChild(right); + ul.appendChild(li); + }); + const noRow = document.getElementById('noUpdatesRow'); + if (noRow) { + if (drafts.length) noRow.classList.add('d-none'); + else noRow.classList.remove('d-none'); + } + } + + function deleteDraft(idx) { + if (!Array.isArray(window.newDrafts)) return; + window.newDrafts.splice(idx, 1); + renderNewDrafts(); + } + + function toggleDelete(id) { + window.deletedIds = window.deletedIds || []; + const idx = window.deletedIds.indexOf(id); + const btn = document.getElementById(`deleteBtn${id}`); + const container = document.getElementById(`editContainer${id}`); + if (idx === -1) { + window.deletedIds.push(id); + if (btn) btn.classList.replace('btn-outline-danger', 'btn-danger'); + if (btn) btn.textContent = 'Undelete'; + if (container) container.style.opacity = '0.5'; + } else { + window.deletedIds.splice(idx, 1); + if (btn) btn.classList.replace('btn-danger', 'btn-outline-danger'); + if (btn) btn.textContent = 'Delete'; + if (container) container.style.opacity = '1'; + } + } + // Initial render document.addEventListener('DOMContentLoaded', () => { const ids = [{% for n in items %} {{ n.id }}{% if not loop.last %}, {% endif %} {% endfor %} ];