Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7d3863fde | |||
| ac0e60fadb | |||
| cbeed68147 | |||
| 715d932dba | |||
| 606e1822d1 | |||
| c0ce80d223 | |||
| 96debef4f4 | |||
| c6c57e4b53 | |||
| 3dc9ecfe5b | |||
| 8e20580552 | |||
| aa47325e80 | |||
| 539cf2b21c |
+1
-1
@@ -1 +1 @@
|
|||||||
v0.8.4
|
v0.8.9
|
||||||
|
|||||||
+1
-1
@@ -32,4 +32,4 @@ RUN if [ "$NUITKA_BUILD" = "1" ]; then \
|
|||||||
WORKDIR /app/Web
|
WORKDIR /app/Web
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ from Web.modules.inventarsystem.data_protection import (
|
|||||||
encrypt_document_fields,
|
encrypt_document_fields,
|
||||||
encrypt_soft_deleted_media_pack,
|
encrypt_soft_deleted_media_pack,
|
||||||
)
|
)
|
||||||
|
from Web.modules.terminplaner.blueprint import appoint_bp as terminplaner_bp
|
||||||
|
|
||||||
# Set base directory and centralized settings
|
# Set base directory and centralized settings
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -109,6 +110,7 @@ app.config['SESSION_COOKIE_SECURE'] = cfg.SSL_ENABLED if os.getenv('INVENTAR_SES
|
|||||||
app.config['PREFERRED_URL_SCHEME'] = 'https' if app.config['SESSION_COOKIE_SECURE'] else 'http'
|
app.config['PREFERRED_URL_SCHEME'] = 'https' if app.config['SESSION_COOKIE_SECURE'] else 'http'
|
||||||
# app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated
|
# app.config['QR_CODE_FOLDER'] = cfg.QR_CODE_FOLDER # QR Code storage deactivated
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
app.register_blueprint(terminplaner_bp, url_prefix='/terminplaner')
|
||||||
|
|
||||||
"""--------------------------------------------------------------Path Init-------------------------------------------------------"""
|
"""--------------------------------------------------------------Path Init-------------------------------------------------------"""
|
||||||
|
|
||||||
@@ -1259,6 +1261,7 @@ def inject_version():
|
|||||||
'CURRENT_MODULE': current_module,
|
'CURRENT_MODULE': current_module,
|
||||||
'school_periods': SCHOOL_PERIODS,
|
'school_periods': SCHOOL_PERIODS,
|
||||||
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
|
'inventory_module_enabled': cfg.MODULES.is_enabled('inventory'),
|
||||||
|
'terminplan_module_enabled': cfg.MODULES.is_enabled('terminplan'),
|
||||||
'library_module_enabled': cfg.MODULES.is_enabled('library'),
|
'library_module_enabled': cfg.MODULES.is_enabled('library'),
|
||||||
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),
|
'student_cards_module_enabled': cfg.MODULES.is_enabled('student_cards'),
|
||||||
'is_admin': is_admin,
|
'is_admin': is_admin,
|
||||||
@@ -7564,6 +7567,10 @@ def terminplan():
|
|||||||
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')
|
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'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if not cfg.MODULES.is_enabled('terminplan'):
|
||||||
|
flash('Der Terminplaner ist deaktiviert.', 'info')
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
# Make sure the template exists
|
# Make sure the template exists
|
||||||
template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html')
|
template_path = os.path.join(BASE_DIR, 'templates', 'terminplan.html')
|
||||||
if not os.path.exists(template_path):
|
if not os.path.exists(template_path):
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ DEFAULTS = {
|
|||||||
'inventory': {
|
'inventory': {
|
||||||
'enabled': True
|
'enabled': True
|
||||||
},
|
},
|
||||||
|
'terminplan': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
'library': {
|
'library': {
|
||||||
'enabled': False
|
'enabled': False
|
||||||
},
|
},
|
||||||
@@ -272,13 +275,20 @@ class _TenantAwareBool:
|
|||||||
from Web.modules.module_registry import registry as MODULES
|
from Web.modules.module_registry import registry as MODULES
|
||||||
|
|
||||||
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
||||||
|
TERMINPLAN_MODULE_ENABLED = _TenantAwareBool('terminplan', _get(_conf, ['modules', 'terminplan', 'enabled'], DEFAULTS['modules']['terminplan']['enabled']))
|
||||||
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['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']))
|
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
||||||
|
|
||||||
def _match_inventory(path):
|
def _match_inventory(path):
|
||||||
if not path: return False
|
if not path: return False
|
||||||
if path == '/' or path.startswith('/home'): return True
|
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'))
|
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items'))
|
||||||
|
|
||||||
|
|
||||||
|
def _match_terminplan(path):
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
return path.startswith(('/terminplan', '/terminplaner'))
|
||||||
|
|
||||||
def _match_library(path):
|
def _match_library(path):
|
||||||
if not path: return False
|
if not path: return False
|
||||||
@@ -290,6 +300,7 @@ def _match_student_cards(path):
|
|||||||
|
|
||||||
# Register core modules into the pipeline
|
# Register core modules into the pipeline
|
||||||
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
|
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
|
||||||
|
MODULES.register('terminplan', TERMINPLAN_MODULE_ENABLED, _match_terminplan)
|
||||||
MODULES.register('library', LIBRARY_MODULE_ENABLED, _match_library)
|
MODULES.register('library', LIBRARY_MODULE_ENABLED, _match_library)
|
||||||
MODULES.register('student_cards', STUDENT_CARDS_MODULE_ENABLED, _match_student_cards)
|
MODULES.register('student_cards', STUDENT_CARDS_MODULE_ENABLED, _match_student_cards)
|
||||||
|
|
||||||
|
|||||||
@@ -108,3 +108,58 @@ def update(id,slots_used: list):
|
|||||||
print(f"Error updating item: {e}")
|
print(f"Error updating item: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_slot(id, date_start_time, name):
|
||||||
|
"""
|
||||||
|
Remove a booked slot from an appointment's `slots_booked`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id (str): Appointment ID
|
||||||
|
date_start_time: The start time value used when booking
|
||||||
|
name (str): Name associated with the booking
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if a slot was removed, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
items = db['appointments']
|
||||||
|
|
||||||
|
# Attempt to pull the exact element (stored as an array/tuple)
|
||||||
|
result = items.update_one(
|
||||||
|
{'_id': ObjectId(id)},
|
||||||
|
{'$pull': {'slots_booked': [date_start_time, name]}}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return result.modified_count > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing slot: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove(id):
|
||||||
|
"""
|
||||||
|
Soft-delete an appointment by setting its `Deleted` flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id (str): Appointment ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the appointment was marked deleted, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
db = client[cfg.MONGODB_DB]
|
||||||
|
items = db['appointments']
|
||||||
|
|
||||||
|
result = items.delete_one({'_id': ObjectId(id)})
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
return result.modified_count > 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing appointment: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""
|
||||||
|
Class for all funktions of the executive -> Lehrer
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import Web.modules.emailservice.email as mail_service
|
||||||
|
import Web.modules.database.termine as termin
|
||||||
|
import Web.modules.database.settings as cfg
|
||||||
|
from Web.tenant import get_tenant_context
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_time_span(time_span):
|
||||||
|
if isinstance(time_span, list):
|
||||||
|
return [str(entry).strip() for entry in time_span if str(entry).strip()]
|
||||||
|
if isinstance(time_span, tuple):
|
||||||
|
return [str(entry).strip() for entry in time_span if str(entry).strip()]
|
||||||
|
if isinstance(time_span, str):
|
||||||
|
normalized = []
|
||||||
|
for line in time_span.replace(';', '\n').replace(',', '\n').splitlines():
|
||||||
|
value = line.strip()
|
||||||
|
if value:
|
||||||
|
normalized.append(value)
|
||||||
|
return normalized
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_mail_list(mail):
|
||||||
|
if isinstance(mail, list):
|
||||||
|
return [str(entry).strip() for entry in mail if str(entry).strip()]
|
||||||
|
if isinstance(mail, tuple):
|
||||||
|
return [str(entry).strip() for entry in mail if str(entry).strip()]
|
||||||
|
if isinstance(mail, str):
|
||||||
|
return [entry.strip() for entry in mail.replace(';', ',').split(',') if entry.strip()]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="") -> str:
|
||||||
|
"""
|
||||||
|
Generates a link for the executive to send to his clients to book a time Slot
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- date_start: start of the time frae area
|
||||||
|
- date_end: end of the time frame area
|
||||||
|
- time_span: Time window for the days as a list [(first day Time Frame), (second day Time frame), (third etc.)]
|
||||||
|
- slots: amount of slots that are available
|
||||||
|
- slot_lenght: the lenght of a slot in minutes
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- link: The link for the user to send to the clients
|
||||||
|
"""
|
||||||
|
normalized_time_span = _normalize_time_span(time_span)
|
||||||
|
normalized_mail = _normalize_mail_list(mail)
|
||||||
|
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note)
|
||||||
|
id_str = str(id)
|
||||||
|
|
||||||
|
tenant_context = get_tenant_context()
|
||||||
|
subdomain = ''
|
||||||
|
if tenant_context:
|
||||||
|
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||||
|
|
||||||
|
host = f"https://{subdomain}.invario.eu" if subdomain else "invario.eu"
|
||||||
|
link = host + "/terminplaner/client" + "?" + "client_id=" + id_str
|
||||||
|
subject = f"Terminanfrage von {user}"
|
||||||
|
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
||||||
|
if normalized_mail:
|
||||||
|
mail_service.send(normalized_mail, subject, note_link)
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def book_slot(id, date_start_time, name):
|
||||||
|
"""
|
||||||
|
Updates slot for the booking per a id
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- id: the id is the id you get from the
|
||||||
|
- date_start_time: the date of the booking that was selected with date and time
|
||||||
|
- name: name that the client gave himself
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- bool: if worked or not
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Retrieve the current appointment
|
||||||
|
item = termin.get_item(id)
|
||||||
|
if not item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
slots = item.get('slots_booked', []) or []
|
||||||
|
if not isinstance(slots, list):
|
||||||
|
slots = []
|
||||||
|
|
||||||
|
capacity = int(item.get('slots', 0) or 0)
|
||||||
|
if capacity and len(slots) >= capacity:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for existing in slots:
|
||||||
|
if isinstance(existing, (list, tuple)) and len(existing) >= 2:
|
||||||
|
if existing[0] == date_start_time and existing[1] == name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Append the new booking as a tuple (start_time, name)
|
||||||
|
slots.append((date_start_time, name))
|
||||||
|
|
||||||
|
# Update the appointment in the database
|
||||||
|
success = termin.update(id, slots)
|
||||||
|
return bool(success)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error booking slot: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_slot(id, date_start_time, name):
|
||||||
|
"""
|
||||||
|
Remove a booked slot for an appointment.
|
||||||
|
|
||||||
|
Returns True if the removal succeeded, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prefer DB-level remove if available
|
||||||
|
if hasattr(termin, 'remove_slot'):
|
||||||
|
removed = termin.remove_slot(id, date_start_time, name)
|
||||||
|
if removed:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback: fetch, filter, and replace the slot list
|
||||||
|
item = termin.get_item(id)
|
||||||
|
if not item:
|
||||||
|
return False
|
||||||
|
|
||||||
|
slots = item.get('slots_booked', []) or []
|
||||||
|
new_slots = []
|
||||||
|
for s in slots:
|
||||||
|
try:
|
||||||
|
# s may be list or tuple like [start_time, name]
|
||||||
|
if isinstance(s, (list, tuple)) and len(s) >= 2 and s[0] == date_start_time and s[1] == name:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
new_slots.append(s)
|
||||||
|
|
||||||
|
success = termin.update(id, new_slots)
|
||||||
|
return bool(success)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing slot: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_appointment(id):
|
||||||
|
"""
|
||||||
|
Remove an entire appointment by id.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return bool(termin.remove(id))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing appointment: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_available(id):
|
||||||
|
"""
|
||||||
|
Gets the available time slots -> more over it returns the time frame and the allready booked slots also the lenght.
|
||||||
|
And checks if there are slots left.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- id: id of the appointment
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- dict: all the needet information -> [Start_date, End_date, (first day Time Frame,
|
||||||
|
second day Time frame, third etc.), slot lenght, (bookedslots -> list)]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
termin_range = termin.get_item(id)
|
||||||
|
if not termin_range:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
date_start = termin_range.get('date_start')
|
||||||
|
date_end = termin_range.get('date_end')
|
||||||
|
time_span = termin_range.get('time_span', [])
|
||||||
|
slot_lenght = termin_range.get('slot_lenght')
|
||||||
|
total_slots = termin_range.get('slots', 0)
|
||||||
|
booked = termin_range.get('slots_booked', []) or []
|
||||||
|
|
||||||
|
# Normalize booked entries to dicts for easier consumption
|
||||||
|
normalized = []
|
||||||
|
for s in booked:
|
||||||
|
if isinstance(s, (list, tuple)) and len(s) >= 2:
|
||||||
|
normalized.append({'start': s[0], 'name': s[1]})
|
||||||
|
elif isinstance(s, dict):
|
||||||
|
normalized.append(s)
|
||||||
|
else:
|
||||||
|
normalized.append({'value': s})
|
||||||
|
|
||||||
|
slots_used = len(normalized)
|
||||||
|
slots_left = max(0, total_slots - slots_used)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'date_start': date_start,
|
||||||
|
'date_end': date_end,
|
||||||
|
'time_span': time_span,
|
||||||
|
'slot_lenght': slot_lenght,
|
||||||
|
'slots_total': total_slots,
|
||||||
|
'slots_booked': normalized,
|
||||||
|
'slots_left': slots_left,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting available slots: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_available_user(id):
|
||||||
|
"""
|
||||||
|
Gets the available time slots -> more over it returns the time frame and the allready booked slots also the lenght.
|
||||||
|
And checks if there are slots left.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- id: id of the appointment
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- dict: all the needet information -> [Start_date, End_date, (first day Time Frame,
|
||||||
|
second day Time frame, third etc.), slot lenght, (bookedslots -> list)]
|
||||||
|
"""
|
||||||
|
return get_available(id)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from flask import Blueprint, render_template, request, session, url_for, redirect, flash
|
||||||
|
import Web.modules.terminplaner.backend_server as appointment_service
|
||||||
|
import Web.modules.database.settings as cfg
|
||||||
|
|
||||||
|
# Create a blueprint instance
|
||||||
|
appoint_bp = Blueprint('terminplaner', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_module_enabled():
|
||||||
|
if not cfg.MODULES.is_enabled('terminplan'):
|
||||||
|
flash('Der Terminplaner ist deaktiviert.', 'info')
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
return None
|
||||||
|
|
||||||
|
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
|
||||||
|
def client(appointment_id):
|
||||||
|
"""
|
||||||
|
The Route for the terminplaner to work with the client
|
||||||
|
"""
|
||||||
|
guard = _require_module_enabled()
|
||||||
|
if guard:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
available = appointment_service.get_available(appointment_id)
|
||||||
|
if not available:
|
||||||
|
flash('Der Termin wurde nicht gefunden.', 'error')
|
||||||
|
return redirect(url_for('terminplan'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
start_daytime = request.form.get('start_day_time')
|
||||||
|
username = request.form.get('client_name')
|
||||||
|
if not start_daytime or not username:
|
||||||
|
flash('Bitte Name und gewünschte Uhrzeit angeben.', 'error')
|
||||||
|
return render_template(
|
||||||
|
'termin_client.html',
|
||||||
|
appointment_id=appointment_id,
|
||||||
|
available=available,
|
||||||
|
current_user=session.get('username', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
if appointment_service.book_slot(appointment_id, start_daytime, username):
|
||||||
|
flash('Der Termin wurde gespeichert.', 'success')
|
||||||
|
return redirect(url_for('terminplaner.client', appointment_id=appointment_id))
|
||||||
|
|
||||||
|
flash('Der Termin konnte nicht gespeichert werden.', 'error')
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'termin_client.html',
|
||||||
|
appointment_id=appointment_id,
|
||||||
|
available=available,
|
||||||
|
current_user=session.get('username', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
@appoint_bp.route('/configure', methods=['GET', 'POST'])
|
||||||
|
def configure():
|
||||||
|
"""
|
||||||
|
Route for authenticated persons to configure a new appointment for them
|
||||||
|
"""
|
||||||
|
guard = _require_module_enabled()
|
||||||
|
if guard:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
if 'username' not in session:
|
||||||
|
flash('Bitte mit einem Konto anmelden.', 'error')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
start = request.form.get('start_date')
|
||||||
|
end = request.form.get('end_date')
|
||||||
|
time = request.form.get('time_frame')
|
||||||
|
slots_amount = request.form.get('slots_amounts')
|
||||||
|
slot_lenght = request.form.get('slot_lenght')
|
||||||
|
mail = request.form.get('mail', '')
|
||||||
|
note = request.form.get('note', '')
|
||||||
|
|
||||||
|
if not start or not end or not time or not slots_amount or not slot_lenght:
|
||||||
|
flash('Bitte alle Pflichtfelder ausfüllen.', 'error')
|
||||||
|
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=None)
|
||||||
|
|
||||||
|
link = appointment_service.new(start, end, time, slots_amount, slot_lenght, session["username"], mail, note)
|
||||||
|
flash('Der Terminplan wurde angelegt.', 'success')
|
||||||
|
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=link)
|
||||||
|
elif request.method == "GET":
|
||||||
|
return render_template('termin_configure.html', school_periods=cfg.SCHOOL_PERIODS, generated_link=None)
|
||||||
|
|
||||||
|
@appoint_bp.route('/')
|
||||||
|
def main():
|
||||||
|
guard = _require_module_enabled()
|
||||||
|
if guard:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'terminplaner.html',
|
||||||
|
school_periods=cfg.SCHOOL_PERIODS,
|
||||||
|
current_user=session.get('username', ''),
|
||||||
|
)
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
Class for all funktions of the executive -> Lehrer
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
import emailservice.email as mail_service
|
|
||||||
import Web.modules.database.termine as termin
|
|
||||||
import Web.modules.database.settings as cfg
|
|
||||||
from tenant import get_tenant_context
|
|
||||||
|
|
||||||
def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="") -> str:
|
|
||||||
"""
|
|
||||||
Generates a link for the executive to send to his clients to book a time Slot
|
|
||||||
|
|
||||||
Input:
|
|
||||||
- date_start: start of the time frae area
|
|
||||||
- date_end: end of the time frame area
|
|
||||||
- time_span: Time window for the days as a list [(first day Time Frame), (second day Time frame), (third etc.)]
|
|
||||||
- slots: amount of slots that are available
|
|
||||||
- slot_lenght: the lenght of a slot in minutes
|
|
||||||
|
|
||||||
Output:
|
|
||||||
- link: The link for the user to send to the clients
|
|
||||||
"""
|
|
||||||
id = termin.add(date_start, date_end, time_span, slots, slot_lenght, user, mail, note)
|
|
||||||
|
|
||||||
tenant_context = get_tenant_context()
|
|
||||||
subdomain = ''
|
|
||||||
if tenant_context:
|
|
||||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
|
||||||
|
|
||||||
host = f"https://{subdomain}.invario.eu" if subdomain else "invario.eu"
|
|
||||||
link = host + "/terminplaner/client" + "?" + "client_id=" + id
|
|
||||||
subject = f"Terminanfrage von {user}"
|
|
||||||
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
|
||||||
mail_service.send(mail, subject, note_link)
|
|
||||||
return link
|
|
||||||
|
|
||||||
|
|
||||||
def book_slot(id, date_start_time, name):
|
|
||||||
"""
|
|
||||||
Updates slot for the booking per a id
|
|
||||||
|
|
||||||
Input:
|
|
||||||
- id: the id is the id you get from the
|
|
||||||
- date_start_time: the date of the booking that was selected with date and time
|
|
||||||
- name: name that the client gave himself
|
|
||||||
|
|
||||||
Output:
|
|
||||||
- bool: if worked or not
|
|
||||||
"""
|
|
||||||
termin.update()
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
bson
|
|
||||||
werkzeug
|
werkzeug
|
||||||
gunicorn
|
gunicorn
|
||||||
pymongo
|
pymongo
|
||||||
|
|||||||
+71
-13
@@ -129,6 +129,14 @@
|
|||||||
--module-brand-icon: "📚";
|
--module-brand-icon: "📚";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-module="terminplan"] {
|
||||||
|
--module-primary-color: #0f4c5c;
|
||||||
|
--module-primary-light: #16697a;
|
||||||
|
--module-accent-color: #2ec4b6;
|
||||||
|
--module-accent-light: rgba(46, 196, 182, 0.1);
|
||||||
|
--module-brand-icon: "📅";
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme="dark"] {
|
||||||
--module-primary-color: #1a252f;
|
--module-primary-color: #1a252f;
|
||||||
--module-primary-light: #2c3e50;
|
--module-primary-light: #2c3e50;
|
||||||
@@ -145,6 +153,12 @@
|
|||||||
--module-accent-color: #8e44ad;
|
--module-accent-color: #8e44ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"][data-module="terminplan"] {
|
||||||
|
--module-primary-color: #082f35;
|
||||||
|
--module-primary-light: #0f4c5c;
|
||||||
|
--module-accent-color: #21b6a8;
|
||||||
|
}
|
||||||
|
|
||||||
/* Global styles */
|
/* Global styles */
|
||||||
body {
|
body {
|
||||||
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
|
||||||
@@ -1078,29 +1092,73 @@
|
|||||||
<span class="module-label">Module:</span>
|
<span class="module-label">Module:</span>
|
||||||
<div class="module-tabs">
|
<div class="module-tabs">
|
||||||
{% if inventory_module_enabled %}
|
{% if inventory_module_enabled %}
|
||||||
<a href="{{ url_for('home') }}"
|
<a href="{{ url_for('home') }}" class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}" id="inventoryModule" data-module="inventory">
|
||||||
class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}"
|
|
||||||
id="inventoryModule"
|
|
||||||
data-module="inventory">
|
|
||||||
📦 Inventarsystem
|
📦 Inventarsystem
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if inventory_module_enabled and library_module_enabled %}
|
{% if inventory_module_enabled and terminplan_module_enabled %}
|
||||||
|
<div class="module-separator"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if terminplan_module_enabled %}
|
||||||
|
<a href="{{ url_for('terminplaner.main') }}" class="module-tab {% if CURRENT_MODULE == 'terminplan' %}active{% endif %}" id="terminplanModule" data-module="terminplan">
|
||||||
|
📅 Terminplaner
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if (inventory_module_enabled or terminplan_module_enabled) and library_module_enabled %}
|
||||||
<div class="module-separator"></div>
|
<div class="module-separator"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if library_module_enabled %}
|
{% if library_module_enabled %}
|
||||||
<a href="{{ url_for('library_view') }}"
|
<a href="{{ url_for('library_view') }}" class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}" id="libraryModule" data-module="library">
|
||||||
class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}"
|
|
||||||
id="libraryModule"
|
|
||||||
data-module="library">
|
|
||||||
📚 Bibliothek
|
📚 Bibliothek
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if 'username' in session %}
|
{% if terminplan_module_enabled and CURRENT_MODULE == 'terminplan' %}
|
||||||
{% if CURRENT_MODULE != 'library' and inventory_module_enabled %}
|
<nav class="navbar navbar-expand-lg navbar-dark" id="terminplanNavbar">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('terminplaner.main') }}" data-icon="📅">Terminplaner</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#terminplanNavContent">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="terminplanNavContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="terminplanNavList">
|
||||||
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
|
<a class="nav-link {% if current_path == url_for('terminplaner.main') %}nav-active{% endif %}" href="{{ url_for('terminplaner.main') }}" data-tutorial-tip="Die Terminplaner-Übersicht bündelt die wichtigsten Aktionen.">Übersicht</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
|
<a class="nav-link {% if current_path == url_for('terminplan') %}nav-active{% endif %}" href="{{ url_for('terminplan') }}" data-tutorial-tip="Hier sehen Sie den Kalender mit den bestehenden Terminen.">Kalender</a>
|
||||||
|
</li>
|
||||||
|
{% if current_permissions.pages.get('terminplan', True) and current_permissions.actions.get('can_insert', True) %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link quick-link-pill {% if current_path == url_for('terminplaner.configure') %}nav-active{% endif %}" href="{{ url_for('terminplaner.configure') }}" data-tutorial-tip="Neuen Terminplan erstellen und an Ihr Team versenden.">Neue Planung</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
|
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
||||||
|
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||||
|
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown ms-lg-auto">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="termMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">Mehr Optionen</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown">
|
||||||
|
{% if current_permissions.pages.get('home', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('home') }}">Inventarsystem</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('library_view', True) and library_module_enabled %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('library_view') }}">Bibliothek</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% elif CURRENT_MODULE != 'library' and inventory_module_enabled %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
|
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
|
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
|
||||||
@@ -1220,7 +1278,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Termin buchen - Inventarsystem{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-12 col-lg-5">
|
||||||
|
<div class="card border-0 shadow-lg rounded-4 h-100">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<p class="text-uppercase text-muted fw-semibold mb-2">Terminplaner</p>
|
||||||
|
<h1 class="h3 fw-bold mb-3">Termin buchen</h1>
|
||||||
|
<p class="text-muted mb-4">Wählen Sie einen freien Zeitpunkt und tragen Sie Ihren Namen ein. Der Termin wird anschließend im Plan gespeichert.</p>
|
||||||
|
|
||||||
|
<div class="p-3 rounded-3 bg-light mb-3">
|
||||||
|
<div class="fw-semibold">Zeitraum</div>
|
||||||
|
<div>{{ available.date_start }} bis {{ available.date_end }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 rounded-3 bg-light mb-3">
|
||||||
|
<div class="fw-semibold">Verfügbare Slots</div>
|
||||||
|
<div>{{ available.slots_left }} von {{ available.slots_total }} frei</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 rounded-3 bg-light mb-3">
|
||||||
|
<div class="fw-semibold">Slot-Länge</div>
|
||||||
|
<div>{{ available.slot_lenght }} Minuten</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if available.slots_booked %}
|
||||||
|
<div class="p-3 rounded-3 bg-light">
|
||||||
|
<div class="fw-semibold mb-2">Bereits gebucht</div>
|
||||||
|
<ul class="mb-0 small">
|
||||||
|
{% for booking in available.slots_booked %}
|
||||||
|
<li>{{ booking.start }}{% if booking.name %} - {{ booking.name }}{% endif %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-7">
|
||||||
|
<div class="card border-0 shadow-lg rounded-4 h-100">
|
||||||
|
<div class="card-body p-4 p-md-5 bg-white">
|
||||||
|
<h2 class="h4 fw-bold mb-4">Buchung absenden</h2>
|
||||||
|
<form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id) }}" class="vstack gap-3">
|
||||||
|
<div>
|
||||||
|
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label>
|
||||||
|
<input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="2026-05-29 10:30" required>
|
||||||
|
<div class="form-text">Tragen Sie Datum und Uhrzeit im Format YYYY-MM-DD HH:MM ein, sofern kein Kalenderfeld genutzt wird.</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="client_name" class="form-label fw-semibold">Ihr Name</label>
|
||||||
|
<input type="text" id="client_name" name="client_name" class="form-control form-control-lg" placeholder="Vor- und Nachname" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Termin buchen</button>
|
||||||
|
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main') }}">Zur Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Terminplan konfigurieren - Inventarsystem{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-10 col-xl-8">
|
||||||
|
<div class="card border-0 shadow-lg rounded-4 overflow-hidden">
|
||||||
|
<div class="card-header text-white" style="background: linear-gradient(135deg, #0f4c5c, #16697a);">
|
||||||
|
<h1 class="h3 mb-1 fw-bold">Terminplan konfigurieren</h1>
|
||||||
|
<p class="mb-0 opacity-75">Erstellen Sie einen neuen Plan und verschicken Sie anschließend den Buchungslink.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4 p-md-5 bg-white">
|
||||||
|
<form method="post" action="{{ url_for('terminplaner.configure') }}" class="vstack gap-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="start_date" class="form-label fw-semibold">Startdatum</label>
|
||||||
|
<input type="date" id="start_date" name="start_date" class="form-control form-control-lg" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="end_date" class="form-label fw-semibold">Enddatum</label>
|
||||||
|
<input type="date" id="end_date" name="end_date" class="form-control form-control-lg" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="time_frame" class="form-label fw-semibold">Zeitfenster</label>
|
||||||
|
<textarea id="time_frame" name="time_frame" class="form-control" rows="5" placeholder="Beispiele:\nMontag 08:00-12:00\nDienstag 08:00-12:00" required></textarea>
|
||||||
|
<div class="form-text">Jede Zeile kann ein eigenes Zeitfenster enthalten.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="slots_amounts" class="form-label fw-semibold">Anzahl Slots</label>
|
||||||
|
<input type="number" id="slots_amounts" name="slots_amounts" class="form-control" min="1" value="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="slot_lenght" class="form-label fw-semibold">Slot-Länge in Minuten</label>
|
||||||
|
<input type="number" id="slot_lenght" name="slot_lenght" class="form-control" min="1" value="45" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="mail" class="form-label fw-semibold">Empfänger-Mails</label>
|
||||||
|
<input type="text" id="mail" name="mail" class="form-control" placeholder="adresse1@schule.de, adresse2@schule.de">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="note" class="form-label fw-semibold">Notiz</label>
|
||||||
|
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Terminplan erzeugen</button>
|
||||||
|
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main') }}">Zur Übersicht</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if generated_link %}
|
||||||
|
<div class="alert alert-success mt-4 shadow-sm rounded-4">
|
||||||
|
<div class="fw-bold mb-1">Buchungslink erstellt</div>
|
||||||
|
<div class="mb-2">Teilen Sie diesen Link mit den Teilnehmenden:</div>
|
||||||
|
<a href="{{ generated_link }}" class="d-inline-block text-break">{{ generated_link }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Terminplaner - Inventarsystem{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-11 col-xl-10">
|
||||||
|
<section class="p-4 p-md-5 rounded-4 shadow-lg" style="background: linear-gradient(135deg, rgba(15,76,92,0.96), rgba(22,105,122,0.92)); color: #fff;">
|
||||||
|
<div class="d-flex flex-column flex-lg-row justify-content-between gap-4 align-items-start align-items-lg-end">
|
||||||
|
<div>
|
||||||
|
<p class="text-uppercase fw-semibold mb-2" style="letter-spacing: .12em; opacity: .8;">Terminplaner</p>
|
||||||
|
<h1 class="display-6 fw-bold mb-3">Reservierungen und Termine übersichtlich verwalten</h1>
|
||||||
|
<p class="lead mb-0" style="max-width: 60ch; opacity: .95;">Erstellen Sie neue Terminreihen, teilen Sie Buchungslinks und öffnen Sie den Kalender für bestehende Reservierungen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<a class="btn btn-light btn-lg fw-semibold" href="{{ url_for('terminplaner.configure') }}">Neue Planung</a>
|
||||||
|
<a class="btn btn-outline-light btn-lg fw-semibold" href="{{ url_for('terminplan') }}">Kalender öffnen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="row g-4 mt-1">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card h-100 shadow-sm border-0 rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="display-6 mb-3">🗓️</div>
|
||||||
|
<h2 class="h4 fw-bold">Kalender</h2>
|
||||||
|
<p class="mb-4 text-muted">Sehen Sie vorhandene Termine, ihre Auslastung und die aktuellen Reservierungen im Kalender.</p>
|
||||||
|
<a class="btn btn-primary w-100" href="{{ url_for('terminplan') }}">Zum Kalender</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card h-100 shadow-sm border-0 rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="display-6 mb-3">✍️</div>
|
||||||
|
<h2 class="h4 fw-bold">Neue Planung</h2>
|
||||||
|
<p class="mb-4 text-muted">Erstellen Sie einen neuen Terminplan und verschicken Sie den Buchungslink an Ihre Zielgruppe.</p>
|
||||||
|
<a class="btn btn-outline-primary w-100" href="{{ url_for('terminplaner.configure') }}">Konfiguration öffnen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card h-100 shadow-sm border-0 rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="display-6 mb-3">🔗</div>
|
||||||
|
<h2 class="h4 fw-bold">Buchungslink</h2>
|
||||||
|
<p class="mb-4 text-muted">Die öffentliche Buchungsseite liegt unter dem jeweiligen Terminlink und kann direkt an Teilnehmende weitergegeben werden.</p>
|
||||||
|
<div class="small text-muted">Die Buchungsseite wird aus dem Terminplan automatisch erzeugt.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_user %}
|
||||||
|
<div class="mt-4 p-4 rounded-4 bg-white shadow-sm">
|
||||||
|
<h2 class="h5 fw-bold mb-2">Angemeldet als {{ current_user }}</h2>
|
||||||
|
<p class="mb-0 text-muted">Sie können Termine anlegen, den Kalender prüfen und Buchungslinks verteilen.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -212,6 +212,7 @@ def _module_name_candidates(module_name):
|
|||||||
candidates = [normalized]
|
candidates = [normalized]
|
||||||
alias_groups = {
|
alias_groups = {
|
||||||
'inventory': {'inventory', 'inventar'},
|
'inventory': {'inventory', 'inventar'},
|
||||||
|
'terminplan': {'terminplan', 'terminplaner', 'termin', 'appointments'},
|
||||||
'library': {'library', 'bib', 'bibliothek'},
|
'library': {'library', 'bib', 'bibliothek'},
|
||||||
'student_cards': {'student_cards', 'studentcards', 'schuelerausweise', 'schueler_ausweise'},
|
'student_cards': {'student_cards', 'studentcards', 'schuelerausweise', 'schueler_ausweise'},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
PYTHON_VERSION: "3.13"
|
PYTHON_VERSION: "3.13"
|
||||||
OPTIMIZATION_LEVEL: 2
|
OPTIMIZATION_LEVEL: 2
|
||||||
working_dir: /app/Web
|
working_dir: /app/Web
|
||||||
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
+154
-121
@@ -23,59 +23,70 @@ ensure_runtime_config_json() {
|
|||||||
echo "Warning: moved unexpected directory $config_path to $backup_path"
|
echo "Warning: moved unexpected directory $config_path to $backup_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$config_path" ]; then
|
FORCE_REMOVE=false
|
||||||
|
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
||||||
|
FORCE_REMOVE=true
|
||||||
|
TENANT_ID="${3:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$TENANT_ID" ]; then
|
||||||
cat > "$config_path" <<'EOF'
|
cat > "$config_path" <<'EOF'
|
||||||
{
|
{
|
||||||
"ver": "2.6.5",
|
"ver": "2.6.5",
|
||||||
"dbg": false,
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8000,
|
|
||||||
"mongodb": {
|
|
||||||
"host": "mongodb",
|
|
||||||
"port": 27017,
|
|
||||||
"db": "Inventarsystem"
|
|
||||||
},
|
|
||||||
"modules": {
|
|
||||||
"library": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"student_cards": {
|
|
||||||
"enabled": false,
|
|
||||||
"default_borrow_days": 14,
|
|
||||||
"max_borrow_days": 365
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
echo "Created default runtime config at $config_path"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
show_help() {
|
if [ "$FORCE_REMOVE" != true ]; then
|
||||||
echo "Usage: ./manage-tenant.sh [COMMAND] [OPTIONS]"
|
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||||
echo ""
|
read confirm
|
||||||
echo "Commands:"
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||||
echo " add <tenant_id> [port] Add a new tenant (initializes database)"
|
echo "Removal canceled."
|
||||||
echo " trial <tenant_id> [port] [days] Create a 7-day demo tenant that auto-deletes after expiry"
|
exit 0
|
||||||
echo " remove <tenant_id> Remove a tenant completely (deletes data!)"
|
fi
|
||||||
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
fi
|
||||||
echo " restart-all Restart all application containers (zero-downtime reload)"
|
|
||||||
echo " list List active tenants"
|
|
||||||
echo " module <tenant_id> <module>=<on|off>... Enable/disable modules for a tenant"
|
|
||||||
echo " -h, --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " ./manage-tenant.sh add school_a 10001"
|
|
||||||
echo " ./manage-tenant.sh trial school_demo 10002 7"
|
|
||||||
echo " ./manage-tenant.sh remove test_tenant"
|
|
||||||
echo " ./manage-tenant.sh module school_a inventory=off library=on"
|
|
||||||
echo " ./manage-tenant.sh restart-all"
|
|
||||||
echo " ./manage-tenant.sh -h"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
tenant_aliases() {
|
echo "Removing tenant '$TENANT_ID'..."
|
||||||
local tenant_id="$1"
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
|
port_to_remove=""
|
||||||
|
if [ -n "$APP_CONTAINER" ]; then
|
||||||
|
port_to_remove="$({ docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
sys.path.insert(0, '/app/Web')
|
||||||
|
from tenant import delete_tenant, get_tenant_config
|
||||||
|
|
||||||
|
tenant_id = sys.argv[1]
|
||||||
|
tenant_cfg = get_tenant_config(tenant_id)
|
||||||
|
port = tenant_cfg.get('port')
|
||||||
|
|
||||||
|
if not delete_tenant(tenant_id):
|
||||||
|
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if port is not None:
|
||||||
|
print(port)
|
||||||
|
PY
|
||||||
|
} 2>/dev/null)"
|
||||||
|
echo "Tenant '$TENANT_ID' database and config removed."
|
||||||
|
else
|
||||||
|
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||||
|
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$port_to_remove" ]; then
|
||||||
|
remove_runtime_port "$port_to_remove"
|
||||||
|
fi
|
||||||
|
sync_tenant_port_map
|
||||||
|
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||||
|
restart_app_container
|
||||||
|
fi
|
||||||
|
if [ -n "$port_to_remove" ]; then
|
||||||
|
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||||
|
else
|
||||||
|
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||||
|
fi
|
||||||
local normalized alias
|
local normalized alias
|
||||||
normalized="$(printf '%s' "$tenant_id" | tr '[:upper:]' '[:lower:]')"
|
normalized="$(printf '%s' "$tenant_id" | tr '[:upper:]' '[:lower:]')"
|
||||||
printf '%s\n' "$tenant_id"
|
printf '%s\n' "$tenant_id"
|
||||||
@@ -201,6 +212,7 @@ existing['modules'] = {
|
|||||||
'inventory': {'enabled': True},
|
'inventory': {'enabled': True},
|
||||||
'library': {'enabled': True},
|
'library': {'enabled': True},
|
||||||
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
||||||
|
'terminplan': {'enabled': True},
|
||||||
}
|
}
|
||||||
existing['trial'] = trial_config
|
existing['trial'] = trial_config
|
||||||
|
|
||||||
@@ -312,58 +324,60 @@ print(f"Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
update_runtime_ports() {
|
update_runtime_ports() {
|
||||||
local new_port="$1"
|
local new_port="$1"
|
||||||
local env_file="$PWD/.docker-build.env"
|
local env_file="$PWD/.docker-build.env"
|
||||||
local current_ports=""
|
local current_ports port_list unique_ports first_port ports_csv
|
||||||
local port_list=()
|
|
||||||
local unique_ports=()
|
if [ -z "$new_port" ]; then
|
||||||
local first_port=""
|
return 0
|
||||||
if [ -n "$new_port" ]; then
|
fi
|
||||||
if [ -f "$env_file" ]; then
|
|
||||||
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORTS=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
|
current_ports=""
|
||||||
if [ -z "$current_ports" ]; then
|
if [ -f "$env_file" ]; then
|
||||||
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
|
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORTS=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
|
||||||
fi
|
if [ -z "$current_ports" ]; then
|
||||||
|
current_ports="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2; exit}' "$env_file" | tr -d ' ' || true)"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$current_ports" ]; then
|
port_list=()
|
||||||
IFS=',' read -r -a port_list <<<"${current_ports// /,}"
|
unique_ports=()
|
||||||
|
if [ -n "$current_ports" ]; then
|
||||||
|
IFS=',' read -r -a port_list <<<"${current_ports// /,}"
|
||||||
|
fi
|
||||||
|
port_list+=("$new_port")
|
||||||
|
|
||||||
|
for port in "${port_list[@]}"; do
|
||||||
|
if [ -n "$port" ] && ! printf '%s\n' "${unique_ports[@]}" | grep -qx "$port"; then
|
||||||
|
unique_ports+=("$port")
|
||||||
fi
|
fi
|
||||||
port_list+=("$new_port")
|
done
|
||||||
|
|
||||||
for port in "${port_list[@]}"; do
|
if [ ${#unique_ports[@]} -eq 0 ]; then
|
||||||
if [ -n "$port" ] && ! printf '%s\n' "${unique_ports[@]}" | grep -qx "$port"; then
|
unique_ports=("$new_port")
|
||||||
unique_ports+=("$port")
|
fi
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#unique_ports[@]} -eq 0 ]; then
|
first_port="${unique_ports[0]}"
|
||||||
unique_ports=("$new_port")
|
ports_csv="$(IFS=,; echo "${unique_ports[*]}")"
|
||||||
fi
|
|
||||||
|
|
||||||
first_port="${unique_ports[0]}"
|
if [ ! -f "$env_file" ]; then
|
||||||
local ports_csv
|
cat > "$env_file" <<EOF
|
||||||
ports_csv="$(IFS=,; echo "${unique_ports[*]}")"
|
|
||||||
|
|
||||||
if [ ! -f "$env_file" ]; then
|
|
||||||
cat > "$env_file" <<EOF
|
|
||||||
NUITKA_BUILD=0
|
NUITKA_BUILD=0
|
||||||
INVENTAR_HTTP_PORT=$first_port
|
INVENTAR_HTTP_PORT=$first_port
|
||||||
INVENTAR_HTTP_PORTS=$ports_csv
|
INVENTAR_HTTP_PORTS=$ports_csv
|
||||||
EOF
|
EOF
|
||||||
|
else
|
||||||
|
if grep -q '^INVENTAR_HTTP_PORTS=' "$env_file" 2>/dev/null; then
|
||||||
|
sed -i "s|^INVENTAR_HTTP_PORTS=.*|INVENTAR_HTTP_PORTS=$ports_csv|" "$env_file"
|
||||||
else
|
else
|
||||||
if grep -q '^INVENTAR_HTTP_PORTS=' "$env_file" 2>/dev/null; then
|
printf '\nINVENTAR_HTTP_PORTS=%s\n' "$ports_csv" >> "$env_file"
|
||||||
sed -i "s|^INVENTAR_HTTP_PORTS=.*|INVENTAR_HTTP_PORTS=$ports_csv|" "$env_file"
|
fi
|
||||||
else
|
if grep -q '^INVENTAR_HTTP_PORT=' "$env_file" 2>/dev/null; then
|
||||||
printf '\nINVENTAR_HTTP_PORTS=%s\n' "$ports_csv" >> "$env_file"
|
sed -i "s|^INVENTAR_HTTP_PORT=.*|INVENTAR_HTTP_PORT=$first_port|" "$env_file"
|
||||||
fi
|
else
|
||||||
if grep -q '^INVENTAR_HTTP_PORT=' "$env_file" 2>/dev/null; then
|
printf '\nINVENTAR_HTTP_PORT=%s\n' "$first_port" >> "$env_file"
|
||||||
sed -i "s|^INVENTAR_HTTP_PORT=.*|INVENTAR_HTTP_PORT=$first_port|" "$env_file"
|
|
||||||
else
|
|
||||||
printf '\nINVENTAR_HTTP_PORT=%s\n' "$first_port" >> "$env_file"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Updated runtime env ports: $ports_csv"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Updated runtime env ports: $ports_csv"
|
||||||
}
|
}
|
||||||
|
|
||||||
sync_tenant_port_map() {
|
sync_tenant_port_map() {
|
||||||
@@ -675,50 +689,69 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
remove)
|
remove)
|
||||||
|
FORCE_REMOVE=false
|
||||||
|
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
||||||
|
FORCE_REMOVE=true
|
||||||
|
TENANT_ID="${3:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$TENANT_ID" ]; then
|
if [ -z "$TENANT_ID" ]; then
|
||||||
echo "Error: Please provide a tenant_id to remove."
|
echo "Error: Please provide a tenant_id to remove."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
|
||||||
read confirm
|
|
||||||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
|
||||||
echo "Removing tenant '$TENANT_ID'..."
|
|
||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
|
||||||
docker exec $APP_CONTAINER python3 -c "
|
|
||||||
import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from tenant import TenantContext; from Web.modules.database import settings; from pymongo import MongoClient
|
|
||||||
ctx = TenantContext()
|
|
||||||
db_name = ctx._get_db_name(sys.argv[1])
|
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
||||||
client.drop_database(db_name)
|
|
||||||
print(f'Database for tenant {sys.argv[1]} dropped.')
|
|
||||||
" "$TENANT_ID"
|
|
||||||
echo "Tenant '$TENANT_ID' database removed."
|
|
||||||
else
|
|
||||||
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
|
||||||
fi
|
|
||||||
|
|
||||||
port_to_remove=""
|
if [ "$FORCE_REMOVE" != true ]; then
|
||||||
|
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
||||||
|
read confirm
|
||||||
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||||
|
echo "Removal canceled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removing tenant '$TENANT_ID'..."
|
||||||
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
|
port_to_remove=""
|
||||||
|
if [ -n "$APP_CONTAINER" ]; then
|
||||||
|
port_to_remove="$(docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
sys.path.insert(0, '/app/Web')
|
||||||
|
from tenant import delete_tenant, get_tenant_config
|
||||||
|
|
||||||
|
tenant_id = sys.argv[1]
|
||||||
|
tenant_cfg = get_tenant_config(tenant_id)
|
||||||
|
port = tenant_cfg.get('port')
|
||||||
|
|
||||||
|
if not delete_tenant(tenant_id):
|
||||||
|
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if port is not None:
|
||||||
|
print(port)
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
echo "Tenant '$TENANT_ID' database and config removed."
|
||||||
|
else
|
||||||
|
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
||||||
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
||||||
if [ -n "$port_to_remove" ]; then
|
:
|
||||||
remove_runtime_port "$port_to_remove"
|
|
||||||
sync_tenant_port_map
|
|
||||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
|
||||||
restart_app_container
|
|
||||||
fi
|
|
||||||
echo "Removed tenant '$TENANT_ID' from config.json and cleaned runtime port $port_to_remove."
|
|
||||||
else
|
|
||||||
sync_tenant_port_map
|
|
||||||
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
|
||||||
restart_app_container
|
|
||||||
fi
|
|
||||||
echo "Removed tenant '$TENANT_ID' from config.json. No port mapping was present."
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$port_to_remove" ]; then
|
||||||
|
remove_runtime_port "$port_to_remove"
|
||||||
|
fi
|
||||||
|
sync_tenant_port_map
|
||||||
|
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||||
|
restart_app_container
|
||||||
|
fi
|
||||||
|
if [ -n "$port_to_remove" ]; then
|
||||||
|
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
||||||
else
|
else
|
||||||
echo "Removal canceled."
|
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ write_runtime_compose_override() {
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
working_dir: /app/Web
|
working_dir: /app/Web
|
||||||
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "${INVENTAR_WORKERS:-2}", "--threads", "${INVENTAR_THREADS:-2}", "--timeout", "${INVENTAR_WORKER_TIMEOUT:-30}", "--graceful-timeout", "20", "--worker-connections", "${INVENTAR_WORKER_CONNECTIONS:-100}", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "${INVENTAR_WORKERS:-2}", "--threads", "${INVENTAR_THREADS:-2}", "--timeout", "${INVENTAR_WORKER_TIMEOUT:-30}", "--graceful-timeout", "20", "--worker-connections", "${INVENTAR_WORKER_CONNECTIONS:-100}", "--max-requests", "1000", "--max-requests-jitter", "100", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
image: ${APP_IMAGE_VALUE}
|
image: ${APP_IMAGE_VALUE}
|
||||||
build: null
|
build: null
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Reference in New Issue
Block a user