|
|
|
@@ -26,6 +26,7 @@ from jinja2 import TemplateNotFound
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
|
from gridfs import GridFS
|
|
|
|
|
|
|
|
|
|
# Ensure imports work regardless of whether gunicorn starts in /app or /app/Web.
|
|
|
|
|
_CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
@@ -395,6 +396,12 @@ SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower",
|
|
|
|
|
APP_VERSION = __version__
|
|
|
|
|
RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version')
|
|
|
|
|
|
|
|
|
|
def get_gridfs():
|
|
|
|
|
"""Return a GridFS instance connected to the current tenant's database context."""
|
|
|
|
|
client = cfg.MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
|
|
|
db = client[cfg.MONGODB_DB] # Intercepted by your multi-tenant proxy automatically
|
|
|
|
|
return GridFS(db)
|
|
|
|
|
|
|
|
|
|
""" -----------------------------------------------------------------------Before Request Handlers---------------------------------------------------------------------------- """
|
|
|
|
|
|
|
|
|
|
def _get_csrf_token():
|
|
|
|
@@ -797,7 +804,7 @@ 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."""
|
|
|
|
|
"""Save an uploaded school logo directly into MongoDB GridFS."""
|
|
|
|
|
if not upload_file or not getattr(upload_file, 'filename', ''):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
@@ -813,33 +820,44 @@ def _save_school_logo_upload(upload_file, tenant_id=None, tenant_db=None):
|
|
|
|
|
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)
|
|
|
|
|
thumb_filename = f'school-logo-{safe_tenant}-thumb.png'
|
|
|
|
|
|
|
|
|
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
|
|
|
upload_file.save(logo_path)
|
|
|
|
|
fs = get_gridfs()
|
|
|
|
|
|
|
|
|
|
# Generate a thumbnail for consistent navbar sizing
|
|
|
|
|
# Read file data into memory
|
|
|
|
|
upload_file.seek(0)
|
|
|
|
|
file_bytes = upload_file.read()
|
|
|
|
|
|
|
|
|
|
# Save main logo to GridFS
|
|
|
|
|
fs.put(
|
|
|
|
|
io.BytesIO(file_bytes),
|
|
|
|
|
filename=logo_filename,
|
|
|
|
|
content_type=upload_file.content_type or 'image/png'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Generate thumbnail in-memory and save to GridFS
|
|
|
|
|
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
|
|
|
|
|
with Image.open(io.BytesIO(file_bytes)) as img:
|
|
|
|
|
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)
|
|
|
|
|
thumb_io = io.BytesIO()
|
|
|
|
|
img.save(thumb_io, format='PNG', optimize=True)
|
|
|
|
|
thumb_io.seek(0)
|
|
|
|
|
|
|
|
|
|
fs.put(
|
|
|
|
|
thumb_io,
|
|
|
|
|
filename=thumb_filename,
|
|
|
|
|
content_type='image/png'
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
app.logger.warning(f"Thumbnail generation failed for {logo_path}: {e}")
|
|
|
|
|
app.logger.warning(f"Thumbnail generation failed for logo: {e}")
|
|
|
|
|
thumb_filename = None
|
|
|
|
|
|
|
|
|
|
return (logo_filename, thumb_filename)
|
|
|
|
|
|
|
|
|
|
"""---------------------------------------------Invoice Generation----------------------------------------------------------------------------- """
|
|
|
|
|
|
|
|
|
|
def _parse_money_value(value):
|
|
|
|
@@ -2534,14 +2552,15 @@ def upload_student_cards_excel():
|
|
|
|
|
|
|
|
|
|
"""-------------------------------------------------------------File Serving-----------------------------------------------------------------------------"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/uploads/<filename>')
|
|
|
|
|
def uploaded_file(filename):
|
|
|
|
|
"""
|
|
|
|
|
Serve uploaded files from the uploads directory.
|
|
|
|
|
|
|
|
|
|
Serve uploaded files from MongoDB GridFS with local path fallbacks.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
filename (str): Name of the file to serve
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
flask.Response: The requested file or placeholder image if not found
|
|
|
|
|
"""
|
|
|
|
@@ -2550,24 +2569,37 @@ def uploaded_file(filename):
|
|
|
|
|
if denied:
|
|
|
|
|
return denied
|
|
|
|
|
|
|
|
|
|
# Check production path first (deployed environment)
|
|
|
|
|
# 1. Try serving from MongoDB GridFS first
|
|
|
|
|
try:
|
|
|
|
|
fs = get_gridfs()
|
|
|
|
|
grid_out = fs.find_one({'filename': filename})
|
|
|
|
|
if grid_out:
|
|
|
|
|
return send_file(
|
|
|
|
|
io.BytesIO(grid_out.read()),
|
|
|
|
|
mimetype=grid_out.content_type or 'application/octet-stream',
|
|
|
|
|
download_name=filename
|
|
|
|
|
)
|
|
|
|
|
except Exception as grid_err:
|
|
|
|
|
app.logger.warning(f"GridFS lookup failed for {filename}: {grid_err}")
|
|
|
|
|
|
|
|
|
|
# 2. Fallback to production path for legacy local files
|
|
|
|
|
prod_path = "/opt/Inventarsystem/Web/uploads"
|
|
|
|
|
dev_path = app.config['UPLOAD_FOLDER']
|
|
|
|
|
if os.path.exists(os.path.join(prod_path, filename)):
|
|
|
|
|
return send_from_directory(prod_path, filename)
|
|
|
|
|
# Then check development path
|
|
|
|
|
# 3. Fallback to development path
|
|
|
|
|
if os.path.exists(os.path.join(dev_path, filename)):
|
|
|
|
|
return send_from_directory(dev_path, filename)
|
|
|
|
|
|
|
|
|
|
# Use a placeholder image if file not found - first try SVG, then PNG
|
|
|
|
|
|
|
|
|
|
# 4. Use a placeholder image if file not found - first try SVG, then PNG
|
|
|
|
|
svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg')
|
|
|
|
|
png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(svg_placeholder_path):
|
|
|
|
|
return send_from_directory(app.static_folder, 'img/no-image.svg')
|
|
|
|
|
elif os.path.exists(png_placeholder_path):
|
|
|
|
|
return send_from_directory(app.static_folder, 'img/no-image.png')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Default placeholder from static folder
|
|
|
|
|
return send_from_directory(app.static_folder, 'favicon.ico')
|
|
|
|
|
except Exception as e:
|
|
|
|
@@ -10248,74 +10280,58 @@ def get_period_times(booking_date, period_num):
|
|
|
|
|
|
|
|
|
|
"""---------------------------------------------------------Borrowing-----------------------------------------------------------------"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/my_borrowed_items')
|
|
|
|
|
def my_borrowed_items():
|
|
|
|
|
"""
|
|
|
|
|
Zeigt alle vom aktuellen Benutzer ausgeliehenen und geplanten Objekte an.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Response: Gerendertes Template mit den ausgeliehenen und geplanten Objekten des Benutzers
|
|
|
|
|
"""
|
|
|
|
|
if 'username' not in session:
|
|
|
|
|
flash('Bitte melden Sie sich an, um Ihre ausgeliehenen Objekte anzuzeigen', 'error')
|
|
|
|
|
return redirect(url_for('login', next=request.path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
username = session['username']
|
|
|
|
|
|
|
|
|
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
|
|
|
|
db = client[MONGODB_DB]
|
|
|
|
|
items_collection = db.items
|
|
|
|
|
ausleihungen_collection = db.ausleihungen
|
|
|
|
|
|
|
|
|
|
# Get current time for comparison
|
|
|
|
|
current_time = datetime.datetime.now()
|
|
|
|
|
|
|
|
|
|
# Check if user is admin
|
|
|
|
|
user_is_admin = False
|
|
|
|
|
if 'is_admin' in session:
|
|
|
|
|
user_is_admin = session['is_admin']
|
|
|
|
|
|
|
|
|
|
# Get items currently borrowed by the user (where Verfuegbar=false and User=username)
|
|
|
|
|
borrowed_items = list(items_collection.find({'Verfuegbar': False, 'User': username}))
|
|
|
|
|
|
|
|
|
|
# Get active and planned ausleihungen for the user
|
|
|
|
|
active_ausleihungen = list(ausleihungen_collection.find({
|
|
|
|
|
'User': username,
|
|
|
|
|
'Status': 'active'
|
|
|
|
|
items_collection = db['items']
|
|
|
|
|
ausleihungen_collection = db['ausleihungen']
|
|
|
|
|
|
|
|
|
|
all_ausleihungen = list(ausleihungen_collection.find({
|
|
|
|
|
'Status': {'$in': ['active', 'planned']}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
planned_ausleihungen = list(ausleihungen_collection.find({
|
|
|
|
|
'User': username,
|
|
|
|
|
'Status': 'planned'
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
# Process items
|
|
|
|
|
|
|
|
|
|
active_items = []
|
|
|
|
|
planned_items = []
|
|
|
|
|
processed_item_ids = set() # Keep track of processed item IDs to avoid duplicates
|
|
|
|
|
|
|
|
|
|
# First, process items that are directly marked as borrowed by the user
|
|
|
|
|
for item in borrowed_items:
|
|
|
|
|
# Convert ObjectId to string for template
|
|
|
|
|
item['_id'] = str(item['_id'])
|
|
|
|
|
active_items.append(item)
|
|
|
|
|
processed_item_ids.add(item['_id'])
|
|
|
|
|
|
|
|
|
|
# Process active appointments
|
|
|
|
|
for appointment in active_ausleihungen:
|
|
|
|
|
# Get the item ID from the appointment
|
|
|
|
|
processed_item_ids = set()
|
|
|
|
|
|
|
|
|
|
for appointment in all_ausleihungen:
|
|
|
|
|
raw_user = appointment.get('User', '')
|
|
|
|
|
try:
|
|
|
|
|
decrypted_user = decrypt_text(raw_user) if raw_user else ''
|
|
|
|
|
except Exception as e:
|
|
|
|
|
app.logger.error(f"Entschlüsselungsfehler: {e}")
|
|
|
|
|
decrypted_user = ''
|
|
|
|
|
|
|
|
|
|
if decrypted_user != username:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
item_id = appointment.get('Item')
|
|
|
|
|
|
|
|
|
|
if not item_id or str(item_id) in processed_item_ids:
|
|
|
|
|
continue # Skip if we already processed this item or no item ID
|
|
|
|
|
|
|
|
|
|
# Get item details
|
|
|
|
|
item_obj = items_collection.find_one({'_id': ObjectId(item_id)})
|
|
|
|
|
|
|
|
|
|
if not item_id:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if isinstance(item_id, str):
|
|
|
|
|
query_id = ObjectId(item_id)
|
|
|
|
|
else:
|
|
|
|
|
query_id = item_id
|
|
|
|
|
item_obj = items_collection.find_one({'_id': query_id})
|
|
|
|
|
except Exception:
|
|
|
|
|
item_obj = None
|
|
|
|
|
|
|
|
|
|
if item_obj:
|
|
|
|
|
# Convert ObjectId to string for template
|
|
|
|
|
item_obj['_id'] = str(item_obj['_id'])
|
|
|
|
|
|
|
|
|
|
# Add appointment data
|
|
|
|
|
|
|
|
|
|
item_obj['AppointmentData'] = {
|
|
|
|
|
'id': str(appointment['_id']),
|
|
|
|
|
'start': appointment.get('Start'),
|
|
|
|
@@ -10324,55 +10340,43 @@ def my_borrowed_items():
|
|
|
|
|
'period': appointment.get('Period'),
|
|
|
|
|
'status': appointment.get('VerifiedStatus', appointment.get('Status')),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Mark that this item is part of an active appointment
|
|
|
|
|
item_obj['ActiveAppointment'] = True
|
|
|
|
|
|
|
|
|
|
# Add to the list only if not already there
|
|
|
|
|
if str(item_obj['_id']) not in processed_item_ids:
|
|
|
|
|
active_items.append(item_obj)
|
|
|
|
|
processed_item_ids.add(str(item_obj['_id']))
|
|
|
|
|
|
|
|
|
|
# Process planned appointments
|
|
|
|
|
for appointment in planned_ausleihungen:
|
|
|
|
|
item_id = appointment.get('Item')
|
|
|
|
|
|
|
|
|
|
if not item_id:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
item_obj = items_collection.find_one({'_id': ObjectId(item_id)})
|
|
|
|
|
|
|
|
|
|
if item_obj:
|
|
|
|
|
item_obj['_id'] = str(item_obj['_id'])
|
|
|
|
|
|
|
|
|
|
# Add appointment data
|
|
|
|
|
item_obj['AppointmentData'] = {
|
|
|
|
|
'id': str(appointment['_id']),
|
|
|
|
|
'start': appointment.get('Start'),
|
|
|
|
|
'end': appointment.get('End'),
|
|
|
|
|
'notes': appointment.get('Notes'),
|
|
|
|
|
'period': appointment.get('Period'),
|
|
|
|
|
'status': appointment.get('Status'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
planned_items.append(item_obj)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
status = appointment.get('Status')
|
|
|
|
|
if status == 'active':
|
|
|
|
|
item_obj['ActiveAppointment'] = True
|
|
|
|
|
if str(item_obj['_id']) not in processed_item_ids:
|
|
|
|
|
active_items.append(item_obj)
|
|
|
|
|
processed_item_ids.add(str(item_obj['_id']))
|
|
|
|
|
elif status == 'planned':
|
|
|
|
|
planned_items.append(item_obj)
|
|
|
|
|
|
|
|
|
|
all_borrowed_items = list(items_collection.find({'Verfuegbar': False}))
|
|
|
|
|
for item in all_borrowed_items:
|
|
|
|
|
raw_item_user = item.get('User', '')
|
|
|
|
|
try:
|
|
|
|
|
dec_item_user = decrypt_text(raw_item_user) if raw_item_user else ''
|
|
|
|
|
except Exception:
|
|
|
|
|
dec_item_user = ''
|
|
|
|
|
|
|
|
|
|
if dec_item_user == username and str(item['_id']) not in processed_item_ids:
|
|
|
|
|
item['_id'] = str(item['_id'])
|
|
|
|
|
item['ActiveAppointment'] = True
|
|
|
|
|
item['AppointmentData'] = {'status': 'active (no document)'}
|
|
|
|
|
active_items.append(item)
|
|
|
|
|
processed_item_ids.add(item['_id'])
|
|
|
|
|
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
# DEBUG: Log what we're passing to the template
|
|
|
|
|
app.logger.info(f"Passing {len(active_items)} active items and {len(planned_items)} planned items to template")
|
|
|
|
|
if planned_items:
|
|
|
|
|
for i, item in enumerate(planned_items):
|
|
|
|
|
app.logger.info(f"Planned item {i+1}: {item['Name']}, Appointment ID: {item['AppointmentData']['id']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# DEBUG Logging
|
|
|
|
|
app.logger.info(
|
|
|
|
|
f"Passing {len(active_items)} active items and {len(planned_items)} planned items to template for user {username}")
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
'my_borrowed_items.html',
|
|
|
|
|
items=active_items,
|
|
|
|
|
planned_items=planned_items,
|
|
|
|
|
user_is_admin=user_is_admin
|
|
|
|
|
planned_items=planned_items
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/push/vapid-key', methods=['GET'])
|
|
|
|
|
def get_vapid_key():
|
|
|
|
|
"""
|
|
|
|
@@ -10612,53 +10616,109 @@ def notifications_unread_status():
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/admin/damaged_items')
|
|
|
|
|
@app.route('/admin/damaged_items')
|
|
|
|
|
def admin_damaged_items():
|
|
|
|
|
"""Admin-Übersicht aller aktiven und vergangenen Ausleihen."""
|
|
|
|
|
"""Admin-Übersicht aller defekten, reparierten oder zerstörten Items."""
|
|
|
|
|
if 'username' not in session:
|
|
|
|
|
flash('Administratorrechte erforderlich.', 'error')
|
|
|
|
|
flash('Anmeldung erforderlich.', 'error')
|
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
|
|
|
|
from modules.inventarsystem.data_protection import decrypt_text
|
|
|
|
|
from bson.objectid import ObjectId
|
|
|
|
|
|
|
|
|
|
client = None
|
|
|
|
|
try:
|
|
|
|
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
|
|
|
|
db = client[MONGODB_DB]
|
|
|
|
|
ausleihungen_col = db['ausleihungen']
|
|
|
|
|
items_col = db['items']
|
|
|
|
|
ausleihungen_col = db['ausleihungen']
|
|
|
|
|
|
|
|
|
|
ausleihungen = list(ausleihungen_col.find().sort('Start', -1))
|
|
|
|
|
# 1. Alle Items suchen, die einen Schaden haben, repariert oder zerstört sind
|
|
|
|
|
query = {
|
|
|
|
|
'$or': [
|
|
|
|
|
{'HasDamage': True},
|
|
|
|
|
{'DamageReports': {'$exists': True, '$not': {'$size': 0}}},
|
|
|
|
|
{'Condition': {'$in': ['destroyed', 'broken', 'damaged', 'repaired', 'fixed']}}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for record in ausleihungen:
|
|
|
|
|
raw_user = record.get('User', '')
|
|
|
|
|
if raw_user:
|
|
|
|
|
record['User'] = decrypt_text(raw_user)
|
|
|
|
|
raw_items = list(items_col.find(query).sort('LastUpdated', -1))
|
|
|
|
|
damaged_items = []
|
|
|
|
|
|
|
|
|
|
item_id = record.get('Item')
|
|
|
|
|
if item_id:
|
|
|
|
|
try:
|
|
|
|
|
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
|
|
|
|
if item_doc:
|
|
|
|
|
if item_doc.get('User'):
|
|
|
|
|
item_doc['User'] = decrypt_text(item_doc['User'])
|
|
|
|
|
|
|
|
|
|
record['ItemDetails'] = item_doc
|
|
|
|
|
except Exception as e:
|
|
|
|
|
app.logger.warning(f"Konnte Item {item_id} für Ausleihe {record.get('_id')} nicht laden: {e}")
|
|
|
|
|
for item in raw_items:
|
|
|
|
|
# Dynamischen Status ermitteln (wie zuvor besprochen)
|
|
|
|
|
condition_value = str(item.get('Condition', '')).strip().lower()
|
|
|
|
|
has_damage_flag = bool(item.get('HasDamage'))
|
|
|
|
|
has_reports = bool(item.get('DamageReports'))
|
|
|
|
|
|
|
|
|
|
if condition_value == 'destroyed':
|
|
|
|
|
status_text = 'Komplett zerstört'
|
|
|
|
|
status_color = 'danger' # Rot
|
|
|
|
|
elif condition_value in ['repaired', 'fixed'] or (has_reports and not has_damage_flag):
|
|
|
|
|
status_text = 'Bereits repariert'
|
|
|
|
|
status_color = 'success' # Grün
|
|
|
|
|
else:
|
|
|
|
|
status_text = 'Aktuelles Problem'
|
|
|
|
|
status_color = 'warning' # Gelb
|
|
|
|
|
|
|
|
|
|
# Schadensberichte analysieren
|
|
|
|
|
damage_reports = item.get('DamageReports', [])
|
|
|
|
|
latest_report = damage_reports[-1] if damage_reports else {}
|
|
|
|
|
|
|
|
|
|
# Prüfen, ob es eine aktive oder geplante Ausleihe für dieses Item gibt
|
|
|
|
|
active_borrow = ausleihungen_col.find_one({
|
|
|
|
|
'Item': item.get('_id'), # Suche mit ObjectId
|
|
|
|
|
'Status': {'$in': ['active', 'planned']}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Falls die ID in der Ausleihen-Collection als String hinterlegt ist (Fallback)
|
|
|
|
|
if not active_borrow:
|
|
|
|
|
active_borrow = ausleihungen_col.find_one({
|
|
|
|
|
'Item': str(item.get('_id')),
|
|
|
|
|
'Status': {'$in': ['active', 'planned']}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Benutzernamen der Ausleihe entschlüsseln
|
|
|
|
|
if active_borrow and active_borrow.get('User'):
|
|
|
|
|
active_borrow['User'] = decrypt_text(active_borrow['User'])
|
|
|
|
|
|
|
|
|
|
# Direkt am Item hinterlegten Nutzer entschlüsseln
|
|
|
|
|
item_user = decrypt_text(item.get('User')) if item.get('User') else ''
|
|
|
|
|
|
|
|
|
|
# Datenstruktur exakt für dein Jinja2 Template aufbauen
|
|
|
|
|
damaged_items.append({
|
|
|
|
|
'id': str(item['_id']),
|
|
|
|
|
'name': item.get('Name', ''),
|
|
|
|
|
'code': item.get('Code_4', ''),
|
|
|
|
|
'item_type': item.get('ItemType', ''),
|
|
|
|
|
'author': item.get('Author', item.get('Autor', '')), # Deckt beide Schreibweisen ab
|
|
|
|
|
'isbn': item.get('ISBN', ''),
|
|
|
|
|
'borrow_user': item_user,
|
|
|
|
|
'damage_count': len(damage_reports),
|
|
|
|
|
'condition': item.get('Condition', '-'),
|
|
|
|
|
'available': item.get('Verfuegbar', False),
|
|
|
|
|
|
|
|
|
|
# Letzte Meldung
|
|
|
|
|
'latest_damage_description': latest_report.get('description', ''),
|
|
|
|
|
'latest_damage_by': latest_report.get('reported_by', ''),
|
|
|
|
|
'latest_damage_at': latest_report.get('reported_at', ''),
|
|
|
|
|
|
|
|
|
|
# Status & Ausleihe
|
|
|
|
|
'status_text': status_text,
|
|
|
|
|
'status_color': status_color,
|
|
|
|
|
'active_borrow': active_borrow
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
'admin_damaged_items.html',
|
|
|
|
|
ausleihungen=ausleihungen,
|
|
|
|
|
'admin_damaged_items.html',
|
|
|
|
|
damaged_items=damaged_items,
|
|
|
|
|
library_module_enabled=cfg.MODULES.is_enabled('library'),
|
|
|
|
|
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
|
|
|
|
|
mail_module_enabled=cfg.MODULES.is_enabled('mail')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
app.logger.error(f"Fehler beim Laden der Ausleihen-Verwaltung: {exc}")
|
|
|
|
|
flash('Fehler beim Laden der Ausleihen-Übersicht.', 'error')
|
|
|
|
|
app.logger.error(f"Fehler beim Laden der defekten Items: {exc}")
|
|
|
|
|
flash('Fehler beim Laden der Übersicht.', 'error')
|
|
|
|
|
return redirect(url_for('home_admin'))
|
|
|
|
|
finally:
|
|
|
|
|
if client:
|
|
|
|
@@ -10783,7 +10843,7 @@ def admin_school_settings():
|
|
|
|
|
flash('Bitte melden Sie sich mit einem administrativen Konto an.', 'error')
|
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
|
current_permissions = us.get_effective_permissions(session['username'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not current_permissions['actions'].get('can_manage_settings', False):
|
|
|
|
|
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
|
|
|
|
|
return redirect(url_for('home_admin'))
|
|
|
|
@@ -10827,21 +10887,22 @@ def admin_school_settings():
|
|
|
|
|
school_info['logo_path'] = saved_logo_filename
|
|
|
|
|
school_info['logo_thumb'] = saved_thumb_filename or ''
|
|
|
|
|
|
|
|
|
|
# remove previous files if different
|
|
|
|
|
fs = get_gridfs()
|
|
|
|
|
# Remove previous logo files from GridFS 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
|
|
|
|
|
try:
|
|
|
|
|
old_logo_file = fs.find_one({'filename': previous_logo_path})
|
|
|
|
|
if old_logo_file:
|
|
|
|
|
fs.delete(old_logo_file._id)
|
|
|
|
|
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
|
|
|
|
|
try:
|
|
|
|
|
old_thumb_file = fs.find_one({'filename': previous_thumb_path})
|
|
|
|
|
if old_thumb_file:
|
|
|
|
|
fs.delete(old_thumb_file._id)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
flash(f'Logo konnte nicht hochgeladen werden: {exc}', 'error')
|
|
|
|
|
return redirect(url_for('admin_school_settings'))
|
|
|
|
@@ -10867,7 +10928,7 @@ def admin_school_settings():
|
|
|
|
|
try:
|
|
|
|
|
updated_school = cfg.update_school_info(school_info)
|
|
|
|
|
current_school = updated_school
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Also process filter names
|
|
|
|
|
filter_name_1 = request.form.get('filter_name_1')
|
|
|
|
|
if filter_name_1: it.set_filter_name(1, filter_name_1.strip())
|
|
|
|
|