feat(school-logo): implement logo upload functionality and update school settings UI

This commit is contained in:
2026-05-10 17:21:27 +02:00
parent b905ec9024
commit b1fabfbfcf
4 changed files with 103 additions and 6 deletions
+45
View File
@@ -636,6 +636,30 @@ 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)
return logo_filename
def _parse_money_value(value): def _parse_money_value(value):
"""Parse a user-facing money value into a float when possible.""" """Parse a user-facing money value into a float when possible."""
if value is None: if value is None:
@@ -9891,6 +9915,27 @@ def admin_school_settings():
'logo_path': sanitize_form_value(request.form.get('logo_path')), 'logo_path': sanitize_form_value(request.form.get('logo_path')),
} }
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 ''
saved_logo_filename = _save_school_logo_upload(uploaded_logo, tenant_id=tenant_id, tenant_db=tenant_db)
if saved_logo_filename:
school_info['logo_path'] = saved_logo_filename
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
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', '')
missing_fields = [ missing_fields = [
label for label, key in [ label for label, key in [
('Schulname', 'name'), ('Schulname', 'name'),
+35
View File
@@ -7,6 +7,7 @@ PDF/A archiving standards for German schools and educational authorities.
import io import io
import json import json
import datetime import datetime
import os
import qrcode import qrcode
from reportlab.lib.pagesizes import A4 from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
@@ -110,6 +111,20 @@ class DIN5008AuditPDF:
textColor=HexColor('#000000'), 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""" school_info_text = f"""
<b>{school_name}</b><br/> <b>{school_name}</b><br/>
{address}<br/> {address}<br/>
@@ -117,6 +132,26 @@ class DIN5008AuditPDF:
<i>Schulnummer: {school_number}</i> <i>Schulnummer: {school_number}</i>
""" """
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)) story.append(Paragraph(school_info_text, header_style))
# Information block (right side simulation) # Information block (right side simulation)
+10
View File
@@ -61,6 +61,15 @@ DEFAULTS = {
'logs': os.path.join(os.path.dirname(os.path.dirname(BASE_DIR)), 'logs'), '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'), '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': '',
},
'schoolPeriods': { 'schoolPeriods': {
"1": {"start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)"}, "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)"}, "2": {"start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)"},
@@ -163,6 +172,7 @@ SSL_KEY = _get(_conf, ['ssl', 'key'], DEFAULTS['ssl']['key'])
# School periods # School periods
SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods']) SCHOOL_PERIODS = _get(_conf, ['schoolPeriods'], DEFAULTS['schoolPeriods'])
SCHOOL_INFO_DEFAULT = _get(_conf, ['school'], DEFAULTS['school'])
# Optional feature modules # Optional feature modules
TENANT_CONFIGS = _get(_conf, ['tenants'], {}) TENANT_CONFIGS = _get(_conf, ['tenants'], {})
+11 -4
View File
@@ -32,7 +32,7 @@
<h2>Schuldaten</h2> <h2>Schuldaten</h2>
</div> </div>
<div class="card-body"> <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-row">
<div class="form-group"> <div class="form-group">
<label for="name">Schulname *</label> <label for="name">Schulname *</label>
@@ -66,9 +66,9 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="logo_path">Logo-Pfad oder Dateiname</label> <label for="logo_upload">Schullogo hochladen</label>
<input type="text" id="logo_path" name="logo_path" value="{{ school_info.logo_path or '' }}" placeholder="optional, z. B. static/img/schullogo.png"> <input type="file" id="logo_upload" name="logo_upload" accept=".png,.jpg,.jpeg,.gif,.webp,.svg,image/*">
<small>Der Logo-Pfad wird gespeichert und kann später für den PDF-Briefkopf verwendet werden.</small> <small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
</div> </div>
<div class="actions-row"> <div class="actions-row">
@@ -84,6 +84,13 @@
<h2>Aktuelle Vorschau</h2> <h2>Aktuelle Vorschau</h2>
</div> </div>
<div class="card-body"> <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>Schulname:</strong> {{ school_info.name or 'Nicht gesetzt' }}</p>
<p><strong>Adresse:</strong> {{ school_info.address 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> <p><strong>Schulnummer:</strong> {{ school_info.school_number or 'Nicht gesetzt' }}</p>