Compare commits

...

22 Commits

Author SHA1 Message Date
Aiirondev_dev 23d9d0871a feat: implement customizable filter names and update management interfaces 2026-05-18 23:20:52 +02:00
Aiirondev_dev 576d31a384 feat: add edit functionality for predefined filter values in filter management 2026-05-18 22:01:37 +02:00
Aiirondev_dev c43bb30301 feat: enhance bookmark and favorite button styles for dark mode 2026-05-18 18:30:14 +02:00
Aiirondev_dev 7eeea1d319 feat: update dark theme colors and add border radius for improved aesthetics 2026-05-18 17:49:42 +02:00
Aiirondev_dev 56486bcbb2 feat: update address details in impressum section 2026-05-18 17:39:19 +02:00
Aiirondev_dev 2c3fb779ce feat: update styles for dark mode and enhance filter handling in admin template 2026-05-18 17:35:02 +02:00
Aiirondev_dev 299d11c1d0 feat: implement DOM virtualization for item cards in main and admin templates 2026-05-18 17:06:46 +02:00
Aiirondev_dev 4d1f95cb1f feat: improve filter3 options handling and enhance item card visibility 2026-05-18 16:53:11 +02:00
Aiirondev_dev 6b197fd9d1 feat: add third filter management and update filter handling logic 2026-05-18 16:40:11 +02:00
Aiirondev_dev 58064ee249 Changes to be committed:
modified:   Web/user.py
2026-05-13 16:05:13 +02:00
Aiirondev_dev f7573b9a62 feat: remove back navigation link from upload admin template 2026-05-12 09:32:13 +02:00
Aiirondev_dev 85d758fadb feat: update application image version to v0.7.42 2026-05-12 09:08:38 +02:00
Aiirondev_dev 212dd2abcf feat: Add 404 error handling for missing templates and files 2026-05-12 09:07:37 +02:00
Aiirondev_dev 689819df08 feat: update application image version and enhance docker-compose configuration 2026-05-12 08:16:04 +02:00
Aiirondev_dev 457d32a087 feat: Module fallback redirects and duplicate item modal redirects 2026-05-12 08:00:02 +02:00
Aiirondev_dev cded7b02fe fix(modal): add functionality to open edit modal for existing items based on URL parameters 2026-05-12 07:37:26 +02:00
Aiirondev_dev f314b5ae70 fix(tenant): apply DB aliases to tenant config resolution to prevent module configuration overrides by defaults 2026-05-11 21:08:37 +02:00
Aiirondev_dev 77c4e1b4c5 fix(modules): restore accidentally deleted routes (library_view, etc.) and ensure STUDENT_MAX_BORROW_DAYS is set 2026-05-11 20:37:35 +02:00
Aiirondev_dev e17f098c47 Changes to be committed:
deleted:    .scheduler_lock
2026-05-11 20:25:48 +02:00
Aiirondev_dev f41acc78cd fix(modules): patch routing redirect logic and restore missing STUDENT_MAX_BORROW_DAYS variable 2026-05-11 20:25:19 +02:00
Aiirondev_dev a7a5341961 refactor(app): replace direct module enabled checks with centralized module management calls 2026-05-11 19:41:25 +02:00
Aiirondev_dev 9ace2e2e5e feat(modules): implement module registry for dynamic module management and path matching 2026-05-11 15:49:40 +02:00
22 changed files with 922 additions and 241 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTP_PORTS=10000
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
INVENTAR_APP_CPUS=1.0
INVENTAR_APP_MEM_LIMIT=384m
INVENTAR_APP_MEM_SWAP_LIMIT=768m
+1
View File
@@ -0,0 +1 @@
v0.7.42
View File
+193 -128
View File
@@ -25,9 +25,10 @@ Features:
- Booking and reservation of items
"""
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file
from flask import Flask, render_template, request, redirect, url_for, session, flash, send_from_directory, get_flashed_messages, jsonify, Response, make_response, send_file, abort
from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
from jinja2 import TemplateNotFound
import user as us
import items as it
import ausleihung as au
@@ -269,40 +270,6 @@ def _get_csrf_token():
def _is_csrf_exempt_request():
return request.method in {'GET', 'HEAD', 'OPTIONS', 'TRACE'}
def _is_library_module_path(path):
"""Return True when the current request path belongs to the library module."""
if not path:
return False
library_prefixes = (
'/library',
'/library_',
'/student_cards',
)
return path.startswith(library_prefixes)
def _is_inventory_module_path(path):
"""Return True when the current request path belongs to the inventory module."""
if not path:
return False
if path == '/' or path.startswith('/home'):
return True
inventory_prefixes = (
'/scanner',
'/inventory',
'/upload_admin',
'/manage_filters',
'/manage_locations',
'/admin_borrowings',
'/admin_damaged_items',
'/admin/borrowings',
'/admin/damaged_items',
'/terminplan',
)
return path.startswith(inventory_prefixes)
@app.before_request
def _enforce_csrf_protection():
@@ -387,14 +354,28 @@ def _enforce_active_session_user():
@app.before_request
def _enforce_module_access():
endpoint = request.endpoint or ''
if endpoint == 'static' or endpoint.startswith('static') or request.path.startswith('/api/'):
if endpoint == 'static' or endpoint.startswith('static'):
return None
if not cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(request.path):
return "Bibliotheks-Modul ist deaktiviert.", 403
if not cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(request.path):
return "Inventar-Modul ist deaktiviert.", 403
for name, matcher in cfg.MODULES._path_matchers.items():
if matcher(request.path) and not cfg.MODULES.is_enabled(name):
msg = {
'library': "Bibliotheks-Modul ist deaktiviert.",
'inventory': "Inventar-Modul ist deaktiviert.",
'student_cards': "Schülerausweis-Modul ist deaktiviert."
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
if request.path.startswith('/api/') or request.is_json:
return jsonify({'ok': False, 'message': msg}), 403
flash(msg, 'info')
if name != 'inventory' and cfg.MODULES.is_enabled('inventory'):
return redirect(url_for('home'))
elif name != 'library' and cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_view'))
return redirect(url_for('my_borrowed_items'))
""" -----------------------------------------------------------After Request Handlers----------------------------------------------------------------------------- """
@@ -432,6 +413,26 @@ def _set_security_headers(response):
return response
@app.errorhandler(TemplateNotFound)
def handle_template_not_found(e):
"""Handle missing template files with a 404 response."""
app.logger.error(f"Template not found: {e.name}")
if request.is_json or request.path.startswith('/api/'):
return jsonify({'error': 'Template not found', 'status': 404}), 404
return abort(404)
@app.errorhandler(404)
def handle_not_found(e):
"""Handle 404 errors with a user-friendly response."""
app.logger.warning(f"404 Not Found: {request.path}")
if request.is_json or request.path.startswith('/api/'):
return jsonify({'error': 'Not found', 'status': 404}), 404
if 'username' in session:
return render_template('error.html', error_code=404, error_message='Seite nicht gefunden.'), 404
return redirect(url_for('login'))
def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
if request.is_json or request.path.startswith('/api/') or request.path in {'/download_book_cover', '/proxy_image', '/log_mobile_issue'}:
return jsonify({'error': message}), 400
@@ -440,24 +441,18 @@ def _csrf_error_response(message='CSRF token fehlt oder ist ungültig.'):
def _get_current_module(path):
"""Resolve the active UI module for navbar separation."""
if cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(path):
session['last_module'] = 'library'
return 'library'
if cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(path):
session['last_module'] = 'inventory'
return 'inventory'
mod = cfg.MODULES.get_module_for_path(path)
if mod:
session['last_module'] = mod
return mod
last_module = session.get('last_module')
if last_module == 'library' and cfg.LIBRARY_MODULE_ENABLED:
return 'library'
if last_module == 'inventory' and cfg.INVENTORY_MODULE_ENABLED:
return 'inventory'
if last_module and cfg.MODULES.is_enabled(last_module):
return last_module
# Default fallback: prefer inventory if enabled, otherwise library, else inventory
if cfg.INVENTORY_MODULE_ENABLED:
if cfg.MODULES.is_enabled('inventory'):
return 'inventory'
return 'library' if cfg.LIBRARY_MODULE_ENABLED else 'inventory'
return 'library' if cfg.MODULES.is_enabled('library') else 'inventory'
"""---------------------------------------------User Access Permissions----------------------------------------------------------------------------- """
@@ -1063,9 +1058,9 @@ def inject_version():
'csrf_token': csrf_token,
'CURRENT_MODULE': current_module,
'school_periods': SCHOOL_PERIODS,
'inventory_module_enabled': cfg.INVENTORY_MODULE_ENABLED,
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
'library_module_enabled': cfg.MODULES.is_enabled('library'),
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),
'is_admin': is_admin,
'unread_notification_count': unread_notification_count,
'current_permissions': current_permissions,
@@ -1750,7 +1745,7 @@ def _upload_student_cards_excel():
flash('Administratorrechte erforderlich.', 'error')
return redirect(url_for('home_admin'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -1945,7 +1940,7 @@ def _upload_excel_items(scope='inventory'):
fallback_route = 'library_admin' if is_library_scope else 'upload_admin'
if is_library_scope:
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home'))
@@ -2643,8 +2638,8 @@ def home():
flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login'))
if not cfg.INVENTORY_MODULE_ENABLED:
if cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('inventory'):
if cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_view'))
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
@@ -2653,10 +2648,11 @@ def home():
return render_template(
'main.html',
username=session['username'],
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
open_item=request.args.get('open_item')
)
else:
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
@@ -2682,8 +2678,8 @@ def home_admin():
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.INVENTORY_MODULE_ENABLED:
if cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('inventory'):
if cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_admin'))
else:
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
@@ -2694,11 +2690,12 @@ def home_admin():
return render_template(
'main_admin.html',
username=session['username'],
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS,
school_info=_get_school_info_for_export(),
open_item=request.args.get('open_item')
)
@@ -2713,8 +2710,8 @@ def tutorial_page():
'tutorial.html',
username=session['username'],
is_admin=us.check_admin(session['username']),
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
)
@@ -2732,16 +2729,16 @@ def library_view():
if 'username' not in session:
flash('Bitte mit registriertem Konto anmelden!', 'error')
return redirect(url_for('login'))
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home'))
return render_template(
'library_table.html',
username=session['username'],
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
is_admin=us.check_admin(session['username']),
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
)
@@ -2756,7 +2753,7 @@ def library_loans_admin():
if not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -2857,8 +2854,8 @@ def library_loans_admin():
'library_borrowings_admin.html',
loan_entries=loan_entries,
damaged_items=damaged_items,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as e:
app.logger.error(f"Error loading library loans admin view: {e}")
@@ -2993,9 +2990,9 @@ def api_library_scan_action():
"""
if 'username' not in session:
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
return jsonify({'ok': False, 'message': 'Schülerausweis-Modul ist deaktiviert.'}), 403
payload = request.get_json(silent=True) or {}
@@ -3199,7 +3196,7 @@ def api_library_item_update(item_id):
return jsonify({'ok': False, 'message': 'Nicht angemeldet.'}), 401
if not us.check_admin(session['username']):
return jsonify({'ok': False, 'message': 'Administratorrechte erforderlich.'}), 403
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
return jsonify({'ok': False, 'message': 'Bibliotheks-Modul ist deaktiviert.'}), 403
payload = request.get_json(silent=True) or {}
@@ -3330,8 +3327,8 @@ def upload_admin():
'upload_admin.html',
username=session['username'],
duplicate_data=duplicate_data,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
show_library_features=False,
upload_mode='item',
page_title='Artikel hochladen',
@@ -3352,7 +3349,7 @@ def library_admin():
if not _action_access_allowed(permissions, 'can_insert'):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -3396,8 +3393,8 @@ def library_admin():
'upload_admin.html',
username=session['username'],
duplicate_data=duplicate_data,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
show_library_features=True,
upload_mode='library',
page_title='Bücher hochladen',
@@ -3417,7 +3414,7 @@ def student_cards_admin():
if not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -3543,8 +3540,8 @@ def student_cards_admin():
edit_mode=edit_mode,
form_data=form_data,
config={'default': cfg.STUDENT_DEFAULT_BORROW_DAYS},
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@@ -3560,7 +3557,7 @@ def student_cards_print():
if not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -3592,7 +3589,7 @@ def student_card_barcode_print():
if not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -3623,7 +3620,7 @@ def student_card_barcode_download():
if not us.check_admin(session['username']):
flash('Zugriff verweigert.', 'error')
return redirect(url_for('login'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -3797,7 +3794,7 @@ def student_card_single_barcode_download(card_id):
if not us.check_admin(session['username']):
flash('Zugriff verweigert.', 'error')
return redirect(url_for('login'))
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Schülerausweis-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -4666,7 +4663,7 @@ def upload_item():
item_isbn = ''
item_type = 'general'
if cfg.LIBRARY_MODULE_ENABLED:
if cfg.MODULES.is_enabled('library'):
item_isbn = normalize_and_validate_isbn(isbn_raw)
if isbn_raw and not item_isbn:
error_msg = 'Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.'
@@ -4678,7 +4675,7 @@ def upload_item():
item_type = 'book'
if upload_mode == 'library':
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
error_msg = 'Bibliotheks-Modul ist deaktiviert.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
@@ -4704,12 +4701,26 @@ def upload_item():
# Check if base code is unique for single-item uploads
if code_4 and item_count == 1 and not it.is_code_unique(code_4[0]):
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
existing_item = it._get_items_collection().find_one({"code_4": code_4[0]})
if existing_item:
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
if is_mobile:
return jsonify({
'success': False,
'message': error_msg,
'existing_item_id': str(existing_item['_id']),
'redirect_to_item': True
}), 400
flash(error_msg, 'info')
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
else:
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
error_msg = 'Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# Validate optional per-item codes
if individual_codes:
@@ -5853,7 +5864,7 @@ def edit_item(id):
item_isbn = ''
item_type = 'general'
if cfg.LIBRARY_MODULE_ENABLED:
if cfg.MODULES.is_enabled('library'):
item_isbn = normalize_and_validate_isbn(isbn_raw)
if isbn_raw and not item_isbn:
flash('Ungültige ISBN. Bitte ISBN-10 oder ISBN-13 verwenden.', 'error')
@@ -6136,7 +6147,7 @@ def ausleihen(id):
username = session['username']
requested_return_to = (request.form.get('return_to') or '').strip().lower()
if requested_return_to == 'library' and cfg.LIBRARY_MODULE_ENABLED:
if requested_return_to == 'library' and cfg.MODULES.is_enabled('library'):
redirect_target = 'library_view'
else:
redirect_target = 'home_admin' if us.check_admin(username) else 'home'
@@ -6154,7 +6165,7 @@ def ausleihen(id):
# Library media can only be borrowed with a valid student card.
if is_library_item:
if not cfg.STUDENT_CARDS_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('student_cards'):
flash('Bibliotheksmedien können nur mit aktivem Schülerausweis-Modul ausgeliehen werden.', 'error')
return redirect(url_for(redirect_target))
if not student_card_id:
@@ -6183,7 +6194,7 @@ def ausleihen(id):
if client:
client.close()
if cfg.STUDENT_CARDS_MODULE_ENABLED:
if cfg.MODULES.is_enabled('student_cards'):
duration_raw = (request.form.get('borrow_duration_days') or '').strip()
if duration_raw:
try:
@@ -7103,8 +7114,8 @@ def register():
return redirect(url_for('home'))
return render_template(
'register.html',
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
student_default_borrow_days=cfg.STUDENT_DEFAULT_BORROW_DAYS,
student_max_borrow_days=cfg.STUDENT_MAX_BORROW_DAYS
)
@@ -7170,8 +7181,8 @@ def user_del():
return render_template(
'user_del.html',
users=users_list,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@@ -7311,8 +7322,8 @@ def admin_borrowings():
return render_template(
'admin_borrowings.html',
entries=entries,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
"""-----------------------------------------------------------Audit Routes-------------------------------------------------------"""
@@ -7373,8 +7384,8 @@ def admin_audit_dashboard():
'admin_audit.html',
verify_result=verify_result,
audit_rows=audit_rows,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as exc:
app.logger.error(f"Error loading audit dashboard: {exc}")
@@ -8120,7 +8131,7 @@ def library_item_invoices(item_id):
if 'username' not in session or not us.check_admin(session['username']):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
@@ -8188,8 +8199,8 @@ def library_item_invoices(item_id):
'isbn': item_doc.get('ISBN', ''),
},
invoices=entries,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as e:
app.logger.error(f"Error loading invoice history for item {item_id}: {e}")
@@ -8515,8 +8526,8 @@ def logs():
return render_template(
'logs.html',
items=formatted_items,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@@ -8576,13 +8587,19 @@ def manage_filters():
# Get predefined filter values
filter1_values = it.get_predefined_filter_values(1)
filter2_values = it.get_predefined_filter_values(2)
filter3_values = it.get_predefined_filter_values(3)
# Get custom filter names
filter_names = it.get_filter_names()
return render_template(
'manage_filters.html',
filter1_values=filter1_values,
filter2_values=filter2_values,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
filter3_values=filter3_values,
filter_names=filter_names,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@app.route('/add_filter_value/<int:filter_num>', methods=['POST'])
@@ -8644,6 +8661,41 @@ def remove_filter_value(filter_num, value):
return redirect(url_for('manage_filters'))
@app.route('/edit_filter_value/<int:filter_num>/<string:old_value>', methods=['POST'])
def edit_filter_value(filter_num, old_value):
"""
Edit a predefined value from the specified filter.
Args:
filter_num (int): Filter number (1, 2 or 3)
old_value (str): Value to edit
Returns:
flask.Response: Redirect to filter management page
"""
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({'success': False, 'error': 'Not authorized'}), 403
new_value = sanitize_form_value(request.form.get('new_value'))
if not new_value:
flash('Bitte geben Sie einen neuen Wert ein', 'error')
return redirect(url_for('manage_filters'))
if new_value == FILTER_SELECT_ALL_TOKEN:
flash('Dieser Wert ist reserviert und kann nicht als Filterwert gespeichert werden.', 'error')
return redirect(url_for('manage_filters'))
# Update the value from the filter
success = it.edit_predefined_filter_value(filter_num, old_value, new_value)
if success:
flash(f'Wert "{old_value}" wurde in "{new_value}" bei Filter {filter_num} geändert', 'success')
else:
flash(f'Fehler beim Ändern des Wertes oder Wert existiert bereits in Filter {filter_num}', 'error')
return redirect(url_for('manage_filters'))
@app.route('/get_predefined_filter_values/<int:filter_num>')
def get_predefined_filter_values(filter_num):
"""
@@ -8713,7 +8765,7 @@ def fetch_book_info(isbn):
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({"error": "Not authorized"}), 403
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try:
@@ -8808,7 +8860,7 @@ def download_book_cover():
return jsonify({"error": "Not authorized"}), 403
if not us.check_admin(session['username']):
return jsonify({"error": "Admin privileges required"}), 403
if not cfg.LIBRARY_MODULE_ENABLED:
if not cfg.MODULES.is_enabled('library'):
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
try:
@@ -9175,8 +9227,8 @@ def notifications_view():
user_notifications=user_notifications,
admin_notifications=admin_notifications,
is_admin_user=is_admin_user,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as exc:
app.logger.error(f"Error loading notifications: {exc}")
@@ -9407,8 +9459,8 @@ def admin_damaged_items():
return render_template(
'admin_damaged_items.html',
damaged_items=damaged_rows,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
except Exception as exc:
app.logger.error(f"Error loading damaged-items admin view: {exc}")
@@ -9511,8 +9563,8 @@ def manage_locations():
return render_template(
'manage_locations.html',
location_values=location_values,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@@ -9610,18 +9662,31 @@ 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())
filter_name_2 = request.form.get('filter_name_2')
if filter_name_2: it.set_filter_name(2, filter_name_2.strip())
filter_name_3 = request.form.get('filter_name_3')
if filter_name_3: it.set_filter_name(3, filter_name_3.strip())
flash('Schulstammdaten wurden erfolgreich gespeichert.', 'success')
except Exception as exc:
app.logger.error(f'Could not update school settings: {exc}\n{traceback.format_exc()}')
flash('Die Schulstammdaten konnten nicht gespeichert werden.', 'error')
# Get current filter names to display in form
filter_names = it.get_filter_names()
return render_template(
'admin_school_settings.html',
school_info=current_school,
filter_names=filter_names,
tenant_id=tenant_id,
tenant_db=tenant_db,
library_module_enabled=cfg.LIBRARY_MODULE_ENABLED,
student_cards_module_enabled=cfg.STUDENT_CARDS_MODULE_ENABLED,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
)
@app.route('/check_code_unique/<code>')
+86 -3
View File
@@ -570,7 +570,10 @@ def get_primary_filters():
items = db['items']
filters = [f for f in items.distinct('Filter', _active_record_query(_non_library_query())) if f]
client.close()
return filters
# Add predefined values
predefined = get_predefined_filter_values(1)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving primary filters: {e}")
return []
@@ -589,7 +592,10 @@ def get_secondary_filters():
items = db['items']
filters = [f for f in items.distinct('Filter2', _active_record_query(_non_library_query())) if f]
client.close()
return filters
# Add predefined values
predefined = get_predefined_filter_values(2)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving secondary filters: {e}")
return []
@@ -608,7 +614,10 @@ def get_tertiary_filters():
items = db['items']
filters = [f for f in items.distinct('Filter3', _active_record_query(_non_library_query())) if f]
client.close()
return filters
# Add predefined values
predefined = get_predefined_filter_values(3)
return sorted(list(set(filters + predefined)))
except Exception as e:
print(f"Error retrieving tertiary filters: {e}")
return []
@@ -785,6 +794,80 @@ def remove_predefined_filter_value(filter_num, value):
return result.modified_count > 0
def edit_predefined_filter_value(filter_num, old_value, new_value):
"""
Edit a predefined value from a filter and update all matching items.
Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
old_value (str): Value to replace
new_value (str): New value
Returns:
bool: True if value was updated, False otherwise
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
# Check if the new value already exists
existing = filter_presets.find_one({
'filter_num': filter_num,
'values': new_value
})
if existing and old_value != new_value:
client.close()
return False
# Update the value in the filter
result = filter_presets.update_one(
{'filter_num': filter_num, 'values': old_value},
{'$set': {'values.$': new_value}}
)
if result.modified_count > 0:
items = db['items']
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
# Also update all items that use this filter
items.update_many(
{filter_field: old_value},
{'$set': {filter_field: new_value}}
)
client.close()
return result.modified_count > 0
def get_filter_names():
"""Get customized filter category names."""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names_doc = db.settings.find_one({'setting_type': 'filter_names'})
client.close()
if names_doc and 'names' in names_doc:
return names_doc['names']
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
}
def set_filter_name(filter_num, name):
"""Set custom name for a filter category."""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = get_filter_names()
names[str(filter_num)] = name
db.settings.update_one(
{'setting_type': 'filter_names'},
{'$set': {'names': names}},
upsert=True
)
client.close()
return True
# === LOCATION MANAGEMENT ===
def get_predefined_locations():
+26
View File
@@ -0,0 +1,26 @@
from typing import Dict, Any
class ModuleRegistry:
def __init__(self):
self._modules: Dict[str, Any] = {}
self._path_matchers = {}
def register(self, name: str, settings_bool, path_matcher=lambda path: False):
self._modules[name] = settings_bool
self._path_matchers[name] = path_matcher
def is_enabled(self, name: str) -> bool:
if name not in self._modules:
return False
return bool(self._modules[name])
def get_all_status(self) -> Dict[str, bool]:
return {name: bool(sys_bool) for name, sys_bool in self._modules.items()}
def get_module_for_path(self, path: str) -> str:
for name, matcher in self._path_matchers.items():
if self.is_enabled(name) and matcher(path):
return name
return None
registry = ModuleRegistry()
+22 -1
View File
@@ -205,11 +205,32 @@ class _TenantAwareBool:
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
from module_registry import registry as MODULES
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
def _match_inventory(path):
if not path: return False
if path == '/' or path.startswith('/home'): return True
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items', '/terminplan'))
def _match_library(path):
if not path: return False
return path.startswith(('/library', '/library_', '/student_cards'))
def _match_student_cards(path):
if not path: return False
return path.startswith(('/student_cards'))
# Register core modules into the pipeline
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
MODULES.register('library', LIBRARY_MODULE_ENABLED, _match_library)
MODULES.register('student_cards', STUDENT_CARDS_MODULE_ENABLED, _match_student_cards)
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ["modules", "student_cards", "max_borrow_days"], DEFAULTS["modules"]["student_cards"]["max_borrow_days"]))
# Upload/paths
ALLOWED_EXTENSIONS = set(_get(_conf, ['allowed_extensions'], DEFAULTS['upload']['allowed_extensions']))
+8 -8
View File
@@ -10,13 +10,13 @@
.planned-appointments-panel {
margin: 15px 0;
padding: 15px;
background-color: #e1f5fe;
background-color: var(--ui-surface-soft, #e1f5fe);
border-left: 4px solid #03a9f4;
border-radius: 4px;
}
.planned-appointments-panel h4 {
color: #0277bd;
color: var(--ui-title, #0277bd);
margin-top: 0;
margin-bottom: 10px;
font-size: 1.1em;
@@ -30,9 +30,9 @@
.appointment-item {
padding: 10px;
background-color: #f9fdff;
background-color: var(--ui-surface, #f9fdff);
border-radius: 4px;
border: 1px solid #b3e5fc;
border: 1px solid var(--ui-border, #b3e5fc);
}
.appointment-header {
@@ -44,7 +44,7 @@
.appointment-date {
font-weight: bold;
color: #0288d1;
color: var(--ui-title, #0288d1);
}
.appointment-period {
@@ -86,14 +86,14 @@
.appointment-user {
margin-left: auto;
font-style: italic;
color: #546e7a;
color: var(--ui-text-muted, #546e7a);
}
.appointment-notes {
font-size: 0.9em;
color: #455a64;
color: var(--ui-text, #455a64);
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #e1f5fe;
border-top: 1px solid var(--ui-border, #e1f5fe);
font-style: italic;
}
+112 -10
View File
@@ -22,16 +22,21 @@
}
:root[data-theme="dark"] {
--ui-bg: #0f172a;
--ui-bg-accent: #1e293b;
/* Tiefes Blau-Theme (Dark Blue) */
--ui-bg: #0b1120;
--ui-bg-accent: #111827;
--ui-surface: #1e293b;
--ui-surface-soft: #25314a;
--ui-border: #334155;
--ui-text: #f1f5f9;
--ui-surface-soft: #334155;
--ui-border: #475569;
--ui-text: #f8fafc;
--ui-text-muted: #94a3b8;
--ui-title: #ffffff;
--ui-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
--ui-shadow-md: 0 10px 24px rgba(0, 0, 0, 0.5);
--ui-shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
--ui-shadow-md: 0 10px 30px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
/* Abgerundetes Format im gesamten Dark Mode */
--ui-radius: 20px;
--ui-btn-radius: 99px; /* Pillenform für Buttons */
}
/* Safe Area Support for iPad notches, Dynamic Island, and rounded corners */
@@ -57,9 +62,17 @@ body {
:root[data-theme="dark"] input,
:root[data-theme="dark"] select,
:root[data-theme="dark"] textarea {
background-color: var(--ui-surface-soft) !important;
background-color: var(--ui-bg) !important;
color: var(--ui-text) !important;
border-color: var(--ui-border) !important;
border: 1px solid var(--ui-border) !important;
border-radius: 12px !important;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3) !important;
}
:root[data-theme="dark"] input:focus,
:root[data-theme="dark"] select:focus,
:root[data-theme="dark"] textarea:focus {
border-color: #52a8ff !important;
box-shadow: 0 0 0 2px rgba(82, 168, 255, 0.2), inset 0 1px 2px rgba(0,0,0,0.3) !important;
}
body {
@@ -295,7 +308,7 @@ select:focus {
/* Modal dialog styling */
.modal-dialog-white {
background: white;
background: var(--ui-surface);
padding: 20px;
border-radius: 5px;
text-align: center;
@@ -896,3 +909,92 @@ html, body {
padding-bottom: env(safe-area-inset-bottom);
}
}
/* ========================================================= */
/* UNIVERSAL DARK MODE OVERRIDES FOR HARDCODED HTML STYLES */
/* ========================================================= */
:root[data-theme="dark"] .bg-white,
:root[data-theme="dark"] .bg-light,
:root[data-theme="dark"] [style*="background-color: #fff"],
:root[data-theme="dark"] [style*="background-color: #ffffff"],
:root[data-theme="dark"] [style*="background-color: #f8f9fa"],
:root[data-theme="dark"] [style*="background-color: #f5f5f5"],
:root[data-theme="dark"] [style*="background: #fff"],
:root[data-theme="dark"] [style*="background: #ffffff"],
:root[data-theme="dark"] [style*="background: #f8f9fa"],
:root[data-theme="dark"] [style*="background: #f5f5f5"],
:root[data-theme="dark"] [style*="background: white"],
:root[data-theme="dark"] [style*="background-color: white"],
:root[data-theme="dark"] .modal-content,
:root[data-theme="dark"] .card,
:root[data-theme="dark"] .dropdown-menu {
background-color: var(--ui-surface) !important;
background: var(--ui-surface) !important;
color: var(--ui-text) !important;
}
:root[data-theme="dark"] .text-dark,
:root[data-theme="dark"] .text-black,
:root[data-theme="dark"] [style*="color: #000"],
:root[data-theme="dark"] [style*="color: rgb(0, 0, 0)"],
:root[data-theme="dark"] [style*="color: black"],
:root[data-theme="dark"] [style*="color: #333"] {
color: var(--ui-text) !important;
}
:root[data-theme="dark"] .text-muted {
color: var(--ui-text-muted) !important;
}
/* Fix table backgrounds that might have hardcoded light colors */
:root[data-theme="dark"] .table-light,
:root[data-theme="dark"] table.table,
:root[data-theme="dark"] table [style*="background: #fff"] {
background-color: var(--ui-bg) !important;
color: var(--ui-text) !important;
}
/* Fix list items */
:root[data-theme="dark"] .list-group-item {
background-color: var(--ui-surface) !important;
color: var(--ui-text) !important;
border-color: var(--ui-border) !important;
}
/* Additional inline style coverage */
:root[data-theme="dark"] [style*="background: #eef4ff"],
:root[data-theme="dark"] [style*="background-color: #eef4ff"],
:root[data-theme="dark"] [style*="background: #e9ecef"],
:root[data-theme="dark"] [style*="background-color: #e9ecef"],
:root[data-theme="dark"] [style*="background: #e2e8f0"],
:root[data-theme="dark"] [style*="background-color: #e2e8f0"],
:root[data-theme="dark"] [style*="background-color: #fefefe"],
:root[data-theme="dark"] [style*="background: #fffdfd"] {
background-color: var(--ui-surface) !important;
background: var(--ui-surface) !important;
color: var(--ui-text) !important;
}
:root[data-theme="dark"] .badge-light,
:root[data-theme="dark"] .bg-light {
background-color: var(--ui-surface-soft) !important;
color: var(--ui-text) !important;
border: 1px solid var(--ui-border);
}
/* Fix Bookmark/Favorite Button in Dark Mode */
:root[data-theme="dark"] .bookmark-btn {
background: var(--ui-surface-soft) !important;
border-color: #f59e0b !important;
color: #f59e0b !important;
}
:root[data-theme="dark"] .bookmark-btn:hover,
:root[data-theme="dark"] .bookmark-btn.active {
background: #f59e0b !important;
color: var(--ui-bg) !important;
}
:root[data-theme="dark"] .favorite-item {
outline: 2px solid #f59e0b !important;
}
+19
View File
@@ -71,6 +71,25 @@
<small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
</div>
<hr style="margin: 24px 0; border: 0; border-top: 1px solid #e5e7eb;">
<h3 style="font-size: 1.1rem; margin-bottom: 16px; color: #111827;">Globales Filter-Wording</h3>
<p style="font-size: 0.9rem; color: #6b7280; margin-bottom: 16px;">Hier kannst du die globalen Bezeichnungen für die drei Haupt-Kategorien (Filter) der Anwendung ändern.</p>
<div class="form-row">
<div class="form-group">
<label for="filter_name_1">Name Filter 1</label>
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fach/Kategorie') }}" placeholder="z. B. Fach/Kategorie">
</div>
<div class="form-group">
<label for="filter_name_2">Name Filter 2</label>
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'System/Bereich') }}" placeholder="z. B. System/Bereich">
</div>
<div class="form-group">
<label for="filter_name_3">Name Filter 3</label>
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Typ/Art') }}" placeholder="z. B. Typ/Art">
</div>
</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>
+1 -1
View File
@@ -329,7 +329,7 @@
}
/* iPad/Tablet responsive adjustments */
@media (max-width: 1024px) {
@media (max-width: 768px) {
.navbar .container-fluid {
gap: 6px;
padding-left: max(env(safe-area-inset-left, 0), 10px);
+75
View File
@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}
{% if error_code == 404 %}
Seite nicht gefunden - Inventarsystem
{% else %}
Fehler - Inventarsystem
{% endif %}
{% endblock %}
{% block content %}
<div class="error-container" style="text-align: center; padding: 60px 20px; min-height: 70vh; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<div style="max-width: 600px;">
<!-- Error Code Display -->
<div style="font-size: 120px; font-weight: bold; color: #ef4444; margin-bottom: 20px;">
{{ error_code }}
</div>
<!-- Error Title -->
<h1 style="font-size: 32px; margin: 20px 0; color: #1f2937;">
{% if error_code == 404 %}
Seite nicht gefunden
{% else %}
Ein Fehler ist aufgetreten
{% endif %}
</h1>
<!-- Error Message -->
<p style="font-size: 18px; color: #6b7280; margin: 20px 0 40px;">
{{ error_message or 'Es tut uns leid, aber die angeforderte Seite konnte nicht gefunden werden.' }}
</p>
<!-- Action Buttons -->
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
<a href="{{ url_for('home') }}" class="button" style="background-color: #3b82f6; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
Zur Startseite
</a>
<a href="javascript:history.back()" class="button" style="background-color: #6b7280; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: 500;">
Zurück
</a>
</div>
<!-- Additional Help -->
<div style="margin-top: 60px; padding: 20px; background-color: #f3f4f6; border-radius: 8px;">
<p style="color: #6b7280; font-size: 14px;">
Wenn Sie denken, dass dies ein Fehler ist, oder Sie Hilfe benötigen,
<a href="{{ url_for('impressum') }}" style="color: #3b82f6; text-decoration: none;">kontaktieren Sie den Administrator</a>.
</p>
</div>
</div>
</div>
<style>
.error-container {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.button:hover {
opacity: 0.9;
transform: translateY(-2px);
transition: all 0.2s ease;
}
</style>
{% endblock %}
+4 -4
View File
@@ -12,7 +12,7 @@
{% block content %}
<div class="container my-4">
<div class="impressum-content bg-white p-4 shadow rounded">
<div class="impressum-content p-4 shadow rounded" style="background: var(--ui-surface); color: var(--ui-text);">
<h1 class="mb-4 text-center">Impressum</h1>
<p class="text-center mb-5">(Angaben gemäß § 5 DDG)</p>
@@ -20,7 +20,7 @@
<h3 class="mb-3">Kontaktinformationen</h3>
<address class="mb-0">
<p><strong>Name:</strong> Invario UG</p>
<p><strong>Adresse:</strong> Musterstraße 123<br>12345 Musterstadt<br>Deutschland</p>
<p><strong>Adresse:</strong> Am Sportplatz 10<br>83052 Bruckmühl<br>Deutschland</p>
</address>
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p>
</div>
@@ -29,8 +29,8 @@
<h3 class="mb-3">Verantwortlich für den Inhalt</h3>
<p>(nach § 18 Abs. 2 MStV)</p>
<p>Invario UG<br>
Musterstraße 123<br>
12345 Musterstadt<br>
Am Sportplatz 10<br>
83052 Bruckmühl<br>
Deutschland</p>
</div>
+78 -9
View File
@@ -751,6 +751,7 @@
let mainItemsSentinel = null;
let mainItemsLoadingIndicator = null;
let mainItemsScrollPrefetchBound = false;
let cardVirtualizationObserver = null;
function ensureMainItemsLoadingIndicator() {
const itemsContainer = document.querySelector('#items-container');
@@ -759,11 +760,14 @@
if (!mainItemsLoadingIndicator) {
mainItemsLoadingIndicator = document.createElement('div');
mainItemsLoadingIndicator.id = 'main-items-loading-indicator';
mainItemsLoadingIndicator.className = 'items-loading-indicator';
mainItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
mainItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
`;
mainItemsLoadingIndicator.style.display = 'flex';
mainItemsLoadingIndicator.style.width = '100%';
}
itemsContainer.appendChild(mainItemsLoadingIndicator);
@@ -1189,6 +1193,57 @@
mainItemsScrollPrefetchBound = true;
}
// Setup DOM virtualization for items
if (!cardVirtualizationObserver) {
cardVirtualizationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const card = entry.target;
if (entry.isIntersecting) {
if (card.dataset.deloaded === 'true') {
card.innerHTML = card.dataset.savedHtml;
card.dataset.deloaded = 'false';
card.classList.remove('is-deloaded');
card.classList.remove('empty-frame');
const favBtn = card.querySelector('.bookmark-btn');
if (favBtn) {
favBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
toggleFavorite(card.dataset.itemId, favBtn, card);
});
}
const cardContent = card.querySelector('.card-content');
if (cardContent) {
cardContent.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container')) return;
e.stopPropagation();
openItemQuick(card.dataset.itemId);
});
}
}
} else {
if (card.dataset.deloaded !== 'true') {
const rect = card.getBoundingClientRect();
if (rect.height > 50) {
card.style.minHeight = rect.height + 'px';
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
card.dataset.savedHtml = card.innerHTML;
card.innerHTML = '';
card.dataset.deloaded = 'true';
card.classList.add('is-deloaded');
card.classList.add('empty-frame');
}
}
}
});
}, {
rootMargin: '30000px 30000px 30000px 30000px'
});
}
// Observe all valid cards
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
if (!mainItemsHasMore) return;
mainItemsObserver = new IntersectionObserver(entries => {
@@ -2419,29 +2474,29 @@
// Add these functions right after the loadFilterState() function
function rebuildFilter3Options() {
if (!allItems || allItems.length === 0) return;
if (!allItems) return;
// Get current filter 1 and filter 2 selections
const filter1Selections = activeFilters.filter1;
const filter2Selections = activeFilters.filter2;
// If both filters are empty, show all filter3 values
// If both filters are empty, show all filter3 values from server
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
// Get all unique filter3 values
const allFilter3Values = new Set();
// Include dynamically added ones from loaded items too, just in case
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
allItems.forEach(item => {
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
(item.Filter3 ? [item.Filter3] : []);
filter3Array.forEach(value => {
if (value && typeof value === 'string' && value.trim() !== '') {
allFilter3Values.add(value);
combinedFilter3Values.add(value);
}
});
});
// Populate filter 3 dropdown with all values
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
return;
}
@@ -3114,6 +3169,8 @@
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
content-visibility: auto;
contain-intrinsic-size: 300px 450px;
}
.item-card:hover {
transform: translateY(-5px);
@@ -4614,6 +4671,18 @@ function openItemQuick(id){
}
}
</style>
{% if open_item %}
<script>
document.addEventListener("DOMContentLoaded", function() {
if (typeof openEditModalFromServer === 'function') {
setTimeout(function() {
openEditModalFromServer('{{ open_item }}');
}, 500);
}
});
</script>
{% endif %}
{% endblock %}
<style>
/* Admin/Main Content Container align */
+107 -21
View File
@@ -403,6 +403,8 @@
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
content-visibility: auto;
contain-intrinsic-size: 300px 450px;
}
.item-card.bulk-selected {
@@ -2738,6 +2740,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
let descSearchIds = null; // Set of matching IDs or null when disabled
let currentUsername = '';
let allItems = []; // Cache for all items
let allFilter1Values = new Set();
let allFilter2Values = new Set();
let allFilter3Values = new Set();
let bulkDeleteSelection = new Set();
let bulkDeleteDrawerOpen = false;
const studentCardsModuleEnabled = {{ 'true' if student_cards_module_enabled else 'false' }};
@@ -2876,30 +2881,29 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
function rebuildFilter3Options() {
if (!allItems || allItems.length === 0) return;
if (!allItems) return;
// Get current filter 1 and filter 2 selections
const filter1Selections = activeFilters.filter1;
const filter2Selections = activeFilters.filter2;
// If both filters are empty, show all filter3 values
// If both filters are empty, show all filter3 values from server
if (filter1Selections.length === 0 && filter2Selections.length === 0) {
// Get all unique filter3 values
const allFilter3Values = new Set();
// Include dynamically added ones from loaded items too, just in case
const combinedFilter3Values = new Set(typeof allFilter3Values !== 'undefined' ? [...allFilter3Values] : []);
allItems.forEach(item => {
const filter3Array = Array.isArray(item.Filter3) ? item.Filter3 :
(item.Filter3 ? [item.Filter3] : []);
(item.Filter3 ? [item.Filter3] : []);
filter3Array.forEach(value => {
if (value && typeof value === 'string' && value.trim() !== '') {
allFilter3Values.add(value);
combinedFilter3Values.add(value);
}
});
});
// Populate filter 3 dropdown with all values
populateFilterOptions('filter3-options', [...allFilter3Values].sort(), 3);
return;
populateFilterOptions('filter3-options', [...combinedFilter3Values].sort(), 3);
}
// Filter items based on filter1 and filter2 selections
@@ -3258,6 +3262,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
// Load predefined filter values for dropdowns
loadPredefinedFilterValues(1);
loadPredefinedFilterValues(2);
loadPredefinedFilterValues(3);
// Set up edit form submission
setupEditFormSubmission();
@@ -3282,11 +3287,24 @@ document.addEventListener('DOMContentLoaded', ()=>{
});
});
// Fetch user status to get current username
fetch('/user_status')
.then(response => response.json())
.then(data => {
currentUsername = data.username;
// Fetch user status and global filters
Promise.all([
fetch('/user_status').then(response => response.json()),
fetch('/get_filter').then(response => response.json())
])
.then(([userData, filterData]) => {
currentUsername = userData.username;
if (filterData.filter1) {
allFilter1Values = new Set(filterData.filter1.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
if (filterData.filter2) {
allFilter2Values = new Set(filterData.filter2.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
if (filterData.filter3) {
allFilter3Values = new Set(filterData.filter3.filter(value => value && typeof value === 'string' && value.trim() !== ''));
}
// Now load items with username information
loadItems();
// Setup card images display after a short delay to ensure items are loaded
@@ -3295,7 +3313,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
}, 300);
})
.catch(error => {
console.error('Error fetching user status:', error);
console.error('Error fetching user status or filters:', error);
loadItems(); // Load items anyway in case of error
});
@@ -3356,6 +3374,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
let mainAdminItemsLoadingIndicator = null;
let mainAdminItemsScrollPrefetchBound = false;
let totalItemsInSystem = 0;
let cardVirtualizationObserver = null;
function ensureMainAdminItemsLoadingIndicator() {
const itemsContainer = document.querySelector('#items-container');
@@ -3364,11 +3383,14 @@ document.addEventListener('DOMContentLoaded', ()=>{
if (!mainAdminItemsLoadingIndicator) {
mainAdminItemsLoadingIndicator = document.createElement('div');
mainAdminItemsLoadingIndicator.id = 'main-admin-items-loading-indicator';
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator';
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
mainAdminItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
`;
mainAdminItemsLoadingIndicator.style.display = 'flex';
mainAdminItemsLoadingIndicator.style.width = '100%';
}
itemsContainer.appendChild(mainAdminItemsLoadingIndicator);
@@ -3695,10 +3717,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
});
// Populate filter options
populateFilterOptions('filter1-options', [...filter1Values].sort(), 1);
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
// Populate filter options using server-provided full filter lists when available.
populateFilterOptions('filter1-options', allFilter1Values.size ? [...allFilter1Values].sort() : [...filter1Values].sort(), 1);
populateFilterOptions('filter2-options', allFilter2Values.size ? [...allFilter2Values].sort() : [...filter2Values].sort(), 2);
populateFilterOptions('filter3-options', allFilter3Values.size ? [...allFilter3Values].sort() : [...filter3Values].sort(), 3);
if (!append) {
itemsContainer.scrollLeft = 0;
@@ -3783,6 +3805,58 @@ document.addEventListener('DOMContentLoaded', ()=>{
mainAdminItemsScrollPrefetchBound = true;
}
// Setup DOM virtualization for items
if (!cardVirtualizationObserver) {
cardVirtualizationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const card = entry.target;
if (entry.isIntersecting) {
if (card.dataset.deloaded === 'true') {
card.innerHTML = card.dataset.savedHtml;
card.dataset.deloaded = 'false';
card.classList.remove('is-deloaded');
card.classList.remove('empty-frame');
const favBtn = card.querySelector('.bookmark-btn');
if (favBtn) {
favBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
toggleFavorite(card.dataset.itemId, favBtn, card);
});
}
const cardContent = card.querySelector('.card-content');
if (cardContent) {
cardContent.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A' || e.target.tagName === 'INPUT' ||
e.target.closest('button') || e.target.closest('a') || e.target.closest('.image-container') ||
e.target.closest('.bulk-delete-toggle') || e.target.closest('label')) return;
e.stopPropagation();
openItemQuick(card.dataset.itemId);
});
}
}
} else {
if (card.dataset.deloaded !== 'true') {
const rect = card.getBoundingClientRect();
if (rect.height > 50) {
card.style.minHeight = rect.height + 'px';
card.dataset.itemId = card.querySelector('.card-content')?.dataset.itemId || '';
card.dataset.savedHtml = card.innerHTML;
card.innerHTML = '';
card.dataset.deloaded = 'true';
card.classList.add('is-deloaded');
card.classList.add('empty-frame');
}
}
}
});
}, {
rootMargin: '30000px 30000px 30000px 30000px'
});
}
// Observe all valid cards
document.querySelectorAll('.item-card').forEach(card => cardVirtualizationObserver.observe(card));
if (!mainAdminItemsHasMore) return;
mainAdminItemsObserver = new IntersectionObserver(entries => {
@@ -5716,4 +5790,16 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
}
</style>
{% if open_item %}
<script>
document.addEventListener("DOMContentLoaded", function() {
if (typeof openEditModalFromServer === 'function') {
setTimeout(function() {
openEditModalFromServer('{{ open_item }}');
}, 500);
}
});
</script>
{% endif %}
{% endblock %}
+105 -22
View File
@@ -15,16 +15,16 @@
<h1 class="mb-4">Filterwerte verwalten</h1>
<div class="row">
<!-- Filter 1: Unterrichtsfach -->
<div class="col-md-6">
<!-- Filter 1 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Unterrichtsfach (Filter 1)</h2>
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Fach/Kategorie') }} (Filter 1)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neues Unterrichtsfach..." required>
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
@@ -33,13 +33,25 @@
{% if filter1_values %}
<div class="list-group">
{% for value in filter1_values %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ value }}
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-1-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
@@ -51,16 +63,16 @@
</div>
</div>
<!-- Filter 2: Jahrgangsstufe -->
<div class="col-md-6">
<!-- Filter 2 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Jahrgangsstufe (Filter 2)</h2>
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'System/Bereich') }} (Filter 2)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neue Jahrgangsstufe..." required>
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
@@ -69,13 +81,73 @@
{% if filter2_values %}
<div class="list-group">
{% for value in filter2_values %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ value }}
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-2-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">Keine Werte definiert.</div>
{% endif %}
</div>
</div>
</div>
<!-- Filter 3 -->
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">{{ filter_names.get('3', 'Typ/Art') }} (Filter 3)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=3) }}" class="mb-4">
<div class="input-group">
<input type="text" name="value" class="form-control" placeholder="Neuer Wert..." required>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
<h5 class="mb-3">Vorhandene Werte</h5>
{% if filter3_values %}
<div class="list-group">
{% for value in filter3_values %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-3-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-3-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=3, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-3-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
@@ -97,4 +169,15 @@
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
</div>
</div>
<script>
function toggleEdit(id) {
const el = document.getElementById(id);
if (el.style.display === 'none') {
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
</script>
{% endblock %}
-1
View File
@@ -709,7 +709,6 @@
</style>
<div class="upload-container">
<a href="{{ url_for(back_target|default('home_admin')) }}" class="nav-back-button">← Zurück zur Artikelübersicht</a>
{% if show_library_features %}
<div style="border:1px solid #dbe4ee; border-radius:8px; padding:14px; margin-bottom:16px; background:#f8fbff;">
+4
View File
@@ -96,6 +96,10 @@ def get_tenant_config(tenant_id=None):
if tenant_id in TENANT_REGISTRY:
return TENANT_REGISTRY[tenant_id] or {}
for alias in _tenant_db_aliases(tenant_id):
if alias in TENANT_REGISTRY:
return TENANT_REGISTRY[alias] or {}
return TENANT_REGISTRY.get('default', {}) or {}
+32
View File
@@ -0,0 +1,32 @@
from pymongo import MongoClient
import Web.settings as cfg
def get_filter_names():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = db.settings.find_one({'setting_type': 'filter_names'})
client.close()
if names:
return names.get('names', {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
})
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
}
def set_filter_name(filter_num, name):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = get_filter_names()
names[str(filter_num)] = name
db.settings.update_one(
{'setting_type': 'filter_names'},
{'$set': {'names': names}},
upsert=True
)
client.close()
return True
+1 -7
View File
@@ -498,8 +498,6 @@ def check_nm_pwd(username, password):
finally:
client.close()
return None
def add_user(
username,
@@ -536,16 +534,12 @@ def add_user(
for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value)
alias_first = name if str(name or '').strip() else username
alias_last = last_name if str(last_name or '').strip() else ''
name_alias = build_name_synonym(alias_first, alias_last)
user_doc = {
'Username': username,
'Password': hashing(password),
'Admin': False,
'active_ausleihung': None,
'name': name_alias,
'name': name.strip() if name else '',
'last_name': last_name.strip() if last_name else '',
'IsStudent': bool(is_student),
'PermissionPreset': permission_defaults['preset'],
Regular → Executable
View File
+47 -25
View File
@@ -1,41 +1,63 @@
version: "3.8"
services:
app:
build: .
container_name: inventory-app
image: ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.39
container_name: inventarsystem-app
restart: unless-stopped
environment:
- MONGO_URL=mongodb://mongodb:27017/inventar
- REDIS_URL=redis://redis:6379
- BASE_URL=https://inventar.maximiliangruendinger.de #alle öffentliche subdomains
ports:
- "10000:8000"
depends_on:
- mongodb
- redis
environment:
INVENTAR_MONGODB_HOST: mongodb
INVENTAR_MONGODB_PORT: "27017"
INVENTAR_MONGODB_DB: Inventarsystem
INVENTAR_BACKUP_FOLDER: /data/backups
INVENTAR_LOGS_FOLDER: /data/logs
expose:
- "8000"
volumes:
- ./config.json:/app/config.json:ro
- app_uploads:/app/Web/uploads
- app_thumbnails:/app/Web/thumbnails
- app_previews:/app/Web/previews
- app_qrcodes:/app/Web/QRCodes
- app_backups:/data/backups
- app_logs:/data/logs
mongodb:
image: mongo:latest
container_name: mongodb
image: mongo:7.0
container_name: inventarsystem-mongodb
restart: unless-stopped
volumes:
- mongo_data:/data/db
- mongodb_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:alpine
container_name: redis
image: redis:7-alpine
container_name: inventarsystem-redis
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
# Der Tunnel-Name 'homeserver' muss zu deiner credentials.json passen
command: tunnel run homeserver
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
volumes:
- ./config.yml:/etc/cloudflared/config.yml
- ./credentials.json:/etc/cloudflared/credentials.json
depends_on:
- app
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mongo_data:
mongodb_data:
app_uploads:
app_thumbnails:
app_previews:
app_qrcodes:
app_backups:
app_logs:
redis_data: