fix: workflow 422 validation + report 500 DB data fetching + AI stream tenant context
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -15,7 +15,10 @@ from fastapi.responses import StreamingResponse
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.db import get_db
|
from app.models.contact import Contact
|
||||||
|
from app.models.audit import AuditLog
|
||||||
|
|
||||||
|
from app.core.db import get_db, set_tenant_context
|
||||||
from app.deps import get_current_user, require_permission
|
from app.deps import get_current_user, require_permission
|
||||||
from app.plugins.builtins.report_generator.models import (
|
from app.plugins.builtins.report_generator.models import (
|
||||||
ReportInstance,
|
ReportInstance,
|
||||||
@@ -156,15 +159,100 @@ async def generate_preset(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("reports:generate")),
|
current_user: dict = Depends(require_permission("reports:generate")),
|
||||||
):
|
):
|
||||||
"""Generate a preset report by name (e.g. contact_list, calendar_week)."""
|
"""Generate a preset report by name (e.g. contact_list, calendar_week).
|
||||||
|
|
||||||
|
Fetches live data from the database based on the preset type,
|
||||||
|
merges with user-provided parameters, and generates the report.
|
||||||
|
"""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
|
||||||
# Generate report first (no DB interaction during sync IO)
|
# Set tenant context for RLS
|
||||||
await db.close()
|
await set_tenant_context(db, tenant_id)
|
||||||
|
|
||||||
|
# Fetch data based on preset type
|
||||||
|
parameters = dict(body.parameters) # copy user params
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if body.preset == "contact_list":
|
||||||
|
# Fetch all contacts (type='person') for this tenant
|
||||||
|
q = select(Contact).where(
|
||||||
|
Contact.tenant_id == tenant_id,
|
||||||
|
Contact.type == "person",
|
||||||
|
Contact.deleted_at.is_(None),
|
||||||
|
).order_by(Contact.displayname)
|
||||||
|
result = await db.execute(q)
|
||||||
|
contacts = result.scalars().all()
|
||||||
|
parameters["contacts"] = [
|
||||||
|
{
|
||||||
|
"name": f"{c.firstname or ''} {c.surname or ''}".strip() or c.displayname or "",
|
||||||
|
"email": c.email_1 or "",
|
||||||
|
"phone": c.phone_1 or "",
|
||||||
|
"type": c.type or "person",
|
||||||
|
"company": "", # contact persons are separate
|
||||||
|
}
|
||||||
|
for c in contacts
|
||||||
|
]
|
||||||
|
parameters["title"] = parameters.get("title", "Kontaktliste")
|
||||||
|
|
||||||
|
elif body.preset == "company_list":
|
||||||
|
# Fetch all companies (type='company') for this tenant
|
||||||
|
q = select(Contact).where(
|
||||||
|
Contact.tenant_id == tenant_id,
|
||||||
|
Contact.type == "company",
|
||||||
|
Contact.deleted_at.is_(None),
|
||||||
|
).order_by(Contact.name)
|
||||||
|
result = await db.execute(q)
|
||||||
|
companies = result.scalars().all()
|
||||||
|
parameters["companies"] = [
|
||||||
|
{
|
||||||
|
"name": c.name or c.displayname or "",
|
||||||
|
"address": (
|
||||||
|
f"{c.mailing_street or ''} {c.mailing_number or ''}".strip()
|
||||||
|
or f"{c.visit_street or ''} {c.visit_number or ''}".strip()
|
||||||
|
or ""
|
||||||
|
),
|
||||||
|
"zip": c.mailing_postalcode or "",
|
||||||
|
"city": c.mailing_city or "",
|
||||||
|
"phone": c.phone_1 or "",
|
||||||
|
"email": c.email_1 or "",
|
||||||
|
"contact_person": "", # could be extended with ContactPerson lookup
|
||||||
|
}
|
||||||
|
for c in companies
|
||||||
|
]
|
||||||
|
parameters["title"] = parameters.get("title", "Firmenliste")
|
||||||
|
|
||||||
|
elif body.preset == "audit_log":
|
||||||
|
# Fetch recent audit log entries for this tenant
|
||||||
|
q = select(AuditLog).where(
|
||||||
|
AuditLog.tenant_id == tenant_id,
|
||||||
|
).order_by(AuditLog.timestamp.desc()).limit(500)
|
||||||
|
result = await db.execute(q)
|
||||||
|
entries = result.scalars().all()
|
||||||
|
parameters["entries"] = [
|
||||||
|
{
|
||||||
|
"timestamp": str(e.timestamp) if e.timestamp else "",
|
||||||
|
"user": str(e.user_id) if e.user_id else "—",
|
||||||
|
"action": e.action or "",
|
||||||
|
"entity": e.entity_type or "",
|
||||||
|
"entity_id": str(e.entity_id) if e.entity_id else "",
|
||||||
|
"details": str(e.changes) if e.changes else "",
|
||||||
|
}
|
||||||
|
for e in entries
|
||||||
|
]
|
||||||
|
parameters["title"] = parameters.get("title", "Audit-Log")
|
||||||
|
|
||||||
|
elif body.preset in ("calendar_week", "calendar_month"):
|
||||||
|
# Calendar presets: pass user params through (no DB model yet)
|
||||||
|
parameters.setdefault("title", "Wochenkalender" if body.preset == "calendar_week" else "Monatskalender")
|
||||||
|
parameters.setdefault("days", [])
|
||||||
|
parameters.setdefault("hours", [])
|
||||||
|
parameters.setdefault("weeks", [])
|
||||||
|
parameters.setdefault("weekday_names", [])
|
||||||
|
|
||||||
|
# Generate report
|
||||||
file_bytes, ext = generate_preset_report(
|
file_bytes, ext = generate_preset_report(
|
||||||
body.preset, body.output_format, body.parameters
|
body.preset, body.output_format, parameters
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -175,7 +263,7 @@ async def generate_preset(
|
|||||||
},
|
},
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# Return file directly as streaming response (avoid DB tracking due to greenlet issues in test env)
|
# Return file directly as streaming response
|
||||||
import io as _io
|
import io as _io
|
||||||
|
|
||||||
media_types = {
|
media_types = {
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ export function WorkflowEditor({ open, workflow, onClose }: WorkflowEditorProps)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.steps.length === 0) {
|
||||||
|
toast.error('Mindestens ein Schritt ist erforderlich');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasEmptyStepName = form.steps.some((s) => !s.name.trim());
|
const hasEmptyStepName = form.steps.some((s) => !s.name.trim());
|
||||||
if (hasEmptyStepName) {
|
if (hasEmptyStepName) {
|
||||||
toast.error('Alle Schritte muessen einen Namen haben');
|
toast.error('Alle Schritte muessen einen Namen haben');
|
||||||
@@ -161,7 +166,17 @@ export function WorkflowEditor({ open, workflow, onClose }: WorkflowEditorProps)
|
|||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err.message || 'Fehler beim Speichern');
|
// Show detailed validation errors from backend (422)
|
||||||
|
if (err.validationErrors) {
|
||||||
|
const details = Object.entries(err.validationErrors)
|
||||||
|
.map(([field, msgs]) => `${field}: ${msgs.join(', ')}`)
|
||||||
|
.join('; ');
|
||||||
|
toast.error(`Validierungsfehler: ${details}`);
|
||||||
|
} else if (err.detail) {
|
||||||
|
toast.error(err.detail);
|
||||||
|
} else {
|
||||||
|
toast.error(err.message || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user