Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68dc2f71c6 | |||
| 736dafd1e1 | |||
| 123ad6f0a7 | |||
| c953f22ab4 | |||
| b1fabfbfcf |
+86
@@ -636,6 +636,51 @@ def _get_school_info_for_export():
|
||||
}
|
||||
|
||||
|
||||
def _save_school_logo_upload(upload_file, tenant_id=None, tenant_db=None):
|
||||
"""Save an uploaded school logo to the shared upload folder with a tenant-specific filename."""
|
||||
if not upload_file or not getattr(upload_file, 'filename', ''):
|
||||
return None
|
||||
|
||||
is_allowed, error_message = allowed_file(upload_file.filename, upload_file, max_size_mb=cfg.IMAGE_MAX_UPLOAD_MB)
|
||||
if not is_allowed:
|
||||
raise ValueError(error_message)
|
||||
|
||||
safe_tenant = re.sub(r'[^a-zA-Z0-9_\-]+', '_', str(tenant_id or tenant_db or 'default').strip())
|
||||
safe_tenant = safe_tenant.strip('_') or 'default'
|
||||
_, original_ext = os.path.splitext(secure_filename(upload_file.filename))
|
||||
extension = (original_ext or '').lower()
|
||||
if not extension:
|
||||
raise ValueError('Das Logo benötigt eine Dateiendung.')
|
||||
|
||||
logo_filename = f'school-logo-{safe_tenant}{extension}'
|
||||
logo_path = os.path.join(app.config['UPLOAD_FOLDER'], logo_filename)
|
||||
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
upload_file.save(logo_path)
|
||||
|
||||
# Generate a thumbnail for consistent navbar sizing
|
||||
try:
|
||||
thumb_filename = f'school-logo-{safe_tenant}-thumb.png'
|
||||
thumb_path = os.path.join(app.config['UPLOAD_FOLDER'], thumb_filename)
|
||||
|
||||
with Image.open(logo_path) as img:
|
||||
# Ensure RGBA for transparency and convert if needed
|
||||
if img.mode not in ('RGBA', 'RGB'):
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# Target max size: width up to 520px, height up to 114px (matches CSS constraints)
|
||||
max_thumb_size = (520, 114)
|
||||
img.thumbnail(max_thumb_size, Image.LANCZOS)
|
||||
|
||||
# Save as PNG for predictable rendering in web UI
|
||||
img.save(thumb_path, format='PNG', optimize=True)
|
||||
except Exception as e:
|
||||
app.logger.warning(f"Thumbnail generation failed for {logo_path}: {e}")
|
||||
thumb_filename = None
|
||||
|
||||
return (logo_filename, thumb_filename)
|
||||
|
||||
|
||||
def _parse_money_value(value):
|
||||
"""Parse a user-facing money value into a float when possible."""
|
||||
if value is None:
|
||||
@@ -1207,6 +1252,7 @@ def inject_version():
|
||||
'current_permissions': current_permissions,
|
||||
'current_tenant_db': current_tenant_db,
|
||||
'current_tenant_id': current_tenant_id,
|
||||
'school_info': _get_school_info_for_export(),
|
||||
'permission_action_options': PERMISSION_ACTION_OPTIONS,
|
||||
'permission_page_options': PERMISSION_PAGE_OPTIONS,
|
||||
'permission_presets': us.get_permission_preset_definitions(),
|
||||
@@ -9889,8 +9935,48 @@ def admin_school_settings():
|
||||
'school_number': sanitize_form_value(request.form.get('school_number')),
|
||||
'it_admin': sanitize_form_value(request.form.get('it_admin')),
|
||||
'logo_path': sanitize_form_value(request.form.get('logo_path')),
|
||||
'logo_thumb': sanitize_form_value(request.form.get('logo_thumb')),
|
||||
}
|
||||
|
||||
uploaded_logo = request.files.get('logo_upload')
|
||||
if uploaded_logo and getattr(uploaded_logo, 'filename', ''):
|
||||
try:
|
||||
previous_logo_path = current_school.get('logo_path') if isinstance(current_school, dict) else ''
|
||||
previous_thumb_path = current_school.get('logo_thumb') if isinstance(current_school, dict) else ''
|
||||
saved = _save_school_logo_upload(uploaded_logo, tenant_id=tenant_id, tenant_db=tenant_db)
|
||||
if isinstance(saved, (list, tuple)):
|
||||
saved_logo_filename, saved_thumb_filename = saved
|
||||
else:
|
||||
saved_logo_filename, saved_thumb_filename = saved, None
|
||||
|
||||
if saved_logo_filename:
|
||||
school_info['logo_path'] = saved_logo_filename
|
||||
school_info['logo_thumb'] = saved_thumb_filename or ''
|
||||
|
||||
# remove previous files if different
|
||||
if previous_logo_path and previous_logo_path != saved_logo_filename:
|
||||
previous_logo_file = os.path.join(app.config['UPLOAD_FOLDER'], previous_logo_path)
|
||||
if os.path.exists(previous_logo_file):
|
||||
try:
|
||||
os.remove(previous_logo_file)
|
||||
except Exception:
|
||||
pass
|
||||
if previous_thumb_path and previous_thumb_path != saved_thumb_filename:
|
||||
previous_thumb_file = os.path.join(app.config['UPLOAD_FOLDER'], previous_thumb_path)
|
||||
if os.path.exists(previous_thumb_file):
|
||||
try:
|
||||
os.remove(previous_thumb_file)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
flash(f'Logo konnte nicht hochgeladen werden: {exc}', 'error')
|
||||
return redirect(url_for('admin_school_settings'))
|
||||
|
||||
if not school_info.get('logo_path') and current_school.get('logo_path'):
|
||||
school_info['logo_path'] = current_school.get('logo_path', '')
|
||||
if not school_info.get('logo_thumb') and current_school.get('logo_thumb'):
|
||||
school_info['logo_thumb'] = current_school.get('logo_thumb', '')
|
||||
|
||||
missing_fields = [
|
||||
label for label, key in [
|
||||
('Schulname', 'name'),
|
||||
|
||||
+37
-2
@@ -7,6 +7,7 @@ PDF/A archiving standards for German schools and educational authorities.
|
||||
import io
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
import qrcode
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
@@ -109,6 +110,20 @@ class DIN5008AuditPDF:
|
||||
fontName='Helvetica',
|
||||
textColor=HexColor('#000000'),
|
||||
)
|
||||
|
||||
logo_path = self.school_info.get('logo_path', '')
|
||||
resolved_logo_path = None
|
||||
if logo_path:
|
||||
candidate_paths = [
|
||||
logo_path,
|
||||
os.path.join(cfg.UPLOAD_FOLDER, logo_path),
|
||||
os.path.join('/opt/Inventarsystem/Web/uploads', logo_path),
|
||||
os.path.join('/var/Inventarsystem/Web/uploads', logo_path),
|
||||
]
|
||||
for candidate_path in candidate_paths:
|
||||
if candidate_path and os.path.exists(candidate_path):
|
||||
resolved_logo_path = candidate_path
|
||||
break
|
||||
|
||||
school_info_text = f"""
|
||||
<b>{school_name}</b><br/>
|
||||
@@ -116,8 +131,28 @@ class DIN5008AuditPDF:
|
||||
{postal_code} {city}<br/>
|
||||
<i>Schulnummer: {school_number}</i>
|
||||
"""
|
||||
|
||||
story.append(Paragraph(school_info_text, header_style))
|
||||
|
||||
if resolved_logo_path:
|
||||
logo_image = Image(resolved_logo_path)
|
||||
try:
|
||||
logo_image._restrictSize(3.4 * cm, 3.4 * cm)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
school_table = Table(
|
||||
[[logo_image, Paragraph(school_info_text, header_style)]],
|
||||
colWidths=[3.8 * cm, self.USABLE_WIDTH - 3.8 * cm],
|
||||
)
|
||||
school_table.setStyle(TableStyle([
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 0),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
||||
]))
|
||||
story.append(school_table)
|
||||
else:
|
||||
story.append(Paragraph(school_info_text, header_style))
|
||||
|
||||
# Information block (right side simulation)
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
|
||||
@@ -61,6 +61,17 @@ DEFAULTS = {
|
||||
'logs': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'logs'),
|
||||
'deleted_archives': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'deleted_archives'),
|
||||
},
|
||||
'school': {
|
||||
'name': 'Schulname',
|
||||
'address': 'Schulstraße 1',
|
||||
'postal_code': '00000',
|
||||
'city': 'Ort',
|
||||
'school_number': '000000',
|
||||
'it_admin': 'IT-Beauftragte oder IT-Beauftragter',
|
||||
'logo_path': '',
|
||||
'logo_thumb': '',
|
||||
'logo_thumb': '',
|
||||
},
|
||||
'schoolPeriods': {
|
||||
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"},
|
||||
"2": {"start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)"},
|
||||
@@ -163,6 +174,7 @@ SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key'])
|
||||
|
||||
# School periods
|
||||
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
|
||||
SCHOOL_INFO_DEFAULT = _get(_conf, ['school'], DEFAULTS['school'])
|
||||
|
||||
# Optional feature modules
|
||||
TENANT_CONFIGS = _get(_conf, ['tenants'], {})
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<h2>Schuldaten</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('admin_school_settings') }}">
|
||||
<form method="POST" action="{{ url_for('admin_school_settings') }}" enctype="multipart/form-data">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="name">Schulname *</label>
|
||||
@@ -66,9 +66,9 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="logo_path">Logo-Pfad oder Dateiname</label>
|
||||
<input type="text" id="logo_path" name="logo_path" value="{{ school_info.logo_path or '' }}" placeholder="optional, z. B. static/img/schullogo.png">
|
||||
<small>Der Logo-Pfad wird gespeichert und kann später für den PDF-Briefkopf verwendet werden.</small>
|
||||
<label for="logo_upload">Schullogo hochladen</label>
|
||||
<input type="file" id="logo_upload" name="logo_upload" accept=".png,.jpg,.jpeg,.gif,.webp,.svg,image/*">
|
||||
<small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
|
||||
</div>
|
||||
|
||||
<div class="actions-row">
|
||||
@@ -84,6 +84,13 @@
|
||||
<h2>Aktuelle Vorschau</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if school_info.logo_path %}
|
||||
<div style="margin-bottom:16px; padding:12px; border:1px solid #e5e7eb; border-radius:10px; background:#fff;">
|
||||
<div style="font-weight:700; margin-bottom:8px; color:#374151;">Aktuelles Logo</div>
|
||||
<img src="{{ url_for('uploaded_file', filename=school_info.logo_path) }}" alt="Aktuelles Schullogo" style="max-width:100%; max-height:180px; object-fit:contain; display:block;">
|
||||
<div style="margin-top:8px; font-size:0.85rem; color:#6b7280; word-break:break-word;">{{ school_info.logo_path }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p><strong>Schulname:</strong> {{ school_info.name or 'Nicht gesetzt' }}</p>
|
||||
<p><strong>Adresse:</strong> {{ school_info.address or 'Nicht gesetzt' }}</p>
|
||||
<p><strong>Schulnummer:</strong> {{ school_info.school_number or 'Nicht gesetzt' }}</p>
|
||||
|
||||
+17
-3
@@ -1127,6 +1127,7 @@
|
||||
{% if current_permissions.pages.get('manage_locations', True) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
||||
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
||||
{% endif %}
|
||||
@@ -1252,6 +1253,7 @@
|
||||
{% if student_cards_module_enabled %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||||
<li><h6 class="dropdown-header">System</h6></li>
|
||||
@@ -1311,9 +1313,19 @@
|
||||
{% else %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" id="loginNavbar">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand py-0" href="{{ url_for('home') }}">
|
||||
<img src="{{ url_for('static', filename='img/invario-logo.png') }}" alt="Invario" class="invario-logo">
|
||||
</a>
|
||||
<a class="navbar-brand py-0" href="{{ url_for('home') }}">
|
||||
{% set school_logo_thumb = school_info.get('logo_thumb') if school_info else '' %}
|
||||
{% set school_logo_path = school_info.get('logo_path') if school_info else '' %}
|
||||
{% if school_logo_thumb or school_logo_path %}
|
||||
{% if school_logo_thumb %}
|
||||
<img src="{{ url_for('uploaded_file', filename=school_logo_thumb) }}" alt="{{ school_info.name or 'Schullogo' }}" class="invario-logo">
|
||||
{% else %}
|
||||
<img src="{{ url_for('uploaded_file', filename=school_logo_path) }}" alt="{{ school_info.name or 'Schullogo' }}" class="invario-logo">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='img/invario-logo.png') }}" alt="Invario" class="invario-logo">
|
||||
{% endif %}
|
||||
</a>
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('impressum') }}">Impressum</a>
|
||||
@@ -1519,6 +1531,7 @@
|
||||
<option value="Defekte Items"></option>
|
||||
<option value="Filter verwalten"></option>
|
||||
<option value="Orte verwalten"></option>
|
||||
<option value="Schulstammdaten"></option>
|
||||
<option value="Audit Dashboard"></option>
|
||||
<option value="Logs"></option>
|
||||
<option value="Benutzer verwalten"></option>
|
||||
@@ -1588,6 +1601,7 @@
|
||||
{ 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: '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: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
|
||||
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
|
||||
|
||||
@@ -2353,10 +2353,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
{% 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="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user