Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f47e133c45 | |||
| dec3c16d7f | |||
| 897b2e43ad | |||
| 6dd44508ce | |||
| 204ded6c7c | |||
| fbd2168aff | |||
| 249a2bc2da | |||
| 7ab8f841d0 | |||
| 43c544244c | |||
| 73e4407d7b | |||
| 94fc01c08a | |||
| 788ec1db62 | |||
| 725584a104 | |||
| fbcc3b4522 |
+1
-1
@@ -1 +1 @@
|
||||
v0.8.13
|
||||
v0.8.20
|
||||
|
||||
+77
-1
@@ -84,7 +84,7 @@ from Web.modules.terminplaner.blueprint import appoint_bp as terminplaner_bp
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
import Web.modules.database.settings as cfg
|
||||
from Web.modules.database.settings import MongoClient
|
||||
from tenant import get_tenant_context, get_tenant_trial_status, purge_expired_trial_tenants
|
||||
from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants
|
||||
|
||||
|
||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||
@@ -4803,6 +4803,82 @@ def get_bookings():
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/get_user_appointments')
|
||||
def get_user_appointments():
|
||||
"""Return the current user's planned and active appointments for the calendar."""
|
||||
if 'username' not in session:
|
||||
return jsonify({'ok': False, 'error': 'unauthorized'}), 401
|
||||
|
||||
client = None
|
||||
try:
|
||||
username = session.get('username')
|
||||
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
items_col = db['items']
|
||||
|
||||
# Use the established user-specific route logic for appointments.
|
||||
bookings = au.get_ausleihung_by_user(
|
||||
username,
|
||||
status=['planned', 'active', 'completed'],
|
||||
use_client_side_verification=True,
|
||||
)
|
||||
bookings = sorted(bookings, key=lambda b: b.get('Start') or datetime.datetime.min)
|
||||
|
||||
result = []
|
||||
for booking in bookings:
|
||||
start_dt = booking.get('Start')
|
||||
if not start_dt:
|
||||
continue
|
||||
|
||||
end_dt = booking.get('End')
|
||||
if not end_dt and isinstance(start_dt, datetime.datetime):
|
||||
end_dt = start_dt + datetime.timedelta(minutes=45)
|
||||
elif not end_dt:
|
||||
end_dt = start_dt
|
||||
|
||||
item_id = str(booking.get('Item') or '')
|
||||
item_doc = None
|
||||
if item_id:
|
||||
try:
|
||||
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
||||
except Exception:
|
||||
item_doc = None
|
||||
|
||||
item_name = item_id or 'Termin'
|
||||
if item_doc:
|
||||
item_name = item_doc.get('Name') or item_doc.get('Code_4') or item_name
|
||||
|
||||
period = booking.get('Period')
|
||||
title = item_name
|
||||
if period:
|
||||
title = f"{title} - {period}. Std"
|
||||
|
||||
status = booking.get('VerifiedStatus') or booking.get('Status') or 'unknown'
|
||||
if status == 'active':
|
||||
status = 'current'
|
||||
result.append({
|
||||
'id': str(booking.get('_id')),
|
||||
'title': title,
|
||||
'start': start_dt.isoformat() if isinstance(start_dt, datetime.datetime) else str(start_dt),
|
||||
'end': end_dt.isoformat() if isinstance(end_dt, datetime.datetime) else str(end_dt),
|
||||
'status': status,
|
||||
'itemId': item_id,
|
||||
'userName': str(booking.get('User') or ''),
|
||||
'notes': str(booking.get('Notes') or ''),
|
||||
'period': period,
|
||||
'isCurrentUser': True,
|
||||
'itemBorrower': '',
|
||||
})
|
||||
|
||||
return jsonify({'ok': True, 'bookings': result})
|
||||
except Exception as e:
|
||||
return jsonify({'ok': False, 'error': str(e), 'bookings': []}), 500
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
@app.route('/api/booking_conflicts')
|
||||
def api_booking_conflicts():
|
||||
"""
|
||||
|
||||
@@ -22,6 +22,14 @@ from Web.modules.database.settings import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
import datetime
|
||||
|
||||
|
||||
def _get_tenant_db(client):
|
||||
try:
|
||||
from tenant import get_tenant_db
|
||||
return get_tenant_db(client)
|
||||
except Exception:
|
||||
return client[cfg.MONGODB_DB]
|
||||
|
||||
def _active_record_query(extra_query=None):
|
||||
"""Build a query that excludes logically deleted records."""
|
||||
base_query = {'Deleted': {'$ne': True}}
|
||||
@@ -33,7 +41,7 @@ def _active_record_query(extra_query=None):
|
||||
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False):
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
item = {
|
||||
@@ -68,7 +76,7 @@ def get_item(id):
|
||||
"""
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
|
||||
client.close()
|
||||
@@ -90,11 +98,11 @@ def update(id,slots_used: list):
|
||||
"""
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
update_data = {
|
||||
'slots_booked': [slots_used],
|
||||
'slots_booked': slots_used,
|
||||
'LastUpdated': datetime.datetime.now()
|
||||
}
|
||||
|
||||
@@ -124,7 +132,7 @@ def remove_slot(id, date_start_time, name):
|
||||
"""
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
# Attempt to pull the exact element (stored as an array/tuple)
|
||||
@@ -152,15 +160,75 @@ def remove(id):
|
||||
"""
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
result = items.delete_one({'_id': ObjectId(id)})
|
||||
|
||||
client.close()
|
||||
return result.modified_count > 0
|
||||
return result.deleted_count > 0
|
||||
except Exception as e:
|
||||
print(f"Error removing appointment: {e}")
|
||||
return False
|
||||
|
||||
def remove_done():
|
||||
"""removose already finisched appointments"""
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
removed_count = 0
|
||||
|
||||
cursor = items.find(
|
||||
_active_record_query(
|
||||
{
|
||||
'date_end': {'$lt': today},
|
||||
}
|
||||
)
|
||||
).sort('date_start', 1)
|
||||
|
||||
for item in cursor:
|
||||
item['_id'] = str(item.get('_id'))
|
||||
result = items.delete_one({'_id': ObjectId(item['_id'])})
|
||||
removed_count += result.deleted_count
|
||||
|
||||
client.close()
|
||||
return removed_count > 0
|
||||
except Exception as e:
|
||||
print(f"Error removing appointment: {e}")
|
||||
return False
|
||||
|
||||
def get_upcoming_for_user(user: str, limit: int = 25):
|
||||
"""Return upcoming appointment plans for a user ordered by start date."""
|
||||
remove_done()
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = _get_tenant_db(client)
|
||||
items = db['appointments']
|
||||
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
cursor = items.find(
|
||||
_active_record_query(
|
||||
{
|
||||
'user': str(user or '').strip(),
|
||||
'date_end': {'$gte': today},
|
||||
}
|
||||
)
|
||||
).sort('date_start', 1)
|
||||
|
||||
results = []
|
||||
for item in cursor:
|
||||
item['_id'] = str(item.get('_id'))
|
||||
results.append(item)
|
||||
if len(results) >= max(1, int(limit)):
|
||||
break
|
||||
|
||||
client.close()
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"Error retrieving upcoming appointments: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -3,13 +3,168 @@ Class for all funktions of the executive -> Lehrer
|
||||
"""
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
from flask import url_for
|
||||
from flask import url_for, has_request_context, request
|
||||
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 _parse_date_value(value):
|
||||
try:
|
||||
return datetime.datetime.strptime(str(value), '%Y-%m-%d').date()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_time_span_entry(entry):
|
||||
"""Parse entries like '2026-06-01 08:00-12:00' into a date and time window."""
|
||||
text = str(entry or '').strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
match = None
|
||||
import re
|
||||
match = re.match(r'^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})-(\d{2}:\d{2})$', text)
|
||||
if match:
|
||||
date_text, start_text, end_text = match.groups()
|
||||
return {
|
||||
'date': date_text,
|
||||
'start_time': start_text,
|
||||
'end_time': end_text,
|
||||
'raw': text,
|
||||
}
|
||||
|
||||
match = re.match(r'^(\d{2}:\d{2})-(\d{2}:\d{2})$', text)
|
||||
if match:
|
||||
start_text, end_text = match.groups()
|
||||
return {
|
||||
'date': None,
|
||||
'start_time': start_text,
|
||||
'end_time': end_text,
|
||||
'raw': text,
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _iter_date_range(start_date, end_date):
|
||||
if not start_date or not end_date:
|
||||
return []
|
||||
current = start_date
|
||||
days = []
|
||||
while current <= end_date:
|
||||
days.append(current)
|
||||
current += datetime.timedelta(days=1)
|
||||
return days
|
||||
|
||||
|
||||
def _format_display_date(date_value):
|
||||
if not date_value:
|
||||
return ''
|
||||
try:
|
||||
return date_value.strftime('%d.%m.%Y')
|
||||
except Exception:
|
||||
return str(date_value)
|
||||
|
||||
|
||||
def _weekday_name(date_value):
|
||||
names = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
|
||||
try:
|
||||
return names[date_value.weekday()]
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def _build_slot_overview(termin_range):
|
||||
start_date = _parse_date_value(termin_range.get('date_start'))
|
||||
end_date = _parse_date_value(termin_range.get('date_end'))
|
||||
slot_length = termin_range.get('slot_lenght')
|
||||
try:
|
||||
slot_length = int(slot_length or 0)
|
||||
except Exception:
|
||||
slot_length = 0
|
||||
|
||||
booked_entries = termin_range.get('slots_booked', []) or []
|
||||
booked_starts = set()
|
||||
for booked in booked_entries:
|
||||
if isinstance(booked, (list, tuple)) and len(booked) >= 1:
|
||||
booked_starts.add(str(booked[0]).strip())
|
||||
elif isinstance(booked, dict):
|
||||
booked_starts.add(str(booked.get('start') or booked.get('value') or '').strip())
|
||||
|
||||
windows_by_date = {}
|
||||
fallback_windows = []
|
||||
for entry in termin_range.get('time_span', []) or []:
|
||||
parsed = _parse_time_span_entry(entry)
|
||||
if not parsed:
|
||||
continue
|
||||
if parsed['date']:
|
||||
windows_by_date.setdefault(parsed['date'], []).append(parsed)
|
||||
else:
|
||||
fallback_windows.append(parsed)
|
||||
|
||||
day_items = []
|
||||
for day in _iter_date_range(start_date, end_date):
|
||||
day_key = day.strftime('%Y-%m-%d')
|
||||
day_windows = windows_by_date.get(day_key, []) or fallback_windows
|
||||
slots = []
|
||||
|
||||
for window in day_windows:
|
||||
try:
|
||||
window_start = datetime.datetime.strptime(f"{day_key} {window['start_time']}", '%Y-%m-%d %H:%M')
|
||||
window_end = datetime.datetime.strptime(f"{day_key} {window['end_time']}", '%Y-%m-%d %H:%M')
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
current = window_start
|
||||
while current + datetime.timedelta(minutes=max(1, slot_length)) <= window_end:
|
||||
start_label = current.strftime('%Y-%m-%d %H:%M')
|
||||
end_label = (current + datetime.timedelta(minutes=max(1, slot_length))).strftime('%H:%M')
|
||||
slots.append({
|
||||
'value': start_label,
|
||||
'label': current.strftime('%H:%M') + ' - ' + end_label,
|
||||
'occupied': start_label in booked_starts,
|
||||
})
|
||||
current += datetime.timedelta(minutes=max(1, slot_length))
|
||||
|
||||
day_items.append({
|
||||
'date': day_key,
|
||||
'display_date': _format_display_date(day),
|
||||
'weekday': _weekday_name(day),
|
||||
'slots': slots,
|
||||
'has_slots': bool(slots),
|
||||
})
|
||||
|
||||
return day_items
|
||||
|
||||
|
||||
def _resolve_public_base_url() -> str:
|
||||
if has_request_context():
|
||||
try:
|
||||
return request.url_root.rstrip('/')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tenant_context = get_tenant_context()
|
||||
subdomain = ''
|
||||
if tenant_context:
|
||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||
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):
|
||||
if isinstance(time_span, list):
|
||||
return [str(entry).strip() for entry in time_span if str(entry).strip()]
|
||||
@@ -56,15 +211,14 @@ def build_calendar_ics(appointment_id: str) -> str | None:
|
||||
time_span = item.get('time_span', []) or []
|
||||
creator = item.get('user', 'Terminplaner')
|
||||
note = item.get('note', '') or ''
|
||||
tenant_id = _current_tenant_id()
|
||||
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:
|
||||
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 "https://invario.eu"
|
||||
host = _resolve_public_base_url()
|
||||
link = host + "/terminplaner/client/" + str(appointment_id)
|
||||
if tenant_id:
|
||||
link += f"?tenant={tenant_id}"
|
||||
|
||||
try:
|
||||
start_date = datetime.datetime.strptime(str(date_start), '%Y-%m-%d').date()
|
||||
@@ -123,30 +277,26 @@ def new(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght
|
||||
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_str = str(id)
|
||||
|
||||
tenant_context = get_tenant_context()
|
||||
subdomain = ''
|
||||
if tenant_context:
|
||||
subdomain = getattr(tenant_context, 'subdomain', '') or getattr(tenant_context, 'tenant_id', '') or ''
|
||||
tenant_id = _current_tenant_id()
|
||||
|
||||
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:
|
||||
host = f"https://{subdomain}.invario.eu" if subdomain else "https://invario.eu"
|
||||
host = _resolve_public_base_url()
|
||||
link = host + "/terminplaner/client/" + id_str
|
||||
if tenant_id:
|
||||
link += f"?tenant={tenant_id}"
|
||||
subject = f"Terminanfrage von {user}"
|
||||
note_link = note + f"Bitte klicken sie auf den folgenden Link um einen Termin zu vereinbaren: {link}"
|
||||
calendar_link = None
|
||||
if calendar_enabled:
|
||||
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:
|
||||
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 "https://invario.eu"
|
||||
host = _resolve_public_base_url()
|
||||
calendar_link = host + "/terminplaner/calendar/" + id_str + ".ics"
|
||||
if tenant_id:
|
||||
calendar_link += f"?tenant={tenant_id}"
|
||||
|
||||
email_body = note_link
|
||||
if calendar_link:
|
||||
@@ -272,6 +422,17 @@ def get_available(id):
|
||||
time_span = termin_range.get('time_span', [])
|
||||
slot_lenght = termin_range.get('slot_lenght')
|
||||
total_slots = termin_range.get('slots', 0)
|
||||
# Ensure numeric fields are cast to int when stored as strings
|
||||
try:
|
||||
total_slots = int(termin_range.get('slots', 0) or 0)
|
||||
except Exception:
|
||||
total_slots = 0
|
||||
|
||||
try:
|
||||
slot_lenght = int(termin_range.get('slot_lenght') or 0)
|
||||
except Exception:
|
||||
slot_lenght = termin_range.get('slot_lenght')
|
||||
|
||||
booked = termin_range.get('slots_booked', []) or []
|
||||
|
||||
# Normalize booked entries to dicts for easier consumption
|
||||
@@ -285,7 +446,10 @@ def get_available(id):
|
||||
normalized.append({'value': s})
|
||||
|
||||
slots_used = len(normalized)
|
||||
slots_left = max(0, total_slots - slots_used)
|
||||
try:
|
||||
slots_left = max(0, int(total_slots) - slots_used)
|
||||
except Exception:
|
||||
slots_left = max(0, slots_used - slots_used)
|
||||
|
||||
return {
|
||||
'date_start': date_start,
|
||||
@@ -295,6 +459,7 @@ def get_available(id):
|
||||
'slots_total': total_slots,
|
||||
'slots_booked': normalized,
|
||||
'slots_left': slots_left,
|
||||
'slot_days': _build_slot_overview(termin_range),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting available slots: {e}")
|
||||
@@ -312,4 +477,57 @@ def get_available_user(id):
|
||||
- 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)
|
||||
return get_available(id)
|
||||
|
||||
|
||||
def get_user_upcoming_events(user: str, limit: int = 25) -> list[dict]:
|
||||
"""Return upcoming appointment plans for overview display."""
|
||||
user_name = str(user or '').strip()
|
||||
if not user_name:
|
||||
return []
|
||||
|
||||
appointments = termin.get_upcoming_for_user(user_name, limit=limit)
|
||||
host = _resolve_public_base_url()
|
||||
tenant_id = _current_tenant_id()
|
||||
|
||||
result = []
|
||||
for item in appointments:
|
||||
appointment_id = str(item.get('_id') or '')
|
||||
if not appointment_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
link = url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
|
||||
except Exception:
|
||||
link = host + '/terminplaner/client/' + appointment_id
|
||||
if tenant_id:
|
||||
link += f'?tenant={tenant_id}'
|
||||
|
||||
try:
|
||||
calendar_link = url_for('terminplaner.calendar_export', appointment_id=appointment_id, tenant=tenant_id or None, _external=True)
|
||||
except Exception:
|
||||
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_booked = item.get('slots_booked', []) or []
|
||||
if not isinstance(slots_booked, list):
|
||||
slots_booked = []
|
||||
|
||||
result.append(
|
||||
{
|
||||
'appointment_id': appointment_id,
|
||||
'date_start': str(item.get('date_start') or ''),
|
||||
'date_end': str(item.get('date_end') or ''),
|
||||
'time_span': item.get('time_span', []) or [],
|
||||
'slots_total': slots_total,
|
||||
'slots_booked': len(slots_booked),
|
||||
'slots_left': max(0, slots_total - len(slots_booked)),
|
||||
'note': str(item.get('note') or ''),
|
||||
'link': link,
|
||||
'calendar_link': calendar_link if item.get('calendar_enabled') else None,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -2,6 +2,8 @@ from flask import Blueprint, render_template, request, session, url_for, redirec
|
||||
from flask import Response
|
||||
import Web.modules.terminplaner.backend_server as appointment_service
|
||||
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
|
||||
appoint_bp = Blueprint('terminplaner', __name__)
|
||||
@@ -21,6 +23,17 @@ def _appointment_not_found_response():
|
||||
error_message='Der Termin wurde nicht gefunden.',
|
||||
), 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'])
|
||||
def client(appointment_id):
|
||||
"""
|
||||
@@ -57,8 +70,37 @@ def client(appointment_id):
|
||||
appointment_id=appointment_id,
|
||||
available=available,
|
||||
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'])
|
||||
def configure():
|
||||
"""
|
||||
@@ -132,8 +174,14 @@ def main():
|
||||
if guard:
|
||||
return guard
|
||||
|
||||
current_user = session.get('username', '')
|
||||
upcoming_events = appointment_service.get_user_upcoming_events(current_user) if current_user else []
|
||||
tenant_id = _current_tenant_id()
|
||||
|
||||
return render_template(
|
||||
'terminplaner.html',
|
||||
school_periods=cfg.SCHOOL_PERIODS,
|
||||
current_user=session.get('username', ''),
|
||||
current_user=current_user,
|
||||
upcoming_events=upcoming_events,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
@@ -2,6 +2,73 @@
|
||||
|
||||
{% block title %}Termin buchen - Inventarsystem{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.slot-day-strip {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(220px, 1fr);
|
||||
gap: 0.75rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.5rem;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.slot-day-card {
|
||||
scroll-snap-align: start;
|
||||
border: 1px solid rgba(15, 76, 92, 0.12);
|
||||
border-radius: 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.slot-day-card.is-active {
|
||||
border-color: #0f4c5c;
|
||||
box-shadow: 0 0.75rem 1.75rem rgba(15, 76, 92, 0.12);
|
||||
}
|
||||
|
||||
.slot-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.slot-pill {
|
||||
border: 1px solid rgba(15, 76, 92, 0.18);
|
||||
background: #f7fbfc;
|
||||
color: #0f4c5c;
|
||||
border-radius: 999px;
|
||||
padding: 0.65rem 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.slot-pill:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
background: #e8f4f6;
|
||||
border-color: #0f4c5c;
|
||||
}
|
||||
|
||||
.slot-pill.active {
|
||||
background: #0f4c5c;
|
||||
color: #fff;
|
||||
border-color: #0f4c5c;
|
||||
}
|
||||
|
||||
.slot-pill:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.slot-summary {
|
||||
background: linear-gradient(135deg, rgba(15,76,92,0.08), rgba(46,196,182,0.08));
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
@@ -27,6 +94,11 @@
|
||||
<div>{{ available.slot_lenght }} Minuten</div>
|
||||
</div>
|
||||
|
||||
<div class="slot-summary mb-3">
|
||||
<div class="fw-semibold mb-2">So funktioniert die Buchung</div>
|
||||
<div class="small text-muted mb-0">Wählen Sie zuerst ein Datum und dann einen freien Zeitblock. Der Startzeitpunkt wird automatisch übernommen.</div>
|
||||
</div>
|
||||
|
||||
{% if available.slots_booked %}
|
||||
<div class="p-3 rounded-3 bg-light">
|
||||
<div class="fw-semibold mb-2">Bereits gebucht</div>
|
||||
@@ -44,19 +116,70 @@
|
||||
<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">
|
||||
<form method="post" action="{{ url_for('terminplaner.client', appointment_id=appointment_id, tenant=tenant_id) }}" class="vstack gap-3">
|
||||
<input type="hidden" id="start_day_time" name="start_day_time" required>
|
||||
|
||||
<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>
|
||||
<label for="start_date_picker" class="form-label fw-semibold">Startdatum</label>
|
||||
<input type="date" id="start_date_picker" class="form-control form-control-lg" value="{{ available.date_start }}" min="{{ available.date_start }}" max="{{ available.date_end }}">
|
||||
<div class="form-text">Wählen Sie zuerst den Tag. Danach erscheint der passende Zeitbereich.</div>
|
||||
</div>
|
||||
|
||||
{% if available.slot_days %}
|
||||
<div>
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<label class="form-label fw-semibold mb-0">Freie Zeitblöcke</label>
|
||||
<div id="slot-selection-label" class="small text-muted">Bitte ein Datum und einen Zeitblock auswählen</div>
|
||||
</div>
|
||||
|
||||
<div class="slot-day-strip mb-3" id="slot-day-strip">
|
||||
{% for day in available.slot_days %}
|
||||
<button type="button" class="slot-day-card p-3 text-start {% if loop.first %}is-active{% endif %}" data-slot-day="{{ day.date }}" aria-pressed="{% if loop.first %}true{% else %}false{% endif %}">
|
||||
<div class="small text-uppercase text-muted fw-semibold">{{ day.weekday }}</div>
|
||||
<div class="h5 fw-bold mb-1">{{ day.display_date }}</div>
|
||||
<div class="small text-muted">{{ day.slots|length }} mögliche Zeitblöcke</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="slot-panels" class="vstack gap-3">
|
||||
{% for day in available.slot_days %}
|
||||
<div class="slot-panel {% if not loop.first %}d-none{% endif %}" data-slot-panel="{{ day.date }}">
|
||||
<div class="fw-semibold mb-2">{{ day.weekday }}, {{ day.display_date }}</div>
|
||||
{% if day.slots %}
|
||||
<div class="slot-pills">
|
||||
{% for slot in day.slots %}
|
||||
<button type="button"
|
||||
class="slot-pill"
|
||||
data-slot-value="{{ slot.value }}"
|
||||
data-slot-label="{{ day.display_date }} {{ slot.label }}"
|
||||
{% if slot.occupied %}disabled{% endif %}>
|
||||
{{ slot.label }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted small">Für diesen Tag gibt es keine freien Zeitblöcke.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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="p-3 rounded-3 bg-light">
|
||||
<div class="fw-semibold">Ausgewählt</div>
|
||||
<div id="selected-slot-preview" class="text-muted">Noch kein Termin ausgewählt</div>
|
||||
</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>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main', tenant=tenant_id) }}">Zur Übersicht</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -66,4 +189,97 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const dayButtons = Array.from(document.querySelectorAll('[data-slot-day]'));
|
||||
const slotPanels = Array.from(document.querySelectorAll('[data-slot-panel]'));
|
||||
const slotButtons = Array.from(document.querySelectorAll('.slot-pill[data-slot-value]'));
|
||||
const hiddenStartInput = document.getElementById('start_day_time');
|
||||
const selectedPreview = document.getElementById('selected-slot-preview');
|
||||
const dayPicker = document.getElementById('start_date_picker');
|
||||
const selectionLabel = document.getElementById('slot-selection-label');
|
||||
|
||||
function setPreview(text) {
|
||||
if (selectedPreview) {
|
||||
selectedPreview.textContent = text || 'Noch kein Termin ausgewählt';
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveDay(dayValue) {
|
||||
dayButtons.forEach((btn) => {
|
||||
const isActive = btn.dataset.slotDay === dayValue;
|
||||
btn.classList.toggle('is-active', isActive);
|
||||
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
slotPanels.forEach((panel) => {
|
||||
panel.classList.toggle('d-none', panel.dataset.slotPanel !== dayValue);
|
||||
});
|
||||
|
||||
if (dayPicker && dayValue) {
|
||||
dayPicker.value = dayValue;
|
||||
}
|
||||
|
||||
if (selectionLabel) {
|
||||
selectionLabel.textContent = dayValue ? ('Datum: ' + dayValue) : 'Bitte ein Datum und einen Zeitblock auswählen';
|
||||
}
|
||||
|
||||
const firstAvailable = slotButtons.find((button) => !button.disabled && button.closest('[data-slot-panel]')?.dataset.slotPanel === dayValue);
|
||||
if (firstAvailable) {
|
||||
firstAvailable.click();
|
||||
} else {
|
||||
hiddenStartInput.value = '';
|
||||
setPreview('Für dieses Datum kein freier Block ausgewählt');
|
||||
slotButtons.forEach((button) => button.classList.remove('active'));
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveSlot(button) {
|
||||
if (!button || button.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
slotButtons.forEach((item) => item.classList.toggle('active', item === button));
|
||||
hiddenStartInput.value = button.dataset.slotValue || '';
|
||||
setPreview(button.dataset.slotLabel || button.textContent.trim());
|
||||
|
||||
const parentPanel = button.closest('[data-slot-panel]');
|
||||
const dayValue = parentPanel ? parentPanel.dataset.slotPanel : '';
|
||||
if (dayValue && dayPicker) {
|
||||
dayPicker.value = dayValue;
|
||||
dayButtons.forEach((btn) => {
|
||||
const isActive = btn.dataset.slotDay === dayValue;
|
||||
btn.classList.toggle('is-active', isActive);
|
||||
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
});
|
||||
if (selectionLabel) {
|
||||
selectionLabel.textContent = 'Datum: ' + dayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dayButtons.forEach((button) => {
|
||||
button.addEventListener('click', function () {
|
||||
setActiveDay(button.dataset.slotDay || '');
|
||||
});
|
||||
});
|
||||
|
||||
slotButtons.forEach((button) => {
|
||||
button.addEventListener('click', function () {
|
||||
setActiveSlot(button);
|
||||
});
|
||||
});
|
||||
|
||||
if (dayPicker) {
|
||||
dayPicker.addEventListener('change', function () {
|
||||
setActiveDay(dayPicker.value || '');
|
||||
});
|
||||
}
|
||||
|
||||
const initialDay = dayPicker && dayPicker.value ? dayPicker.value : (dayButtons[0] ? dayButtons[0].dataset.slotDay : '');
|
||||
if (initialDay) {
|
||||
setActiveDay(initialDay);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+129
-790
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
</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>
|
||||
<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', tenant=tenant_id) }}">Kalender öffnen</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -27,7 +27,7 @@
|
||||
<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>
|
||||
<a class="btn btn-primary w-100" href="{{ url_for('terminplan', tenant=tenant_id) }}">Zum Kalender</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
<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>
|
||||
<a class="btn btn-outline-primary w-100" href="{{ url_for('terminplaner.configure', tenant=tenant_id) }}">Konfiguration öffnen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,6 +58,48 @@
|
||||
<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>
|
||||
|
||||
<div class="mt-4 p-4 rounded-4 bg-white shadow-sm">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
|
||||
<h2 class="h5 fw-bold mb-0">Kommende Termine</h2>
|
||||
<span class="text-muted small">Alle offenen Terminpläne dieses Nutzers</span>
|
||||
</div>
|
||||
|
||||
{% if upcoming_events %}
|
||||
<div class="vstack gap-3">
|
||||
{% for event in upcoming_events %}
|
||||
<div class="border rounded-3 p-3">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ event.date_start }} bis {{ event.date_end }}</div>
|
||||
<div class="text-muted small">ID: {{ event.appointment_id }}</div>
|
||||
{% if event.time_span %}
|
||||
<div class="small mt-1">Zeitfenster: {{ event.time_span|join(' | ') }}</div>
|
||||
{% endif %}
|
||||
{% if event.note %}
|
||||
<div class="small mt-1 text-muted">Notiz: {{ event.note }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-lg-end">
|
||||
<div class="small mb-2">Gebucht: <strong>{{ event.slots_booked }}</strong> / {{ event.slots_total }} | Frei: <strong>{{ event.slots_left }}</strong></div>
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-lg-end">
|
||||
<a class="btn btn-sm btn-primary" href="{{ event.link }}" target="_blank" rel="noopener">Client-Link öffnen</a>
|
||||
{% if event.calendar_link %}
|
||||
<a class="btn btn-sm btn-outline-primary" href="{{ event.calendar_link }}">.ics</a>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted">Keine kommenden Termine gefunden.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -473,6 +473,20 @@ class TenantContext:
|
||||
if not has_request_context():
|
||||
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)
|
||||
tenant_from_header = request.headers.get('X-Tenant-ID', '').strip()
|
||||
if tenant_from_header:
|
||||
@@ -531,6 +545,10 @@ class TenantContext:
|
||||
potential_subdomain = parts[0]
|
||||
if potential_subdomain not in ('www', 'api', 'admin', 'app', 'mail'):
|
||||
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:
|
||||
self.subdomain = potential_subdomain
|
||||
self.tenant_id = matched_tenant
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
"inventory": {
|
||||
"enabled": true
|
||||
},
|
||||
"terminplan": {
|
||||
"enabled": true
|
||||
},
|
||||
"library": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Small debug script to check tenant-aware lookup for an appointment id.
|
||||
Usage: ./tools/debug_get_appointment.py <appointment_id> [tenant]
|
||||
"""
|
||||
import sys
|
||||
from bson.objectid import ObjectId
|
||||
from Web.modules.database.settings import MongoClient, MONGODB_HOST, MONGODB_PORT, MONGODB_DB
|
||||
import Web.modules.database.termine as termine
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: debug_get_appointment.py <appointment_id> [tenant]")
|
||||
sys.exit(2)
|
||||
aid = sys.argv[1]
|
||||
tenant = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
if tenant:
|
||||
print(f"Looking up appointment {aid} for tenant {tenant}")
|
||||
else:
|
||||
print(f"Looking up appointment {aid} for default tenant")
|
||||
|
||||
try:
|
||||
# Use the module helper directly
|
||||
item = termine.get_item(aid)
|
||||
if item:
|
||||
print('Found with termini.get_item:')
|
||||
print(item)
|
||||
else:
|
||||
print('termin.get_item returned None')
|
||||
|
||||
# Try explicit MongoClient + tenant DB resolution
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
try:
|
||||
from Web.tenant import TenantContext, get_tenant_db
|
||||
if tenant:
|
||||
ctx = TenantContext()
|
||||
ctx.tenant_id = tenant
|
||||
db = ctx.get_database(client)
|
||||
else:
|
||||
db = get_tenant_db(client) if 'get_tenant_db' in dir() else client[MONGODB_DB]
|
||||
|
||||
doc = db['appointments'].find_one({'_id': ObjectId(aid)})
|
||||
print('Direct DB find_one returned:')
|
||||
print(doc)
|
||||
finally:
|
||||
client.close()
|
||||
except Exception as e:
|
||||
print('Error during debug lookup:', e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user