feat: Enhance tenant management and appointment deletion functionality

This commit is contained in:
2026-05-31 16:13:11 +02:00
parent 43c544244c
commit 7ab8f841d0
6 changed files with 105 additions and 13 deletions
+4 -2
View File
@@ -166,7 +166,7 @@ def remove(id):
result = items.delete_one({'_id': ObjectId(id)}) result = items.delete_one({'_id': ObjectId(id)})
client.close() client.close()
return result.modified_count > 0 return result.deleted_count > 0
except Exception as e: except Exception as e:
print(f"Error removing appointment: {e}") print(f"Error removing appointment: {e}")
return False return False
@@ -179,6 +179,7 @@ def remove_done():
items = db['appointments'] items = db['appointments']
today = datetime.date.today().strftime('%Y-%m-%d') today = datetime.date.today().strftime('%Y-%m-%d')
removed_count = 0
cursor = items.find( cursor = items.find(
_active_record_query( _active_record_query(
@@ -191,9 +192,10 @@ def remove_done():
for item in cursor: for item in cursor:
item['_id'] = str(item.get('_id')) item['_id'] = str(item.get('_id'))
result = items.delete_one({'_id': ObjectId(item['_id'])}) result = items.delete_one({'_id': ObjectId(item['_id'])})
removed_count += result.deleted_count
client.close() client.close()
return result.modified_count > 0 return removed_count > 0
except Exception as e: except Exception as e:
print(f"Error removing appointment: {e}") print(f"Error removing appointment: {e}")
return False return False
+30 -5
View File
@@ -24,6 +24,18 @@ def _resolve_public_base_url() -> str:
return f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu" return f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
def _current_tenant_id() -> str:
tenant_context = get_tenant_context()
if tenant_context and getattr(tenant_context, 'tenant_id', None):
return str(tenant_context.tenant_id)
if has_request_context():
try:
return str(request.args.get('tenant', '') or request.args.get('tenant_id', '') or '').strip()
except Exception:
return ''
return ''
def _normalize_time_span(time_span): def _normalize_time_span(time_span):
if isinstance(time_span, list): if isinstance(time_span, list):
return [str(entry).strip() for entry in time_span if str(entry).strip()] return [str(entry).strip() for entry in time_span if str(entry).strip()]
@@ -70,11 +82,14 @@ def build_calendar_ics(appointment_id: str) -> str | None:
time_span = item.get('time_span', []) or [] time_span = item.get('time_span', []) or []
creator = item.get('user', 'Terminplaner') creator = item.get('user', 'Terminplaner')
note = item.get('note', '') or '' note = item.get('note', '') or ''
tenant_id = _current_tenant_id()
try: try:
link = url_for('terminplaner.client', appointment_id=str(appointment_id), _external=True) link = url_for('terminplaner.client', appointment_id=str(appointment_id), tenant=tenant_id or None, _external=True)
except Exception: except Exception:
host = _resolve_public_base_url() host = _resolve_public_base_url()
link = host + "/terminplaner/client/" + str(appointment_id) link = host + "/terminplaner/client/" + str(appointment_id)
if tenant_id:
link += f"?tenant={tenant_id}"
try: try:
start_date = datetime.datetime.strptime(str(date_start), '%Y-%m-%d').date() start_date = datetime.datetime.strptime(str(date_start), '%Y-%m-%d').date()
@@ -133,21 +148,26 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
normalized_mail = _normalize_mail_list(mail) normalized_mail = _normalize_mail_list(mail)
id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled) id = termin.add(date_start, date_end, normalized_time_span, slots, slot_lenght, user, normalized_mail, note, calendar_enabled=calendar_enabled)
id_str = str(id) id_str = str(id)
tenant_id = _current_tenant_id()
try: try:
link = url_for('terminplaner.client', appointment_id=id_str, _external=True) link = url_for('terminplaner.client', appointment_id=id_str, tenant=tenant_id or None, _external=True)
except Exception: except Exception:
host = _resolve_public_base_url() host = _resolve_public_base_url()
link = host + "/terminplaner/client/" + id_str link = host + "/terminplaner/client/" + id_str
if tenant_id:
link += f"?tenant={tenant_id}"
subject = f"Terminanfrage von {user}" subject = f"Terminanfrage von {user}"
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}" note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
calendar_link = None calendar_link = None
if calendar_enabled: if calendar_enabled:
try: try:
calendar_link = url_for('terminplaner.calendar_export', appointment_id=id_str, _external=True) calendar_link = url_for('terminplaner.calendar_export', appointment_id=id_str, tenant=tenant_id or None, _external=True)
except Exception: except Exception:
host = _resolve_public_base_url() host = _resolve_public_base_url()
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics" calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
if tenant_id:
calendar_link += f"?tenant={tenant_id}"
email_body = note_link email_body = note_link
if calendar_link: if calendar_link:
@@ -324,6 +344,7 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
appointments = termin.get_upcoming_for_user(user_name, limit=limit) appointments = termin.get_upcoming_for_user(user_name, limit=limit)
host = _resolve_public_base_url() host = _resolve_public_base_url()
tenant_id = _current_tenant_id()
result = [] result = []
for item in appointments: for item in appointments:
@@ -332,14 +353,18 @@ def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
continue continue
try: try:
link = url_for('terminplaner.client', appointment_id=appointment_id, _external=True) link = url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
except Exception: except Exception:
link = host + '/terminplaner/client/' + appointment_id link = host + '/terminplaner/client/' + appointment_id
if tenant_id:
link += f'?tenant={tenant_id}'
try: try:
calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, _external=True) calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
except Exception: except Exception:
calendar_link = host + '/terminplaner/calendar/' + appointment_id + '.ics' calendar_link = host + '/terminplaner/calendar/' + appointment_id + '.ics'
if tenant_id:
calendar_link += f'?tenant={tenant_id}'
slots_total = int(item.get('slots', 0) or 0) slots_total = int(item.get('slots', 0) or 0)
slots_booked = item.get('slots_booked', []) or [] slots_booked = item.get('slots_booked', []) or []
+44
View File
@@ -2,6 +2,8 @@ from flask import Blueprint, render_template, request, session, url_for, redirec
from flask import Response from flask import Response
import Web.modules.terminplaner.backend_server as appointment_service import Web.modules.terminplaner.backend_server as appointment_service
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
import Web.modules.database.termine as termin
import Web.modules.database.user as us
# Create a blueprint instance # Create a blueprint instance
appoint_bp = Blueprint('terminplaner', __name__) appoint_bp = Blueprint('terminplaner', __name__)
@@ -21,6 +23,17 @@ def _appointment_not_found_response():
error_message='Der Termin wurde nicht gefunden.', error_message='Der Termin wurde nicht gefunden.',
), 404 ), 404
def _current_tenant_id():
try:
from Web.tenant import get_tenant_context
ctx = get_tenant_context()
if ctx and getattr(ctx, 'tenant_id', None):
return str(ctx.tenant_id)
except Exception:
pass
return str(session.get('tenant_id', '') or '').strip()
@appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET']) @appoint_bp.route('/client/<appointment_id>', methods=['POST', 'GET'])
def client(appointment_id): def client(appointment_id):
""" """
@@ -57,8 +70,37 @@ def client(appointment_id):
appointment_id=appointment_id, appointment_id=appointment_id,
available=available, available=available,
current_user=session.get('username', ''), current_user=session.get('username', ''),
tenant_id=_current_tenant_id(),
) )
@appoint_bp.route('/delete/<appointment_id>', methods=['POST'])
def delete_appointment(appointment_id):
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'))
appointment = termin.get_item(appointment_id)
if not appointment:
return _appointment_not_found_response()
current_user = str(session.get('username', '')).strip()
appointment_user = str(appointment.get('user', '')).strip()
if not us.check_admin(current_user) and appointment_user != current_user:
flash('Sie dürfen diesen Termin nicht löschen.', 'error')
return redirect(url_for('terminplaner.main', tenant=_current_tenant_id() or None))
if termin.remove(appointment_id):
flash('Der Terminplan wurde gelöscht.', 'success')
else:
flash('Der Terminplan konnte nicht gelöscht werden.', 'error')
return redirect(url_for('terminplaner.main', tenant=_current_tenant_id() or None))
@appoint_bp.route('/configure', methods=['GET', 'POST']) @appoint_bp.route('/configure', methods=['GET', 'POST'])
def configure(): def configure():
""" """
@@ -134,10 +176,12 @@ def main():
current_user = session.get('username', '') current_user = session.get('username', '')
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else [] upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
tenant_id = _current_tenant_id()
return render_template( return render_template(
'terminplaner.html', 'terminplaner.html',
school_periods=cfg.SCHOOL_PERIODS, school_periods=cfg.SCHOOL_PERIODS,
current_user=current_user, current_user=current_user,
upcoming_events=upcoming_events, upcoming_events=upcoming_events,
tenant_id=tenant_id,
) )
+2 -2
View File
@@ -44,7 +44,7 @@
<div class="card border-0 shadow-lg rounded-4 h-100"> <div class="card border-0 shadow-lg rounded-4 h-100">
<div class="card-body p-4 p-md-5 bg-white"> <div class="card-body p-4 p-md-5 bg-white">
<h2 class="h4 fw-bold mb-4">Buchung absenden</h2> <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"> <form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" class="vstack gap-3">
<div> <div>
<label for="start_day_time" class="form-label fw-semibold">Gewünschter Zeitpunkt</label> <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> <input type="text" id="start_day_time" name="start_day_time" class="form-control form-control-lg" placeholder="2026-05-29 10:30" required>
@@ -56,7 +56,7 @@
</div> </div>
<div class="d-flex flex-column flex-sm-row gap-2 pt-2"> <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> <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> <a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
</div> </div>
</form> </form>
</div> </div>
+7 -4
View File
@@ -14,8 +14,8 @@
<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> <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>
<div class="d-flex flex-wrap gap-2"> <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-light btn-lg fw-semibold" href="{{ url_for('terminplaner.configure', tenant=tenant_id) }}">Neue Planung</a>
<a class="btn btn-outline-light btn-lg fw-semibold" href="{{ url_for('terminplan') }}">Kalender öffnen</a> <a class="btn btn-outline-light btn-lg fw-semibold" href="{{ url_for('terminplan', tenant=tenant_id) }}">Kalender öffnen</a>
</div> </div>
</div> </div>
</section> </section>
@@ -27,7 +27,7 @@
<div class="display-6 mb-3">🗓️</div> <div class="display-6 mb-3">🗓️</div>
<h2 class="h4 fw-bold">Kalender</h2> <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> <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> <a class="btn btn-primary w-100" href="{{ url_for('terminplan', tenant=tenant_id) }}">Zum Kalender</a>
</div> </div>
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@
<div class="display-6 mb-3">✍️</div> <div class="display-6 mb-3">✍️</div>
<h2 class="h4 fw-bold">Neue Planung</h2> <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> <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> <a class="btn btn-outline-primary w-100" href="{{ url_for('terminplaner.configure', tenant=tenant_id) }}">Konfiguration öffnen</a>
</div> </div>
</div> </div>
</div> </div>
@@ -87,6 +87,9 @@
{% if event.calendar_link %} {% if event.calendar_link %}
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a> <a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
{% endif %} {% endif %}
<form method="post" action="{{ url_for('terminplaner.delete_appointment', appointment_id=event.appointment_id, tenant=tenant_id) }}" class="d-inline" onsubmit="return confirm('Diesen Terminplan wirklich löschen?');">
<button type="submit" class="btn btn-sm btn-outline-danger">Entfernen</button>
</form>
</div> </div>
</div> </div>
</div> </div>
+18
View File
@@ -473,6 +473,20 @@ class TenantContext:
if not has_request_context(): if not has_request_context():
return None return None
# Query parameters are useful for public links that must open a specific tenant
# even when the host/subdomain cannot be mapped reliably.
tenant_from_query = (
request.args.get('tenant', '').strip()
or request.args.get('tenant_id', '').strip()
or request.args.get('tenantId', '').strip()
)
if tenant_from_query:
matched_tenant = _find_registered_tenant_id(tenant_from_query) or tenant_from_query
self.tenant_id = matched_tenant
self.config = get_tenant_config(matched_tenant)
session['tenant_id'] = matched_tenant
return self._get_db_name(matched_tenant)
# Priority 1: X-Tenant-ID header (for testing/internal APIs) # Priority 1: X-Tenant-ID header (for testing/internal APIs)
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip() tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
if tenant_from_header: if tenant_from_header:
@@ -531,6 +545,10 @@ class TenantContext:
potential_subdomain = parts[0] potential_subdomain = parts[0]
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'): if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
matched_tenant = _find_registered_tenant_id(potential_subdomain) matched_tenant = _find_registered_tenant_id(potential_subdomain)
if not matched_tenant and potential_subdomain.startswith('school'):
matched_tenant = _find_registered_tenant_id('schule' + potential_subdomain[len('school'):])
elif not matched_tenant and potential_subdomain.startswith('schule'):
matched_tenant = _find_registered_tenant_id('school' + potential_subdomain[len('schule'):])
if matched_tenant: if matched_tenant:
self.subdomain = potential_subdomain self.subdomain = potential_subdomain
self.tenant_id = matched_tenant self.tenant_id = matched_tenant