From 9ad81d9b6df3b04b1143a0951c93c716ee9dceef Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Tue, 15 Sep 2026 20:20:52 +0200 Subject: [PATCH] some more improvements for the appointment borrowing function --- Web/app.py | 141 +++++++++++++++++------------ Web/modules/database/ausleihung.py | 12 ++- 2 files changed, 95 insertions(+), 58 deletions(-) diff --git a/Web/app.py b/Web/app.py index ff0894f..3558f9f 100755 --- a/Web/app.py +++ b/Web/app.py @@ -8058,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 @@ -8091,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 @@ -8110,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 @@ -8214,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) @@ -8243,7 +8251,22 @@ def add_booking(): 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( @@ -8252,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/', methods=['POST']) def cancel_booking(id): @@ -8276,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} diff --git a/Web/modules/database/ausleihung.py b/Web/modules/database/ausleihung.py index f6e8a97..0a45682 100755 --- a/Web/modules/database/ausleihung.py +++ b/Web/modules/database/ausleihung.py @@ -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).