Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcd1529377 | |||
| ae80be31e7 | |||
| 06682ef8f2 | |||
| d69c478fed | |||
| 5f551331aa | |||
| 68dc2f71c6 | |||
| 736dafd1e1 | |||
| 123ad6f0a7 | |||
| c953f22ab4 | |||
| b1fabfbfcf | |||
| b905ec9024 |
@@ -49,15 +49,6 @@ Bearbeite `config.json`:
|
||||
|
||||
Falls nicht konfiguriert, werden Platzhalter verwendet.
|
||||
|
||||
Alternativ können Sie die Schulinformationen tenant-spezifisch mit dem Manage-Skript setzen:
|
||||
|
||||
```bash
|
||||
# Beispiel: Schulinformationen für Tenant 'school_a' setzen
|
||||
./manage-tenant.sh school school_a name="Grundschule Albert-Schweitzer-Straße" address="Albert-Schweitzer-Straße 1" postal_code=12345 city="Musterstadt" school_number=042123 it_admin="Max Mustermann"
|
||||
```
|
||||
|
||||
Hinweis: `manage-tenant.sh school` schreibt die Daten unter `tenants.<tenant_id>.school` in `config.json` und löst bei aktiver App-Container-Instanz einen Neustart aus, damit die Änderungen wirksam werden.
|
||||
|
||||
### Schritt 2: System starten
|
||||
|
||||
```bash
|
||||
|
||||
+403
-638
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
@@ -250,66 +285,147 @@ class DIN5008AuditPDF:
|
||||
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
|
||||
if self.export_type == 'quick':
|
||||
# Quick-Check: Minimal columns
|
||||
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
|
||||
chain_idx = str(row.get('chain_index', ''))
|
||||
timestamp = str(row.get('timestamp') or row.get('created_at', ''))[:16]
|
||||
event_type = str(row.get('event_type', ''))
|
||||
actor = str(row.get('actor', ''))
|
||||
entry_hash = str(row.get('entry_hash', ''))[:12] + '...'
|
||||
chain_idx = _safe(row.get('chain_index', ''))
|
||||
timestamp = _fmt_ts(row.get('timestamp') or row.get('created_at', ''))
|
||||
event_type = _fmt_event(row.get('event_type', ''))
|
||||
actor = _safe(row.get('actor', ''))
|
||||
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:
|
||||
# Official Report: Full columns
|
||||
table_data = [
|
||||
['Idx', 'Zeitstempel', 'Ereignistyp', 'Benutzer', 'Quelle', 'IP-Adresse', 'Hashwert'],
|
||||
['Idx', 'Zeit', 'Ereignis', 'Benutzer', 'Quelle', 'IP', 'Hash'],
|
||||
]
|
||||
|
||||
for row in audit_rows:
|
||||
chain_idx = str(row.get('chain_index', ''))
|
||||
timestamp = str(row.get('timestamp') or row.get('created_at', ''))[:19]
|
||||
event_type = str(row.get('event_type', ''))
|
||||
actor = str(row.get('actor', ''))
|
||||
source = str(row.get('source', 'System'))[:15]
|
||||
ip = str(row.get('ip', ''))
|
||||
entry_hash = str(row.get('entry_hash', ''))[:16]
|
||||
chain_idx = _safe(row.get('chain_index', ''))
|
||||
timestamp = _fmt_ts(row.get('timestamp') or row.get('created_at', ''))
|
||||
event_type = _fmt_event(row.get('event_type', ''))
|
||||
actor = _safe(row.get('actor', ''))
|
||||
source = _safe(row.get('source', 'System'))
|
||||
ip = _fmt_ip(row.get('ip', ''))
|
||||
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
|
||||
events_table = Table(table_data, colWidths=colWidths)
|
||||
events_table = Table(table_data, colWidths=colWidths, repeatRows=1)
|
||||
events_table.setStyle(TableStyle([
|
||||
# Header styling
|
||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#2c3e50')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), HexColor('#ffffff')),
|
||||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('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),
|
||||
|
||||
# Body styling
|
||||
('FONTSIZE', (0, 1), (-1, -1), 7),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 8),
|
||||
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, HexColor('#bdc3c7')),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [HexColor('#ecf0f1'), HexColor('#ffffff')]),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 4),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 4),
|
||||
('LEFTPADDING', (0, 1), (-1, -1), 3),
|
||||
('RIGHTPADDING', (0, 1), (-1, -1), 3),
|
||||
('TOPPADDING', (0, 1), (-1, -1), 5),
|
||||
('BOTTOMPADDING', (0, 1), (-1, -1), 5),
|
||||
('LEFTPADDING', (0, 1), (-1, -1), 4),
|
||||
('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(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))
|
||||
|
||||
def _add_mismatches(self, story, mismatches):
|
||||
@@ -552,3 +668,123 @@ def generate_audit_pdf(verify_result, event_counts, audit_rows, export_type='off
|
||||
return pdf_gen.generate_quick_check(verify_result, event_counts, audit_rows)
|
||||
else:
|
||||
return pdf_gen.generate_official_report(verify_result, event_counts, audit_rows)
|
||||
|
||||
def _build_invoice_pdf(invoice_data):
|
||||
"""Render a PDF invoice for a damaged borrowed item."""
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.colors import HexColor, black, white
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
pdf_buffer = io.BytesIO()
|
||||
c = canvas.Canvas(pdf_buffer, pagesize=A4)
|
||||
page_width, page_height = A4
|
||||
|
||||
margin_x = 20 * mm
|
||||
margin_top = 20 * mm
|
||||
usable_width = page_width - (2 * margin_x)
|
||||
current_y = page_height - margin_top
|
||||
|
||||
dark_color = HexColor('#0F172A')
|
||||
accent_color = HexColor('#B91C1C')
|
||||
light_color = HexColor('#F8FAFC')
|
||||
border_color = HexColor('#CBD5E1')
|
||||
text_color = HexColor('#1E293B')
|
||||
muted_color = HexColor('#64748B')
|
||||
|
||||
def draw_wrapped_lines(text, x_pos, y_pos, width, font_name='Helvetica', font_size=11, leading=14, color=text_color):
|
||||
if not text:
|
||||
return y_pos
|
||||
c.setFont(font_name, font_size)
|
||||
c.setFillColor(color)
|
||||
for line in simpleSplit(str(text), font_name, font_size, width):
|
||||
c.drawString(x_pos, y_pos, line)
|
||||
y_pos -= leading
|
||||
return y_pos
|
||||
|
||||
def draw_label_value(label, value, x_pos, y_pos, label_width=45 * mm):
|
||||
c.setFont('Helvetica-Bold', 10)
|
||||
c.setFillColor(muted_color)
|
||||
c.drawString(x_pos, y_pos, label)
|
||||
c.setFont('Helvetica', 10)
|
||||
c.setFillColor(text_color)
|
||||
c.drawString(x_pos + label_width, y_pos, str(value or '-'))
|
||||
return y_pos - 7 * mm
|
||||
|
||||
c.setFillColor(light_color)
|
||||
c.rect(0, 0, page_width, page_height, fill=1, stroke=0)
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.rect(0, page_height - 28 * mm, page_width, 28 * mm, fill=1, stroke=0)
|
||||
c.setFillColor(white)
|
||||
c.setFont('Helvetica-Bold', 20)
|
||||
c.drawString(margin_x, page_height - 16 * mm, 'RECHNUNG')
|
||||
c.setFont('Helvetica', 10)
|
||||
c.drawString(margin_x, page_height - 23 * mm, 'Inventarsystem - Schadensersatz für zerstörtes Ausleihobjekt')
|
||||
|
||||
current_y = page_height - 40 * mm
|
||||
c.setStrokeColor(border_color)
|
||||
c.setLineWidth(1)
|
||||
c.line(margin_x, current_y, page_width - margin_x, current_y)
|
||||
current_y -= 12 * mm
|
||||
|
||||
invoice_number = invoice_data.get('invoice_number', '-')
|
||||
created_at = invoice_data.get('created_at_display', '-')
|
||||
borrower = invoice_data.get('borrower', '-')
|
||||
item_name = invoice_data.get('item_name', '-')
|
||||
item_code = invoice_data.get('item_code', '-')
|
||||
item_id = invoice_data.get('item_id', '-')
|
||||
damage_reason = invoice_data.get('damage_reason', '-')
|
||||
amount_text = invoice_data.get('amount_text', '-')
|
||||
|
||||
current_y = draw_label_value('Rechnungsnummer:', invoice_number, margin_x, current_y)
|
||||
current_y = draw_label_value('Datum:', created_at, margin_x, current_y)
|
||||
current_y = draw_label_value('Empfänger:', borrower, margin_x, current_y)
|
||||
current_y = draw_label_value('Ausleihe / Element:', item_name, margin_x, current_y)
|
||||
current_y = draw_label_value('Element-ID:', item_id, margin_x, current_y)
|
||||
current_y = draw_label_value('Code:', item_code, margin_x, current_y)
|
||||
current_y = current_y - 3 * mm
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.setFont('Helvetica-Bold', 12)
|
||||
c.drawString(margin_x, current_y, 'Schadensbeschreibung')
|
||||
current_y -= 6 * mm
|
||||
current_y = draw_wrapped_lines(damage_reason, margin_x, current_y, usable_width, font_size=10, leading=13, color=text_color)
|
||||
current_y -= 4 * mm
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.setFont('Helvetica-Bold', 12)
|
||||
c.drawString(margin_x, current_y, 'Rechnungsbetrag')
|
||||
current_y -= 8 * mm
|
||||
|
||||
c.setFillColor(accent_color)
|
||||
c.setStrokeColor(accent_color)
|
||||
c.rect(margin_x, current_y - 12 * mm, usable_width, 16 * mm, fill=1, stroke=0)
|
||||
c.setFillColor(white)
|
||||
c.setFont('Helvetica-Bold', 16)
|
||||
c.drawString(margin_x + 5 * mm, current_y - 2 * mm, amount_text)
|
||||
current_y -= 20 * mm
|
||||
|
||||
current_y = draw_wrapped_lines(
|
||||
'Bitte begleichen Sie diesen Betrag zeitnah bei der Verwaltung. Der Betrag ergibt sich aus dem zerstörten Ausleihobjekt und der dokumentierten Schadensmeldung.',
|
||||
margin_x,
|
||||
current_y,
|
||||
usable_width,
|
||||
font_size=10,
|
||||
leading=13,
|
||||
color=muted_color,
|
||||
)
|
||||
|
||||
footer_y = 18 * mm
|
||||
c.setStrokeColor(border_color)
|
||||
c.setLineWidth(0.8)
|
||||
c.line(margin_x, footer_y + 8 * mm, page_width - margin_x, footer_y + 8 * mm)
|
||||
c.setFillColor(muted_color)
|
||||
c.setFont('Helvetica', 9)
|
||||
c.drawString(margin_x, footer_y, 'Inventarsystem - Rechnungserstellung')
|
||||
c.drawRightString(page_width - margin_x, footer_y, f'{amount_text}')
|
||||
|
||||
c.save()
|
||||
pdf_buffer.seek(0)
|
||||
return pdf_buffer
|
||||
@@ -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'], {})
|
||||
@@ -348,3 +360,65 @@ def MongoClient(*args, **kwargs):
|
||||
cached_client = _MongoClientProxy(client)
|
||||
_MONGO_CLIENT_CACHE[cache_key] = cached_client
|
||||
return cached_client
|
||||
|
||||
|
||||
def get_school_info():
|
||||
"""Return the tenant-scoped school metadata used for PDFs and admin views."""
|
||||
school_info = dict(SCHOOL_INFO_DEFAULT)
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
if 'settings' not in db.list_collection_names():
|
||||
return school_info
|
||||
|
||||
settings_collection = db['settings']
|
||||
settings_document = settings_collection.find_one({'setting_type': 'school_info'})
|
||||
if not settings_document:
|
||||
return school_info
|
||||
|
||||
configured_school = settings_document.get('school', {})
|
||||
if isinstance(configured_school, dict):
|
||||
for key, value in configured_school.items():
|
||||
if value is not None:
|
||||
school_info[key] = value
|
||||
return school_info
|
||||
except Exception:
|
||||
return school_info
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
def update_school_info(school_info):
|
||||
"""Persist tenant-scoped school metadata into MongoDB and refresh the in-memory cache."""
|
||||
if not isinstance(school_info, dict):
|
||||
raise TypeError('school_info must be a dict')
|
||||
|
||||
updated_school = dict(SCHOOL_INFO_DEFAULT)
|
||||
for key in updated_school.keys():
|
||||
value = school_info.get(key, updated_school[key])
|
||||
if value is None:
|
||||
value = ''
|
||||
updated_school[key] = str(value).strip()
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
settings_collection = db['settings']
|
||||
settings_collection.update_one(
|
||||
{'setting_type': 'school_info'},
|
||||
{
|
||||
'$set': {
|
||||
'setting_type': 'school_info',
|
||||
'school': updated_school,
|
||||
}
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
return dict(updated_school)
|
||||
|
||||
@@ -20,28 +20,12 @@
|
||||
<h4 style="margin:0 0 10px 0; color:#1a1a1a;">📄 PDF-Export (DIN 5008 konform)</h4>
|
||||
<p style="margin:0 0 10px 0; font-size:0.9rem; color:#666;">Professionelle Berichte für Schulträger und Behörden</p>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a class="btn btn-primary" href="{{ url_for('admin_audit_export_pdf_quick') }}" style="flex:1; text-align:center;">
|
||||
🚀 Schnell-Check (kompakt)
|
||||
</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('admin_audit_export_pdf_official') }}" style="flex:1; text-align:center;">
|
||||
📋 Amtlicher Bericht (DIN 5008)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Formats Section -->
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<h4 style="margin:0 0 10px 0; color:#1a1a1a;">📊 Weitere Formate</h4>
|
||||
<p style="margin:0 0 10px 0; font-size:0.9rem; color:#666;">Technische Exporte für System-Integration</p>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin_audit_export', format='md') }}" style="flex:1; text-align:center;">
|
||||
📝 Markdown
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin_audit_export', format='json') }}" style="flex:1; text-align:center;">
|
||||
⚙️ JSON
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
<!--
|
||||
Copyright 2025-2026 AIIrondev
|
||||
|
||||
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
||||
See Legal/LICENSE for the full license text.
|
||||
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
||||
For commercial licensing inquiries: https://github.com/AIIrondev
|
||||
-->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Schulstammdaten verwalten{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>Schulstammdaten verwalten</h1>
|
||||
<p>Diese Angaben werden für amtliche Exporte, PDF-Berichte und die Zuordnung in Behörden verwendet.</p>
|
||||
</div>
|
||||
|
||||
<div class="notice-card">
|
||||
<strong>Hinweis:</strong> Die hier gespeicherten Daten werden tenantweise in der MongoDB gespeichert und sofort für die PDF-Exporte verwendet.
|
||||
</div>
|
||||
|
||||
<div class="notice-card" style="background:#f8fafc; border-color:#dbe3ee; color:#334155;">
|
||||
<strong>Aktueller Kontext:</strong>
|
||||
Tenant {{ tenant_id or 'default' }} · Datenbank {{ tenant_db or 'Inventarsystem' }}
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<div class="card settings-card">
|
||||
<div class="card-header">
|
||||
<h2>Schuldaten</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<input type="text" id="name" name="name" value="{{ school_info.name or '' }}" required placeholder="Grundschule Beispielstadt">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="school_number">Schulnummer *</label>
|
||||
<input type="text" id="school_number" name="school_number" value="{{ school_info.school_number or '' }}" required placeholder="042123">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="address">Adresse *</label>
|
||||
<input type="text" id="address" name="address" value="{{ school_info.address or '' }}" required placeholder="Musterstraße 12">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="postal_code">PLZ *</label>
|
||||
<input type="text" id="postal_code" name="postal_code" value="{{ school_info.postal_code or '' }}" required placeholder="12345">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city">Ort *</label>
|
||||
<input type="text" id="city" name="city" value="{{ school_info.city or '' }}" required placeholder="Musterstadt">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="it_admin">Verantwortliche Person</label>
|
||||
<input type="text" id="it_admin" name="it_admin" value="{{ school_info.it_admin or '' }}" placeholder="IT-Beauftragte/r, Sekretariat oder Schulleitung">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<button type="submit" class="btn btn-primary">Schulstammdaten speichern</button>
|
||||
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card preview-card">
|
||||
<div class="card-header">
|
||||
<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>
|
||||
<p><strong>Ort:</strong> {{ school_info.postal_code or '' }} {{ school_info.city or '' }}</p>
|
||||
<p><strong>Verantwortliche Person:</strong> {{ school_info.it_admin or 'Nicht gesetzt' }}</p>
|
||||
<p><strong>Logo:</strong> {{ school_info.logo_path or 'Nicht gesetzt' }}</p>
|
||||
<hr>
|
||||
<p>Diese Angaben erscheinen künftig im amtlichen Audit-PDF und in anderen Behördenberichten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin-bottom: 8px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #4b5563;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
color: #1d4ed8;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 0.9fr;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--ui-surface);
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: var(--ui-surface-soft);
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #6b7280;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e5e7eb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.preview-card p {
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.preview-card hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.settings-grid,
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
+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 }} },
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
<div id="schedule-modal" class="item-modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" onclick="closeScheduleModal()">×</span>
|
||||
<h2>Termin planen</h2>
|
||||
<h2>Reservieren</h2>
|
||||
<form id="schedule-form">
|
||||
<input type="hidden" id="schedule-item-id" name="item_id">
|
||||
|
||||
@@ -1057,7 +1057,7 @@
|
||||
:
|
||||
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${canScheduleItem ? `<button class="schedule-button" type="button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" type="button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<button class="quick-details-button" type="button" onclick="openItemQuick('${item._id}')">Details</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -1734,7 +1734,7 @@
|
||||
</form>`
|
||||
: `<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<button type="button" class="details-button" onclick="document.getElementById('item-modal').style.display='none';">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2643,7 +2643,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div id="schedule-modal" class="item-modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" onclick="closeScheduleModal()">×</span>
|
||||
<h2>Termin planen</h2>
|
||||
<h2>Reservieren</h2>
|
||||
<form id="schedule-form">
|
||||
<input type="hidden" id="schedule-item-id" name="item_id">
|
||||
<div class="form-group" id="schedule-specific-item-group" style="display:none;">
|
||||
@@ -3654,7 +3654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</form>
|
||||
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
itemsContainer.appendChild(card);
|
||||
@@ -4531,7 +4531,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from app import app
|
||||
with app.test_client() as client:
|
||||
with client.session_transaction() as sess:
|
||||
sess['username'] = 'admin'
|
||||
sess['admin'] = True
|
||||
resp = client.get('/terminplan')
|
||||
print("Status:", resp.status_code)
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI utility to verify the tamper-evident audit chain."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
import audit_log as al
|
||||
|
||||
|
||||
def main():
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
al.ensure_audit_indexes(db)
|
||||
result = al.verify_audit_chain(db)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
||||
return 0 if result.get("ok") else 2
|
||||
except Exception as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
||||
return 1
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -21,7 +21,6 @@ show_help() {
|
||||
echo " restart-all Restart all application containers (zero-downtime reload)"
|
||||
echo " list List active tenants"
|
||||
echo " module <tenant_id> <module>=<on|off>... Enable/disable modules for a tenant"
|
||||
echo " school <tenant_id> [key]=[value]... Set school configuration for tenant (name,address,postal_code,city,school_number,it_admin)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
@@ -566,62 +565,6 @@ PY
|
||||
fi
|
||||
;;
|
||||
|
||||
school)
|
||||
# Usage: manage-tenant.sh school <tenant_id> key=value [...]
|
||||
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
|
||||
echo "Error: Usage: manage-tenant.sh school <tenant_id> key=value [...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pass all remaining args to python to update tenants[tenant_id]['school']
|
||||
shift 2
|
||||
|
||||
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
|
||||
import json, sys, os
|
||||
path = sys.argv[1]
|
||||
tenant_id = sys.argv[2]
|
||||
kv_args = sys.argv[3:]
|
||||
|
||||
if not os.path.isfile(path):
|
||||
print(f"Error: config file not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
cfg = json.load(f)
|
||||
|
||||
tenants = cfg.setdefault('tenants', {})
|
||||
tenant_cfg = tenants.setdefault(tenant_id, {})
|
||||
school_cfg = tenant_cfg.setdefault('school', {})
|
||||
|
||||
for arg in kv_args:
|
||||
if '=' not in arg:
|
||||
print(f"Warning: Ignoring invalid argument '{arg}'. Expected key=value.", file=sys.stderr)
|
||||
continue
|
||||
key, val = arg.split('=', 1)
|
||||
key = key.strip()
|
||||
val = val.strip()
|
||||
# try to coerce numeric values
|
||||
if val.isdigit():
|
||||
val_parsed = int(val)
|
||||
else:
|
||||
val_parsed = val
|
||||
school_cfg[key] = val_parsed
|
||||
print(f"Set school.{key} = {val_parsed} for tenant {tenant_id}")
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
||||
PY
|
||||
then
|
||||
echo "School configuration updated successfully in config.json for tenant '$TENANT_ID'."
|
||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||
restart_app_container
|
||||
fi
|
||||
else
|
||||
echo "Failed to update school configuration for tenant '$TENANT_ID'."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command: $COMMAND"
|
||||
show_help
|
||||
|
||||
Reference in New Issue
Block a user