Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ad81d9b6d | |||
| a8f3907f34 |
+103
-65
@@ -7990,8 +7990,12 @@ def check_availability():
|
||||
items_col = db['items']
|
||||
|
||||
# Collect potential conflicts (planned and active) for this day
|
||||
same_day_start = datetime.datetime.combine(booking_date.date(), datetime.time.min)
|
||||
same_day_end = datetime.datetime.combine(booking_date.date(), datetime.time.max)
|
||||
same_day_start = datetime.datetime.combine(
|
||||
booking_date.date(), datetime.time.min, tzinfo=ZoneInfo("Europe/Berlin")
|
||||
)
|
||||
same_day_end = datetime.datetime.combine(
|
||||
booking_date.date(), datetime.time.max, tzinfo=ZoneInfo("Europe/Berlin")
|
||||
)
|
||||
candidates = list(ausleihungen.find({
|
||||
'Item': item_id,
|
||||
'Status': {'$in': ['planned', 'active']},
|
||||
@@ -8009,6 +8013,8 @@ def check_availability():
|
||||
if r_start is None:
|
||||
r_start = same_day_start
|
||||
# Overlap check: req_start < r_end and req_end > r_start
|
||||
r_start = au.ensure_timezone_aware(r_start)
|
||||
r_end = au.ensure_timezone_aware(r_end)
|
||||
if req_start < r_end and req_end > r_start:
|
||||
conflicts.append({
|
||||
'id': str(r.get('_id')),
|
||||
@@ -8052,18 +8058,20 @@ def plan_booking():
|
||||
# Validate inputs
|
||||
if not all([item_id, start_date_str, period_start]):
|
||||
return {"success": False, "error": "Missing required fields"}, 400
|
||||
if booking_type not in {'single', 'range'}:
|
||||
return {"success": False, "error": "Invalid booking type"}, 400
|
||||
if not end_date_str:
|
||||
end_date_str = start_date_str
|
||||
|
||||
# Parse dates
|
||||
try:
|
||||
if start_date_str:
|
||||
start_date = datetime.datetime.fromisoformat(start_date_str)
|
||||
start_date = datetime.datetime.fromisoformat(start_date_str).replace(tzinfo=None)
|
||||
else:
|
||||
return {"success": False, "error": "Missing start date"}, 400
|
||||
|
||||
if end_date_str:
|
||||
end_date = datetime.datetime.fromisoformat(end_date_str)
|
||||
end_date = datetime.datetime.fromisoformat(end_date_str).replace(tzinfo=None)
|
||||
else:
|
||||
return {"success": False, "error": "Missing end date"}, 400
|
||||
|
||||
@@ -8085,16 +8093,23 @@ def plan_booking():
|
||||
|
||||
# Handle period range
|
||||
periods = []
|
||||
if period_start:
|
||||
try:
|
||||
period_start_num = int(period_start)
|
||||
else:
|
||||
period_start_num = 1 # Default if None
|
||||
except (TypeError, ValueError):
|
||||
return {"success": False, "error": "Invalid start period"}, 400
|
||||
if not 1 <= period_start_num <= 10:
|
||||
return {"success": False, "error": "Invalid start period"}, 400
|
||||
|
||||
# If period_end is provided, it's a range of periods
|
||||
if period_end:
|
||||
period_end_num = int(period_end)
|
||||
try:
|
||||
period_end_num = int(period_end)
|
||||
except (TypeError, ValueError):
|
||||
return {"success": False, "error": "Invalid end period"}, 400
|
||||
|
||||
# Validate period range
|
||||
if not 1 <= period_end_num <= 10:
|
||||
return {"success": False, "error": "Invalid end period"}, 400
|
||||
if period_end_num < period_start_num:
|
||||
return {"success": False, "error": "End period cannot be before start period"}, 400
|
||||
|
||||
@@ -8104,51 +8119,50 @@ def plan_booking():
|
||||
# Single period booking
|
||||
periods = [period_start_num]
|
||||
|
||||
# For date range bookings, we'll process each date separately
|
||||
booking_ids = []
|
||||
errors = []
|
||||
|
||||
# If it's a range of days
|
||||
if booking_type == 'range' and start_date != end_date:
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
# For each day in the range
|
||||
day_booking_ids, day_errors = process_day_bookings(
|
||||
item_id,
|
||||
current_date,
|
||||
periods,
|
||||
notes
|
||||
)
|
||||
booking_ids.extend(day_booking_ids)
|
||||
errors.extend(day_errors)
|
||||
|
||||
# Move to next day
|
||||
current_date += datetime.timedelta(days=1)
|
||||
else:
|
||||
# Single day with multiple periods
|
||||
booking_ids, errors = process_day_bookings(
|
||||
item_id,
|
||||
start_date,
|
||||
periods,
|
||||
notes
|
||||
)
|
||||
|
||||
# Return results
|
||||
if errors:
|
||||
if booking_ids:
|
||||
# Some succeeded, some failed
|
||||
if end_date < start_date:
|
||||
return {"success": False, "error": "End date cannot be before start date"}, 400
|
||||
if booking_type == 'single' and start_date.date() != end_date.date():
|
||||
return {"success": False, "error": "Single bookings must use one date"}, 400
|
||||
|
||||
requested_slots = []
|
||||
current_date = start_date
|
||||
last_date = end_date if booking_type == 'range' else start_date
|
||||
while current_date.date() <= last_date.date():
|
||||
for period in periods:
|
||||
period_times = get_period_times(current_date, period)
|
||||
if not period_times:
|
||||
return {"success": False, "error": f"Invalid period {period}"}, 400
|
||||
requested_slots.append((current_date.date(), period, period_times))
|
||||
current_date += datetime.timedelta(days=1)
|
||||
|
||||
# Preflight the complete request so conflicts never create partial ranges.
|
||||
for index, (booking_date, period, period_times) in enumerate(requested_slots):
|
||||
if au.check_booking_conflict(item_id, period_times['start'], period_times['end'], period):
|
||||
return {
|
||||
"success": True,
|
||||
"partial": True,
|
||||
"booking_ids": booking_ids,
|
||||
"errors": errors
|
||||
}
|
||||
else:
|
||||
# All failed
|
||||
return {"success": False}, 500
|
||||
else:
|
||||
# All succeeded
|
||||
return {"success": True, "booking_ids": booking_ids}
|
||||
"success": False,
|
||||
"error": "Booking conflict",
|
||||
"conflicts": [{"date": booking_date.isoformat(), "period": period}],
|
||||
}, 409
|
||||
for previous_date, previous_period, _ in requested_slots[:index]:
|
||||
if previous_date == booking_date and previous_period == period:
|
||||
return {"success": False, "error": "Duplicate booking period"}, 400
|
||||
|
||||
booking_ids = []
|
||||
try:
|
||||
for _, period, period_times in requested_slots:
|
||||
booking_id = au.add_planned_booking(
|
||||
item_id, session['username'], period_times['start'],
|
||||
period_times['end'], notes, period=period
|
||||
)
|
||||
if not booking_id:
|
||||
raise RuntimeError(f"Failed to create booking for period {period}")
|
||||
booking_ids.append(str(booking_id))
|
||||
except Exception:
|
||||
for booking_id in booking_ids:
|
||||
au.cancel_ausleihung(booking_id)
|
||||
raise
|
||||
|
||||
return {"success": True, "booking_ids": booking_ids}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
@@ -8208,7 +8222,7 @@ def add_booking():
|
||||
if 'username' not in session:
|
||||
return jsonify({'success': False, 'error': 'Not logged in'})
|
||||
|
||||
item_id = html.escape(request.form.get('item_id'))
|
||||
item_id = html.escape((request.form.get('item_id') or '').strip())
|
||||
|
||||
# Check if item exists and is reservable
|
||||
item = it.get_item(item_id)
|
||||
@@ -8223,18 +8237,36 @@ def add_booking():
|
||||
period = request.form.get('period')
|
||||
notes = request.form.get('notes', '')
|
||||
|
||||
# Parse dates as naive datetime objects
|
||||
# Form timestamps represent local school time.
|
||||
try:
|
||||
# Simple datetime parsing without timezone
|
||||
if start_date_str:
|
||||
start_date = datetime.datetime.strptime(start_date_str, '%Y-%m-%d %H:%M:%S')
|
||||
start_date = datetime.datetime.strptime(
|
||||
start_date_str, '%Y-%m-%d %H:%M:%S'
|
||||
).replace(tzinfo=ZoneInfo("Europe/Berlin"))
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Missing start date'})
|
||||
|
||||
if end_date_str:
|
||||
end_date = datetime.datetime.strptime(end_date_str, '%Y-%m-%d %H:%M:%S')
|
||||
end_date = datetime.datetime.strptime(
|
||||
end_date_str, '%Y-%m-%d %H:%M:%S'
|
||||
).replace(tzinfo=ZoneInfo("Europe/Berlin"))
|
||||
else:
|
||||
end_date = None
|
||||
return jsonify({'success': False, 'error': 'Missing end date'}), 400
|
||||
|
||||
if end_date <= start_date:
|
||||
return jsonify({'success': False, 'error': 'End date must be after start date'}), 400
|
||||
|
||||
period_value = None
|
||||
if period not in (None, ''):
|
||||
try:
|
||||
period_value = int(period)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'success': False, 'error': 'Invalid period'}), 400
|
||||
if not 1 <= period_value <= 10:
|
||||
return jsonify({'success': False, 'error': 'Invalid period'}), 400
|
||||
|
||||
if au.check_booking_conflict(item_id, start_date, end_date, period_value):
|
||||
return jsonify({'success': False, 'error': 'Booking conflict'}), 409
|
||||
|
||||
# Continue with adding the booking
|
||||
booking_id = au.add_planned_booking(
|
||||
@@ -8243,12 +8275,15 @@ def add_booking():
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
notes=notes,
|
||||
period=period
|
||||
period=period_value
|
||||
)
|
||||
|
||||
|
||||
if not booking_id:
|
||||
return jsonify({'success': False, 'error': 'Failed to create booking'}), 500
|
||||
return jsonify({'success': True, 'booking_id': str(booking_id)})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False})
|
||||
app.logger.error(f"Error creating booking: {e}")
|
||||
return jsonify({'success': False, 'error': 'Invalid booking data'}), 400
|
||||
|
||||
@app.route('/cancel_booking/<id>', methods=['POST'])
|
||||
def cancel_booking(id):
|
||||
@@ -8267,11 +8302,12 @@ def cancel_booking(id):
|
||||
current_permissions = us.get_effective_permissions(session['username'])
|
||||
|
||||
# Check if user owns this booking
|
||||
if booking.get('User') != session['username'] and not current_permissions['actions'].get('can_manage_users', False):
|
||||
booking_user = au.dp.decrypt_text(booking.get('User')) if booking.get('User') else ''
|
||||
if booking_user != session['username'] and not current_permissions['actions'].get('can_manage_users', False):
|
||||
return {"success": False, "error": "Not authorized to cancel this booking"}, 403
|
||||
|
||||
# Cancel the booking
|
||||
result = au.cancel_booking(id)
|
||||
result = au.cancel_ausleihung(id)
|
||||
|
||||
if result:
|
||||
return {"success": True}
|
||||
@@ -11274,12 +11310,14 @@ def get_period_times(booking_date, period_num):
|
||||
# Create datetime objects for start and end times
|
||||
start_datetime = datetime.datetime.combine(
|
||||
booking_date.date(),
|
||||
datetime.time(start_hour, start_min)
|
||||
datetime.time(start_hour, start_min),
|
||||
tzinfo=ZoneInfo("Europe/Berlin")
|
||||
)
|
||||
|
||||
end_datetime = datetime.datetime.combine(
|
||||
booking_date.date(),
|
||||
datetime.time(end_hour, end_min)
|
||||
datetime.time(end_hour, end_min),
|
||||
tzinfo=ZoneInfo("Europe/Berlin")
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -42,12 +42,12 @@ def _get_client():
|
||||
return MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
# Add this helper function after imports
|
||||
def ensure_timezone_aware(dt):
|
||||
"""Ensures a datetime is timezone-aware, using UTC if naive"""
|
||||
"""Return a timezone-aware datetime, treating naive DB values as UTC."""
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
# Treat naive datetimes as UTC
|
||||
return dt.replace(tzinfo=None)
|
||||
# PyMongo returns BSON datetimes as naive UTC unless tz_aware is enabled.
|
||||
return dt.replace(tzinfo=datetime.timezone.utc)
|
||||
return dt
|
||||
|
||||
def get_current_status(ausleihung, log_changes=False, user=None):
|
||||
@@ -82,8 +82,8 @@ def get_current_status(ausleihung, log_changes=False, user=None):
|
||||
return 'completed'
|
||||
|
||||
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
start_time = ausleihung.get('Start')
|
||||
end_time = ausleihung.get('End')
|
||||
start_time = ensure_timezone_aware(ausleihung.get('Start'))
|
||||
end_time = ensure_timezone_aware(ausleihung.get('End'))
|
||||
|
||||
# Wenn kein Startdatum vorhanden ist, Status auf 'planned' setzen
|
||||
if not start_time:
|
||||
@@ -353,7 +353,7 @@ def cancel_ausleihung(id):
|
||||
|
||||
# Mark the booking as cancelled
|
||||
result = ausleihungen.update_one(
|
||||
{'_id': ObjectId(id)},
|
||||
{'_id': ObjectId(id), 'Status': {'$in': ['planned', 'active']}},
|
||||
{'$set': {
|
||||
'Status': 'cancelled',
|
||||
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||
@@ -367,6 +367,16 @@ def cancel_ausleihung(id):
|
||||
return False
|
||||
|
||||
|
||||
def get_booking(id):
|
||||
"""Compatibility wrapper for the booking route."""
|
||||
return get_ausleihung(id)
|
||||
|
||||
|
||||
def cancel_booking(id):
|
||||
"""Compatibility wrapper for the booking route."""
|
||||
return cancel_ausleihung(id)
|
||||
|
||||
|
||||
def remove_ausleihung(id):
|
||||
"""
|
||||
Markiert einen Ausleihungsdatensatz als gelöscht (Soft-Delete).
|
||||
|
||||
Reference in New Issue
Block a user