diff --git a/backend/src/appointment/controller/calendar.py b/backend/src/appointment/controller/calendar.py index ab7a208f8..a50aebfd8 100644 --- a/backend/src/appointment/controller/calendar.py +++ b/backend/src/appointment/controller/calendar.py @@ -938,58 +938,68 @@ def existing_events_for_schedule( if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() - # Create a single connector for this batch of calendars - con = GoogleConnector( - db=db, - redis_instance=redis, - google_client=google_client, - remote_calendar_id=google_calendars[0].user, # This isn't used for get_busy_time but is still needed. - calendar_id=google_calendars[0].id, # This isn't used for get_busy_time but is still needed. - subscriber_id=subscriber.id, - google_tkn=external_connection.token, - ) - - # Batch all calendar IDs for this connection into a single API call - calendar_ids = [calendar.user for calendar in google_calendars] - existing_events.extend( - [ - schemas.Event(start=busy.get('start'), end=busy.get('end'), title='Busy') - for busy in con.get_busy_time(calendar_ids, start.strftime(DATEFMT), end.strftime(DATEFMT)) - ] - ) - - # Process CalDAV calendars individually (no batching support) - for calendar in caldav_calendars: - con = CalDavConnector( - db=db, - redis_instance=redis, - url=calendar.url, - user=calendar.user, - password=calendar.password, - subscriber_id=subscriber.id, - calendar_id=calendar.id, - ) - try: + # Create a single connector for this batch of calendars + con = GoogleConnector( + db=db, + redis_instance=redis, + google_client=google_client, + remote_calendar_id=google_calendars[0].user, # Not used for get_busy_time but needed. + calendar_id=google_calendars[0].id, # Not used for get_busy_time but needed. + subscriber_id=subscriber.id, + google_tkn=external_connection.token, + ) + + # Batch all calendar IDs for this connection into a single API call + calendar_ids = [calendar.user for calendar in google_calendars] existing_events.extend( [ schemas.Event(start=busy.get('start'), end=busy.get('end'), title='Busy') - for busy in con.get_busy_time([calendar.url], start.strftime(DATEFMT), end.strftime(DATEFMT)) + for busy in con.get_busy_time(calendar_ids, start.strftime(DATEFMT), end.strftime(DATEFMT)) ] ) + except RemoteCalendarConnectionError: + raise + except Exception as e: + logging.warning(f'[Tools.existing_events_for_schedule] Google Calendar connection error: {e}') + raise RemoteCalendarConnectionError() - # We're good here, continue along the loop - continue - except caldav.lib.error.ReportError: - logging.debug('[Tools.existing_events_for_schedule] CalDAV server does not support FreeBusy API.') - pass - - # Okay maybe this server doesn't support freebusy, try the old way + # Process CalDAV calendars individually (no batching support) + for calendar in caldav_calendars: try: + con = CalDavConnector( + db=db, + redis_instance=redis, + url=calendar.url, + user=calendar.user, + password=calendar.password, + subscriber_id=subscriber.id, + calendar_id=calendar.id, + ) + + try: + busy_times = con.get_busy_time( + [calendar.url], start.strftime(DATEFMT), end.strftime(DATEFMT) + ) + existing_events.extend( + [ + schemas.Event(start=busy.get('start'), end=busy.get('end'), title='Busy') + for busy in busy_times + ] + ) + + # We're good here, continue along the loop + continue + except caldav.lib.error.ReportError: + logging.debug('[Tools.existing_events_for_schedule] CalDAV server does not support FreeBusy API.') + + # Okay maybe this server doesn't support freebusy, try the old way existing_events.extend(con.list_events(start.strftime(DATEFMT), end.strftime(DATEFMT))) - except requests.exceptions.ConnectionError: - # Connection error with remote caldav calendar, don't crash this route. - pass + except RemoteCalendarConnectionError: + raise + except Exception as e: + logging.warning(f'[Tools.existing_events_for_schedule] CalDAV connection error: {e}') + raise RemoteCalendarConnectionError() # handle already requested time slots for slot in schedule.slots: diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index aed61fff4..109d19c83 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -19,6 +19,7 @@ "authenticationRequired": "Entschuldigung, um diese Seite zu sehen ist eine Anmeldung erforderlich.", "bookingCancelError": "Buchung konnte nicht abgebrochen werden.", "calendarConnectError": "Es gab ein Problem mit der Kalender-Verbindung.", + "calendarConnectionUnavailable": "Die Kalenderverbindung ist derzeit nicht verfügbar. Bitte kontaktiere den Kalenderbesitzer direkt.", "credentialsIncomplete": "Bitte gib deine Zugangsdaten ein.", "dataSourceIsEmpty": "{name} konnte nicht gefunden werden.", "externalAccountHasNoCalendars": "Dein {external}-Konto enthält keine Kalender. Bitte verbinde ein anderes Konto.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 444d86460..1cec7197f 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -19,6 +19,7 @@ "authenticationRequired": "Sorry, this page requires you to be logged in.", "bookingCancelError": "Booking couldn't be cancelled.", "calendarConnectError": "There was a problem with the calendar connection.", + "calendarConnectionUnavailable": "The calendar connection is currently not available. Please contact the calendar owner directly.", "credentialsIncomplete": "Please provide login credentials.", "dataSourceIsEmpty": "No {name} could be found.", "externalAccountHasNoCalendars": "Your {external} account contains no calendars. Please connect a different account.", diff --git a/frontend/src/views/BookerView/index.vue b/frontend/src/views/BookerView/index.vue index 79a4fbb67..e621e3b7b 100644 --- a/frontend/src/views/BookerView/index.vue +++ b/frontend/src/views/BookerView/index.vue @@ -4,6 +4,7 @@ import { inject, onMounted, ref } from 'vue'; import { storeToRefs } from 'pinia'; import { useBookingViewStore } from '@/stores/booking-view-store'; import { dayjsKey, callKey } from '@/keys'; +import { useI18n } from 'vue-i18n'; import { Appointment, Slot, Exception, ExceptionDetail, AppointmentResponse } from '@/models'; @@ -15,6 +16,7 @@ import BookingViewError from './components/BookingViewError.vue'; // component constants const dj = inject(dayjsKey); const call = inject(callKey); +const { t } = useI18n(); const bookingViewStore = useBookingViewStore(); const errorHeading = ref(null); @@ -71,12 +73,20 @@ const handleError = (data: Exception) => { const errorDetail = data?.detail as ExceptionDetail; - if (errorDetail?.id === 'SCHEDULE_NOT_ACTIVE') { - errorHeading.value = ''; - errorBody.value = errorDetail.message; - } else if (errorDetail.id === 'RATE_LIMIT_EXCEEDED') { - errorHeading.value = ''; - errorBody.value = errorDetail.message; + switch (errorDetail?.id) { + case 'SCHEDULE_NOT_ACTIVE': + case 'RATE_LIMIT_EXCEEDED': + errorHeading.value = ''; + errorBody.value = errorDetail.message; + break; + case 'REMOTE_CALENDAR_CONNECTION_ERROR': + errorHeading.value = ''; + errorBody.value = t('error.calendarConnectionUnavailable'); + break; + default: + errorHeading.value = ''; + errorBody.value = t('error.generalBookingError'); + break; } };