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"}:
|
if sections == {"zusammenfassung"}:
|
||||||
return "Zusammenfassung"
|
return "Zusammenfassung"
|
||||||
if "timeline" in sections:
|
if "timeline" in sections:
|
||||||
return "Vollstaendiger Bericht"
|
return "Vollständiger Bericht"
|
||||||
return "Lagebericht"
|
return "Lagebericht"
|
||||||
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollstaendiger Bericht"}.get(
|
return {"summary": "Zusammenfassung", "report": "Lagebericht", "full": "Vollständiger Bericht"}.get(
|
||||||
scope, "Lagebericht"
|
scope, "Lagebericht"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -434,6 +434,7 @@ def _build_export_metadata(
|
|||||||
sections: set[str] | None,
|
sections: set[str] | None,
|
||||||
organization_name: str | None,
|
organization_name: str | None,
|
||||||
top_locations: list[str] | None,
|
top_locations: list[str] | None,
|
||||||
|
snapshot_count: int = 0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties)."""
|
"""Einheitlicher Metadaten-Dict fuer PDF (HTML-Meta-Tags) und DOCX (core_properties)."""
|
||||||
is_research = incident.get("type") == "research"
|
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')}"
|
identifier = f"urn:aegissight:incident:{incident.get('id', '0')}:{now.strftime('%Y%m%dT%H%M%S')}"
|
||||||
rights = (
|
rights = (
|
||||||
"Vertrauliche Lageanalyse — AegisSight Monitor. "
|
"Vertrauliche Lageanalyse — AegisSight Monitor. "
|
||||||
"Weitergabe nur an autorisierte Empfaenger."
|
"Weitergabe nur an autorisierte Empfänger."
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -546,6 +547,7 @@ def _build_export_metadata(
|
|||||||
"identifier": identifier,
|
"identifier": identifier,
|
||||||
"rights": rights,
|
"rights": rights,
|
||||||
"doc_type": "Report",
|
"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
|
# xmpMM: Document- und Instance-ID fuer DMS-Versionierung
|
||||||
xmp["xmpMM:DocumentID"] = doc_uuid
|
xmp["xmpMM:DocumentID"] = doc_uuid
|
||||||
xmp["xmpMM:InstanceID"] = instance_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()
|
buf_out = io.BytesIO()
|
||||||
pdf.save(buf_out)
|
pdf.save(buf_out)
|
||||||
@@ -639,6 +653,7 @@ async def generate_pdf(
|
|||||||
sections: set[str] | None = None,
|
sections: set[str] | None = None,
|
||||||
organization_name: str | None = None,
|
organization_name: str | None = None,
|
||||||
top_locations: list[str] | None = None,
|
top_locations: list[str] | None = None,
|
||||||
|
snapshot_count: int = 0,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""PDF-Report via WeasyPrint generieren."""
|
"""PDF-Report via WeasyPrint generieren."""
|
||||||
# Sections aus scope ableiten wenn nicht explizit angegeben
|
# Sections aus scope ableiten wenn nicht explizit angegeben
|
||||||
@@ -670,7 +685,7 @@ async def generate_pdf(
|
|||||||
|
|
||||||
meta = _build_export_metadata(
|
meta = _build_export_metadata(
|
||||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
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)))
|
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||||
@@ -721,6 +736,7 @@ async def generate_docx(
|
|||||||
sections: set[str] | None = None,
|
sections: set[str] | None = None,
|
||||||
organization_name: str | None = None,
|
organization_name: str | None = None,
|
||||||
top_locations: list[str] | None = None,
|
top_locations: list[str] | None = None,
|
||||||
|
snapshot_count: int = 0,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Word-Report via python-docx generieren."""
|
"""Word-Report via python-docx generieren."""
|
||||||
doc = Document()
|
doc = Document()
|
||||||
@@ -738,7 +754,7 @@ async def generate_docx(
|
|||||||
is_research = incident.get("type") == "research"
|
is_research = incident.get("type") == "research"
|
||||||
all_sources = _prepare_sources(incident)
|
all_sources = _prepare_sources(incident)
|
||||||
zusammenfassung_text = executive_summary_text
|
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_title = "Zusammenfassung"
|
||||||
zusammenfassung_lines: list[str] = []
|
zusammenfassung_lines: list[str] = []
|
||||||
|
|
||||||
@@ -751,7 +767,7 @@ async def generate_docx(
|
|||||||
|
|
||||||
meta = _build_export_metadata(
|
meta = _build_export_metadata(
|
||||||
incident, articles, fact_checks, all_sources, creator, scope, sections,
|
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)
|
# 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"]]
|
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
|
# Artikel
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT * FROM articles WHERE incident_id = ? ORDER BY collected_at DESC",
|
"SELECT * FROM articles WHERE incident_id = ? ORDER BY collected_at DESC",
|
||||||
@@ -1063,6 +1070,7 @@ async def export_incident(
|
|||||||
sections=sections_set,
|
sections=sections_set,
|
||||||
organization_name=organization_name,
|
organization_name=organization_name,
|
||||||
top_locations=top_locations,
|
top_locations=top_locations,
|
||||||
|
snapshot_count=snapshot_count,
|
||||||
)
|
)
|
||||||
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.pdf"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
@@ -1076,6 +1084,7 @@ async def export_incident(
|
|||||||
sections=sections_set,
|
sections=sections_set,
|
||||||
organization_name=organization_name,
|
organization_name=organization_name,
|
||||||
top_locations=top_locations,
|
top_locations=top_locations,
|
||||||
|
snapshot_count=snapshot_count,
|
||||||
)
|
)
|
||||||
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
filename = f"{slug}_{scope_labels_key}_{date_str}.docx"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren