From 16d5f2ab98adb5e2227ad4fe7d1acb4bbb93cda1 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Thu, 2 Oct 2025 09:55:53 -0500 Subject: [PATCH 1/2] Improving the Work Item rendering. --- crudkit/core/__init__.py | 1 + crudkit/core/service.py | 34 ++++++++------ crudkit/core/utils.py | 28 +++++++++++ inventory/__init__.py | 2 + inventory/templates/update_list.html | 69 ++++++++++++++++++++++++++-- pyproject.toml | 1 + 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/crudkit/core/__init__.py b/crudkit/core/__init__.py index c2b1775..86d90b7 100644 --- a/crudkit/core/__init__.py +++ b/crudkit/core/__init__.py @@ -5,4 +5,5 @@ from .utils import ( deep_diff, diff_to_patch, filter_to_columns, + to_jsonable, ) diff --git a/crudkit/core/service.py b/crudkit/core/service.py index 5766f8f..366900a 100644 --- a/crudkit/core/service.py +++ b/crudkit/core/service.py @@ -10,7 +10,7 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.sql import operators from sqlalchemy.sql.elements import UnaryExpression, ColumnElement -from crudkit.core import deep_diff, diff_to_patch, filter_to_columns, normalize_payload +from crudkit.core import to_jsonable, deep_diff, diff_to_patch, filter_to_columns, normalize_payload from crudkit.core.base import Version from crudkit.core.spec import CRUDSpec from crudkit.core.types import OrderSpec, SeekWindow @@ -731,19 +731,23 @@ class CRUDService(Generic[T]): def _log_version(self, change_type: str, obj: T, actor=None, metadata: dict | None = None): session = self.session - snapshot = {} try: - snapshot = obj.as_dict() - except Exception: - snapshot = {"error": "serialize failed"} + snapshot = {} + try: + snapshot = obj.as_dict() + except Exception: + snapshot = {"error": "serialize failed"} - version = Version( - model_name=self.model.__name__, - object_id=obj.id, - change_type=change_type, - data=snapshot, - actor=str(actor) if actor else None, - meta=metadata or None, - ) - session.add(version) - session.commit() + version = Version( + model_name=self.model.__name__, + object_id=obj.id, + change_type=change_type, + data=to_jsonable(snapshot), + actor=str(actor) if actor else None, + meta=to_jsonable(metadata) if metadata else None, + ) + session.add(version) + session.commit() + except Exception as e: + log.warning(f"Version logging failed for {self.model.__name__} id={getattr(obj, "id", "?")}: {str(e)}") + session.rollback() diff --git a/crudkit/core/utils.py b/crudkit/core/utils.py index 31fa227..d03a7d6 100644 --- a/crudkit/core/utils.py +++ b/crudkit/core/utils.py @@ -1,5 +1,7 @@ from __future__ import annotations from datetime import datetime, date +from decimal import Decimal +from enum import Enum from typing import Any, Dict, Optional, Callable from sqlalchemy import inspect @@ -8,6 +10,32 @@ ISO_DT_FORMATS = ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M", "%Y-%m-%d") +def to_jsonable(obj: Any): + """Recursively convert values into JSON-serializable forms.""" + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + + if isinstance(obj, (datetime, date)): + return obj.isoformat() + + if isinstance(obj, Decimal): + return float(obj) + + if isinstance(obj, Enum): + return obj.value + + if isinstance(obj, dict): + return {str(k): to_jsonable(v) for k, v in obj.items()} + + if isinstance(obj, (list, tuple, set)): + return [to_jsonable(v) for v in obj] + + # fallback: strin-ify weird objects (UUID, ORM instances, etc.) + try: + return str(obj) + except Exception: + return None + def filter_to_columns(data: dict, model_cls): cols = {c.key for c in inspect(model_cls).mapper.columns} return {k: v for k, v in data.items() if k in cols} diff --git a/inventory/__init__.py b/inventory/__init__.py index cea7eea..c059d3b 100644 --- a/inventory/__init__.py +++ b/inventory/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations from flask import Flask +from jinja_markdown import MarkdownExtension from sqlalchemy.engine import Engine from sqlalchemy import event from sqlalchemy.pool import Pool @@ -18,6 +19,7 @@ from .routes.search import init_search_routes def create_app(config_cls=crudkit.DevConfig) -> Flask: app = Flask(__name__) + app.jinja_env.add_extension(MarkdownExtension) init_pretty(app) diff --git a/inventory/templates/update_list.html b/inventory/templates/update_list.html index aff8fed..096543b 100644 --- a/inventory/templates/update_list.html +++ b/inventory/templates/update_list.html @@ -1,15 +1,74 @@ {% set items = (field.template_ctx.instance.updates or []) %} + \ No newline at end of file + + + + + diff --git a/pyproject.toml b/pyproject.toml index e15d734..f0888ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "flask", "flask_sqlalchemy", "html5lib", + "jinja_markdown", "pandas", "pyodbc", "python-dotenv", From 01c6bb3d09f2e3140edb5e0cf27f5771f011b021 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Thu, 2 Oct 2025 10:09:25 -0500 Subject: [PATCH 2/2] Edit logic implemented. --- inventory/templates/update_list.html | 38 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/inventory/templates/update_list.html b/inventory/templates/update_list.html index 096543b..6062dbd 100644 --- a/inventory/templates/update_list.html +++ b/inventory/templates/update_list.html @@ -4,7 +4,7 @@ {% for n in items %}
  • -
    +
    @@ -61,14 +61,46 @@ container.dataset.prev = container.innerHTML; container.innerHTML = ` - +
    - + + +
    +
    `; } else { // Switch to viewer mode renderView(id, contents[id]); } } + + function saveEdit(id) { + const textarea = document.getElementById(`editor${id}`); + const value = textarea.value; + contents[id] = value; + renderView(id, value); + + document.getElementById(`editSwitch${id}`).checked = false; + } + + function cancelEdit(id) { + document.getElementById(`editSwitch${id}`).checked = false; + renderView(id, contents[id]); + } + + function togglePreview(id) { + const textarea = document.getElementById(`editor${id}`); + const preview = document.getElementById(`preview${id}`); + preview.classList.toggle('d-none'); + if (!preview.classList.contains('d-none')) { + const html = marked.parse(textarea.value ?? ""); + preview.innerHTML = DOMPurify.sanitize(html); + } + } + + function escapeForTextarea(s) { + // Keep control of what goes inside the textarea + return (s ?? "").replace(/&/g,'&').replace(//g,'>'); + }