Fix: Throughput API v1 available - Details posted to Irving. POWERBI_KEY was missing from the .ENV file, so was not live. Add: Editor now supports editing a mix's resolved formula directly, with % and kg dual entry on ingredient rows Fix: Mix Editor should bring through correct ingredients. New resolved formula (same logic we use in Mix Calculator). Fix: Security headers on all API responses (hardening) Add: New mix button available on the Mix Editor. Add: New ingredient button available on the Ingredient Editor
98 lines
2.8 KiB
Python
98 lines
2.8 KiB
Python
"""Recording and reading editor change history.
|
|
|
|
The Mix Editor and Ingredients Editor write a row here on every create/edit so
|
|
each mix and ingredient carries an auditable history (who changed what, when).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import AuthSession
|
|
from app.models.change_event import EditorChangeEvent
|
|
|
|
# Entity types — these match the History buttons on the two editors.
|
|
ENTITY_MIX = "mix"
|
|
ENTITY_INGREDIENT = "ingredient"
|
|
|
|
|
|
def _stringify(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bool):
|
|
return "Yes" if value else "No"
|
|
if isinstance(value, float):
|
|
# Trim trailing zeros so 12.50 reads as 12.5 and 12.0 as 12.
|
|
text = f"{value:.4f}".rstrip("0").rstrip(".")
|
|
return text or "0"
|
|
return str(value)
|
|
|
|
|
|
def diff_fields(before: dict[str, Any], after: dict[str, Any], labels: dict[str, str]) -> list[dict[str, Any]]:
|
|
"""Build a list of ``{field, label, before, after}`` deltas for changed fields.
|
|
|
|
Only keys present in ``labels`` are considered, and only those whose value
|
|
actually changed are emitted.
|
|
"""
|
|
deltas: list[dict[str, Any]] = []
|
|
for field, label in labels.items():
|
|
if field not in after:
|
|
continue
|
|
old = before.get(field)
|
|
new = after.get(field)
|
|
if old == new:
|
|
continue
|
|
deltas.append({"field": field, "label": label, "before": _stringify(old), "after": _stringify(new)})
|
|
return deltas
|
|
|
|
|
|
def record_change(
|
|
db: Session,
|
|
*,
|
|
session: AuthSession,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
action: str,
|
|
summary: str,
|
|
changes: list[dict[str, Any]] | None = None,
|
|
) -> None:
|
|
"""Append a change event. Caller is responsible for committing the session."""
|
|
db.add(
|
|
EditorChangeEvent(
|
|
tenant_id=session.tenant_id or "",
|
|
entity_type=entity_type,
|
|
entity_id=entity_id,
|
|
action=action,
|
|
actor_name=session.name or session.email or "Unknown",
|
|
actor_email=session.email or "",
|
|
actor_role=session.client_role or session.role,
|
|
summary=summary,
|
|
changes=changes or [],
|
|
)
|
|
)
|
|
|
|
|
|
def list_changes(
|
|
db: Session,
|
|
*,
|
|
tenant_id: str,
|
|
entity_type: str,
|
|
entity_id: int,
|
|
limit: int = 200,
|
|
) -> list[EditorChangeEvent]:
|
|
return list(
|
|
db.scalars(
|
|
select(EditorChangeEvent)
|
|
.where(
|
|
EditorChangeEvent.tenant_id == tenant_id,
|
|
EditorChangeEvent.entity_type == entity_type,
|
|
EditorChangeEvent.entity_id == entity_id,
|
|
)
|
|
.order_by(EditorChangeEvent.created_at.desc(), EditorChangeEvent.id.desc())
|
|
.limit(limit)
|
|
).all()
|
|
)
|