Export-Metadaten: Umlaut-Fix, xmpMM:VersionID + History
- dc:rights und xmpRights:UsageTerms: Empfaenger -> Empfänger (echte Umlaute) - Scope-Labels: Vollstaendiger Bericht -> Vollständiger Bericht (zwei Stellen) - DOCX-Fallback-Text: verfuegbar -> verfügbar - xmpMM:VersionID: Snapshot-Count der Lage (Proxy fuer Berichts-Revision). Router laedt COUNT(*) FROM incident_snapshots und reicht es durch. - xmpMM:History: Audit-Event pro Export als rdf:Seq-Eintrag mit Timestamp, softwareAgent, InstanceID, Scope und Version. Single-Event-Format aus pragmatischem Grund (pikepdf-API unterstuetzt keine nativen stEvt- Strukturen; Raw-XML-Injection waere dafuer noetig).
Dieser Commit ist enthalten in:
@@ -417,9 +417,9 @@ def _slug_scope_label(scope: str, sections: set[str] | None) -> str:
|
||||
if sections == {"zusammenfassung"}:
|
||||
return "Zusammenfassung"
|
||||
if "timeline" in sections:
|
||||
return "Vollstaendiger Bericht"
|
||||
return "Vollständiger Bericht"
|
||||
return "Lagebericht"
|
||||
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollstaendiger Bericht"}.get(
|
||||
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollständiger Bericht"}.get(
|
||||
scope, "Lagebericht"
|
||||
)
|
||||
|
||||
@@ -434,6 +434,7 @@ def _build_export_metadata(
|
||||
sections: set[str] | None,
|
||||
organization_name: str | None,
|
||||
top_locations: list[str] | None,
|
||||
snapshot_count: int = 0,
|
||||
) -> dict:
|
||||
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties)."""
|
||||
is_research = incident.get("type") == "research"
|
||||
@@ -522,7 +523,7 @@ def _build_export_metadata(
|
||||
identifier = f"urn:aegissight:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
|
||||
rights = (
|
||||
"Vertrauliche Lageanalyse — AegisSight Monitor. "
|
||||
"Weitergabe nur an autorisierte Empfaenger."
|
||||
"Weitergabe nur an autorisierte Empfänger."
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -546,6 +547,7 @@ def _build_export_metadata(
|
||||
"identifier": identifier,
|
||||
"rights": rights,
|
||||
"doc_type": "Report",
|
||||
"version_id": str(max(1, snapshot_count)),
|
||||
}
|
||||
|
||||
|
||||
@@ -624,6 +626,18 @@ def _enrich_pdf_metadata(pdf_bytes: bytes, meta: dict) -> bytes:
|
||||
# xmpMM: Document- und Instance-ID fuer DMS-Versionierung
|
||||
xmp["xmpMM:DocumentID"] = doc_uuid
|
||||
xmp["xmpMM:InstanceID"] = instance_uuid
|
||||
xmp["xmpMM:VersionID"] = meta.get("version_id", "1")
|
||||
|
||||
# xmpMM:History — Audit-Event fuer diesen Export (einzeiliger Eintrag je Seq-Item)
|
||||
history_when = (modified or datetime.now(TIMEZONE)).strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
history_entry = (
|
||||
f"action=published; when={history_when}; "
|
||||
f"softwareAgent={meta.get('creator_app', 'AegisSight Monitor')}; "
|
||||
f"instanceID={instance_uuid}; "
|
||||
f"scope={meta.get('scope_label', '')}; "
|
||||
f"version={meta.get('version_id', '1')}"
|
||||
)
|
||||
xmp["xmpMM:History"] = [history_entry]
|
||||
|
||||
buf_out = io.BytesIO()
|
||||
pdf.save(buf_out)
|
||||
@@ -639,6 +653,7 @@ async def generate_pdf(
|
||||
sections: set[str] | None = None,
|
||||
organization_name: str | None = None,
|
||||
top_locations: list[str] | None = None,
|
||||
snapshot_count: int = 0,
|
||||
) -> bytes:
|
||||
"""PDF-Report via WeasyPrint generieren."""
|
||||
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||
@@ -670,7 +685,7 @@ async def generate_pdf(
|
||||
|
||||
meta = _build_export_metadata(
|
||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
||||
organization_name, top_locations,
|
||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||
)
|
||||
|
||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||
@@ -721,6 +736,7 @@ async def generate_docx(
|
||||
sections: set[str] | None = None,
|
||||
organization_name: str | None = None,
|
||||
top_locations: list[str] | None = None,
|
||||
snapshot_count: int = 0,
|
||||
) -> bytes:
|
||||
"""Word-Report via python-docx generieren."""
|
||||
doc = Document()
|
||||
@@ -738,7 +754,7 @@ async def generate_docx(
|
||||
is_research = incident.get("type") == "research"
|
||||
all_sources = _prepare_sources(incident)
|
||||
zusammenfassung_text = executive_summary_text
|
||||
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfuegbar."
|
||||
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
|
||||
zusammenfassung_title = "Zusammenfassung"
|
||||
zusammenfassung_lines: list[str] = []
|
||||
|
||||
@@ -751,7 +767,7 @@ async def generate_docx(
|
||||
|
||||
meta = _build_export_metadata(
|
||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
||||
organization_name, top_locations,
|
||||
organization_name, top_locations, snapshot_count=snapshot_count,
|
||||
)
|
||||
|
||||
# Dateimetadaten setzen (sichtbar in Explorer/Finder, DMS-Systemen)
|
||||
|
||||
@@ -1009,6 +1009,13 @@ async def export_incident(
|
||||
)
|
||||
top_locations = [r["location_name"] for r in await cursor.fetchall() if r["location_name"]]
|
||||
|
||||
# Snapshot-Count (als xmpMM:VersionID im PDF)
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) AS cnt FROM incident_snapshots WHERE incident_id = ?",
|
||||
(incident_id,),
|
||||
)
|
||||
snapshot_count = (await cursor.fetchone())["cnt"] or 0
|
||||
|
||||
# Artikel
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM articles WHERE incident_id = ? ORDER BY collected_at DESC",
|
||||
@@ -1063,6 +1070,7 @@ async def export_incident(
|
||||
sections=sections_set,
|
||||
organization_name=organization_name,
|
||||
top_locations=top_locations,
|
||||
snapshot_count=snapshot_count,
|
||||
)
|
||||
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
||||
return StreamingResponse(
|
||||
@@ -1076,6 +1084,7 @@ async def export_incident(
|
||||
sections=sections_set,
|
||||
organization_name=organization_name,
|
||||
top_locations=top_locations,
|
||||
snapshot_count=snapshot_count,
|
||||
)
|
||||
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
||||
return StreamingResponse(
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren