Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06682ef8f2 | |||
| d69c478fed | |||
| 5f551331aa | |||
| 68dc2f71c6 | |||
| 736dafd1e1 |
+106
-25
@@ -285,66 +285,147 @@ class DIN5008AuditPDF:
|
|||||||
spaceAfter=8,
|
spaceAfter=8,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
# Build table data with wrapped cells for better readability on A4 pages
|
||||||
|
cell_style = ParagraphStyle(
|
||||||
|
'EventCell',
|
||||||
|
parent=styles['Normal'],
|
||||||
|
fontName='Helvetica',
|
||||||
|
fontSize=8,
|
||||||
|
leading=9.5,
|
||||||
|
textColor=HexColor('#1f2937'),
|
||||||
|
alignment=TA_LEFT,
|
||||||
|
)
|
||||||
|
hash_style = ParagraphStyle(
|
||||||
|
'EventHashCell',
|
||||||
|
parent=cell_style,
|
||||||
|
fontName='Courier',
|
||||||
|
fontSize=7.2,
|
||||||
|
leading=9,
|
||||||
|
wordWrap='CJK',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _safe(value):
|
||||||
|
return str(value or '').replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
|
||||||
|
def _fmt_ts(value):
|
||||||
|
text = _safe(value)
|
||||||
|
if len(text) >= 19 and text[4] == '-' and text[7] == '-':
|
||||||
|
# Convert 2026-05-10 16:19:07... -> 10.05.2026 16:19
|
||||||
|
return f"{text[8:10]}.{text[5:7]}.{text[0:4]} {text[11:16]}"
|
||||||
|
return text[:16]
|
||||||
|
|
||||||
|
def _fmt_event(value):
|
||||||
|
text = _safe(value).replace('_', ' ').strip()
|
||||||
|
return text[:60]
|
||||||
|
|
||||||
|
def _chunk_text(value, chunk=4, sep=' '):
|
||||||
|
text = _safe(value)
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
return sep.join(text[i:i + chunk] for i in range(0, len(text), chunk))
|
||||||
|
|
||||||
|
def _fmt_ip(value):
|
||||||
|
text = _safe(value)
|
||||||
|
# Keep IPv4 intact. For long IPv6 values insert only one line break in the middle.
|
||||||
|
if ':' in text and len(text) > 24:
|
||||||
|
parts = text.split(':')
|
||||||
|
if len(parts) > 4:
|
||||||
|
return ':'.join(parts[:4]) + ':<br/>' + ':'.join(parts[4:])
|
||||||
|
return text
|
||||||
|
|
||||||
# Build table data
|
# Build table data
|
||||||
if self.export_type == 'quick':
|
if self.export_type == 'quick':
|
||||||
# Quick-Check: Minimal columns
|
# Quick-Check: Minimal columns
|
||||||
table_data = [
|
table_data = [
|
||||||
['Index', 'Zeit', 'Ereignis', 'Benutzer', 'Hashwert (gekürzt)'],
|
['Idx', 'Zeit', 'Ereignis', 'Benutzer', 'Hash (gekürzt)'],
|
||||||
]
|
]
|
||||||
|
|
||||||
for row in audit_rows[:20]: # Limit to 20 rows for quick check
|
for row in audit_rows[:20]: # Limit to 20 rows for quick check
|
||||||
chain_idx = str(row.get('chain_index', ''))
|
chain_idx = _safe(row.get('chain_index', ''))
|
||||||
timestamp = str(row.get('timestamp') or row.get('created_at', ''))[:16]
|
timestamp = _fmt_ts(row.get('timestamp') or row.get('created_at', ''))
|
||||||
event_type = str(row.get('event_type', ''))
|
event_type = _fmt_event(row.get('event_type', ''))
|
||||||
actor = str(row.get('actor', ''))
|
actor = _safe(row.get('actor', ''))
|
||||||
entry_hash = str(row.get('entry_hash', ''))[:12] + '...'
|
entry_hash = _chunk_text(_safe(row.get('entry_hash', ''))[:20], chunk=4)
|
||||||
|
if entry_hash:
|
||||||
|
entry_hash += ' ...'
|
||||||
|
|
||||||
table_data.append([chain_idx, timestamp, event_type, actor, entry_hash])
|
table_data.append([
|
||||||
|
Paragraph(chain_idx, cell_style),
|
||||||
|
Paragraph(timestamp, cell_style),
|
||||||
|
Paragraph(event_type, cell_style),
|
||||||
|
Paragraph(actor, cell_style),
|
||||||
|
Paragraph(entry_hash, hash_style),
|
||||||
|
])
|
||||||
|
|
||||||
colWidths = [1.2*cm, 1.8*cm, 2.2*cm, 2*cm, 3.8*cm]
|
colWidths = [1.1*cm, 2.7*cm, 4.8*cm, 3.1*cm, 4.9*cm]
|
||||||
else:
|
else:
|
||||||
# Official Report: Full columns
|
# Official Report: Full columns
|
||||||
table_data = [
|
table_data = [
|
||||||
['Idx', 'Zeitstempel', 'Ereignistyp', 'Benutzer', 'Quelle', 'IP-Adresse', 'Hashwert'],
|
['Idx', 'Zeit', 'Ereignis', 'Benutzer', 'Quelle', 'IP', 'Hash'],
|
||||||
]
|
]
|
||||||
|
|
||||||
for row in audit_rows:
|
for row in audit_rows:
|
||||||
chain_idx = str(row.get('chain_index', ''))
|
chain_idx = _safe(row.get('chain_index', ''))
|
||||||
timestamp = str(row.get('timestamp') or row.get('created_at', ''))[:19]
|
timestamp = _fmt_ts(row.get('timestamp') or row.get('created_at', ''))
|
||||||
event_type = str(row.get('event_type', ''))
|
event_type = _fmt_event(row.get('event_type', ''))
|
||||||
actor = str(row.get('actor', ''))
|
actor = _safe(row.get('actor', ''))
|
||||||
source = str(row.get('source', 'System'))[:15]
|
source = _safe(row.get('source', 'System'))
|
||||||
ip = str(row.get('ip', ''))
|
ip = _fmt_ip(row.get('ip', ''))
|
||||||
entry_hash = str(row.get('entry_hash', ''))[:16]
|
entry_hash = _chunk_text(_safe(row.get('entry_hash', ''))[:40], chunk=8)
|
||||||
|
|
||||||
table_data.append([chain_idx, timestamp, event_type, actor, source, ip, entry_hash])
|
table_data.append([
|
||||||
|
Paragraph(chain_idx, cell_style),
|
||||||
|
Paragraph(timestamp, cell_style),
|
||||||
|
Paragraph(event_type, cell_style),
|
||||||
|
Paragraph(actor, cell_style),
|
||||||
|
Paragraph(source, cell_style),
|
||||||
|
Paragraph(ip, cell_style),
|
||||||
|
Paragraph(entry_hash, hash_style),
|
||||||
|
])
|
||||||
|
|
||||||
colWidths = [0.8*cm, 1.8*cm, 1.5*cm, 1.5*cm, 1.2*cm, 1.5*cm, 2.2*cm]
|
# Give IP and hash columns significantly more room for readability.
|
||||||
|
colWidths = [0.9*cm, 2.4*cm, 2.8*cm, 2.0*cm, 1.4*cm, 3.3*cm, 4.2*cm]
|
||||||
|
|
||||||
# Create table
|
# Create table
|
||||||
events_table = Table(table_data, colWidths=colWidths)
|
events_table = Table(table_data, colWidths=colWidths, repeatRows=1)
|
||||||
events_table.setStyle(TableStyle([
|
events_table.setStyle(TableStyle([
|
||||||
# Header styling
|
# Header styling
|
||||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#2c3e50')),
|
('BACKGROUND', (0, 0), (-1, 0), HexColor('#2c3e50')),
|
||||||
('TEXTCOLOR', (0, 0), (-1, 0), HexColor('#ffffff')),
|
('TEXTCOLOR', (0, 0), (-1, 0), HexColor('#ffffff')),
|
||||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||||
('FONTSIZE', (0, 0), (-1, 0), 8),
|
('FONTSIZE', (0, 0), (-1, 0), 8.5),
|
||||||
|
('TOPPADDING', (0, 0), (-1, 0), 6),
|
||||||
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
|
||||||
|
|
||||||
# Body styling
|
# Body styling
|
||||||
('FONTSIZE', (0, 1), (-1, -1), 7),
|
('FONTSIZE', (0, 1), (-1, -1), 8),
|
||||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||||
('GRID', (0, 0), (-1, -1), 0.5, HexColor('#bdc3c7')),
|
('GRID', (0, 0), (-1, -1), 0.5, HexColor('#bdc3c7')),
|
||||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [HexColor('#ecf0f1'), HexColor('#ffffff')]),
|
('ROWBACKGROUNDS', (0, 1), (-1, -1), [HexColor('#ecf0f1'), HexColor('#ffffff')]),
|
||||||
('TOPPADDING', (0, 1), (-1, -1), 4),
|
('TOPPADDING', (0, 1), (-1, -1), 5),
|
||||||
('BOTTOMPADDING', (0, 1), (-1, -1), 4),
|
('BOTTOMPADDING', (0, 1), (-1, -1), 5),
|
||||||
('LEFTPADDING', (0, 1), (-1, -1), 3),
|
('LEFTPADDING', (0, 1), (-1, -1), 4),
|
||||||
('RIGHTPADDING', (0, 1), (-1, -1), 3),
|
('RIGHTPADDING', (0, 1), (-1, -1), 4),
|
||||||
|
('ALIGN', (0, 1), (0, -1), 'CENTER'),
|
||||||
|
('ALIGN', (1, 1), (1, -1), 'CENTER'),
|
||||||
|
('ALIGN', (-1, 1), (-1, -1), 'LEFT'),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
story.append(events_table)
|
story.append(events_table)
|
||||||
|
story.append(Paragraph(
|
||||||
|
"Hinweis: Zeitangaben sind auf Minuten gerundet; Hashwerte werden aus Platzgründen gekürzt dargestellt.",
|
||||||
|
ParagraphStyle(
|
||||||
|
'TableHint',
|
||||||
|
parent=styles['Normal'],
|
||||||
|
fontSize=7.5,
|
||||||
|
leading=9,
|
||||||
|
textColor=HexColor('#6b7280'),
|
||||||
|
alignment=TA_LEFT,
|
||||||
|
spaceBefore=4,
|
||||||
|
)
|
||||||
|
))
|
||||||
story.append(Spacer(1, 0.2 * cm))
|
story.append(Spacer(1, 0.2 * cm))
|
||||||
|
|
||||||
def _add_mismatches(self, story, mismatches):
|
def _add_mismatches(self, story, mismatches):
|
||||||
|
|||||||
@@ -1127,6 +1127,7 @@
|
|||||||
{% if current_permissions.pages.get('manage_locations', True) %}
|
{% if current_permissions.pages.get('manage_locations', True) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
||||||
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1252,6 +1253,7 @@
|
|||||||
{% if student_cards_module_enabled %}
|
{% if student_cards_module_enabled %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||||||
<li><h6 class="dropdown-header">System</h6></li>
|
<li><h6 class="dropdown-header">System</h6></li>
|
||||||
@@ -1529,6 +1531,7 @@
|
|||||||
<option value="Defekte Items"></option>
|
<option value="Defekte Items"></option>
|
||||||
<option value="Filter verwalten"></option>
|
<option value="Filter verwalten"></option>
|
||||||
<option value="Orte verwalten"></option>
|
<option value="Orte verwalten"></option>
|
||||||
|
<option value="Schulstammdaten"></option>
|
||||||
<option value="Audit Dashboard"></option>
|
<option value="Audit Dashboard"></option>
|
||||||
<option value="Logs"></option>
|
<option value="Logs"></option>
|
||||||
<option value="Benutzer verwalten"></option>
|
<option value="Benutzer verwalten"></option>
|
||||||
@@ -1598,6 +1601,7 @@
|
|||||||
{ label: 'Defekte Items', keywords: ['defekte items', 'defekt', 'schaden'], url: {{ url_for('admin_damaged_items')|tojson }} },
|
{ label: 'Defekte Items', keywords: ['defekte items', 'defekt', 'schaden'], url: {{ url_for('admin_damaged_items')|tojson }} },
|
||||||
{ label: 'Filter verwalten', keywords: ['filter verwalten', 'filter'], url: {{ url_for('manage_filters')|tojson }} },
|
{ label: 'Filter verwalten', keywords: ['filter verwalten', 'filter'], url: {{ url_for('manage_filters')|tojson }} },
|
||||||
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
|
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
|
||||||
|
{ label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} },
|
||||||
{ label: 'Audit Dashboard', keywords: ['audit', 'audit dashboard'], url: {{ url_for('admin_audit_dashboard')|tojson }} },
|
{ label: 'Audit Dashboard', keywords: ['audit', 'audit dashboard'], url: {{ url_for('admin_audit_dashboard')|tojson }} },
|
||||||
{ label: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
|
{ label: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
|
||||||
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
|
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
|
||||||
|
|||||||
@@ -2345,19 +2345,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="admin-school-banner" style="display:flex; flex-wrap:wrap; justify-content:space-between; gap:12px; align-items:center; margin:0 0 14px; padding:14px 16px; border:1px solid #dbe3ee; border-radius:12px; background: var(--ui-surface); box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);">
|
|
||||||
<div>
|
|
||||||
<div style="font-weight:700; font-size:1rem; color:var(--ui-title);">Schulstammdaten</div>
|
|
||||||
<div style="font-size:0.92rem; color:var(--ui-text-muted);">
|
|
||||||
{{ school_info.name if school_info else 'Schulname nicht konfiguriert' }}
|
|
||||||
{% if school_info and school_info.school_number %} | Schulnummer: {{ school_info.school_number }}{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
|
||||||
<a href="{{ url_for('admin_school_settings') }}" class="btn btn-primary">Schulstammdaten bearbeiten</a>
|
|
||||||
<a href="{{ url_for('admin_audit') }}" class="btn btn-outline-secondary">Audit-Export öffnen</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
|
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
|
||||||
<div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
|
<div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
|
||||||
<div class="bulk-delete-content">
|
<div class="bulk-delete-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user