Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test initial calendar connection for caldav #245

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions backend/src/appointment/controller/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Handle connection to a CalDAV server.
"""
import json

import caldav.lib.error
import requests
from caldav import DAVClient
from google.oauth2.credentials import Credentials
from icalendar import Calendar, Event, vCalAddress, vText
Expand Down Expand Up @@ -43,6 +46,10 @@ def __init__(
if google_tkn:
self.google_token = Credentials.from_authorized_user_info(json.loads(google_tkn), self.google_client.SCOPES)

def test_connection(self) -> bool:
"""This occurs during Google OAuth login"""
return bool(self.google_token)

def sync_calendars(self):
"""Sync google calendars"""

Expand Down Expand Up @@ -155,6 +162,22 @@ def __init__(self, url: str, user: str, password: str):
# connect to CalDAV server
self.client = DAVClient(url=url, username=user, password=password)

def test_connection(self) -> bool:
"""Ensure the connection information is correct and the calendar connection works"""
cal = self.client.calendar(url=self.url)

try:
supported_comps = cal.get_supported_components()
except IndexError: # Library has an issue with top level urls, probably due to caldav spec?
return False
except requests.exceptions.RequestException: # Max retries exceeded, bad connection, missing schema, etc...
return False
except caldav.lib.error.NotFoundError: # Good server, bad url.
return False

# They need at least VEVENT support for appointment to work.
return 'VEVENT' in supported_comps

def sync_calendars(self):
pass

Expand Down
8 changes: 8 additions & 0 deletions backend/src/appointment/exceptions/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,11 @@ class ZoomNotConnectedException(APIException):

def get_msg(self):
return l10n('zoom-not-connected')


class RemoteCalendarConnectionError(APIException):
id_code = 'REMOTE_CALENDAR_CONNECTION_ERROR'
status_code = 400

def get_msg(self):
return l10n('remote-calendar-connection-error')
2 changes: 2 additions & 0 deletions backend/src/appointment/l10n/de/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ slot-not-found = Es gibt keine freien Zeitfenster zu buchen.
slot-already-taken = Das gewählte Zeitfenster ist nicht mehr verfügbar. Bitte erneut versuchen.
slot-invalid-email = Die angegebene E-Mail-Adresse war nicht gültig. Bitte erneut versuchen.

remote-calendar-connection-error = Der angebundene Kalender konnte nicht erreicht werden. Bitte die Verbindungsinformationen überprüfen und noch einmal versuchen.

## Authentication Exceptions

email-mismatch = E-Mail-Adresse stimmen nicht überein.
Expand Down
2 changes: 2 additions & 0 deletions backend/src/appointment/l10n/en/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ slot-not-found = There are no available time slots to book.
slot-already-taken = The time slot you have selected is no longer available. Please try again.
slot-invalid-email = The email you have provided was not valid. Please try again.

remote-calendar-connection-error = The remote calendar could not be reached. Please verify your connection information and try again.

## Authentication Exceptions

email-mismatch = Email mismatch.
Expand Down
20 changes: 20 additions & 0 deletions backend/src/appointment/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ..dependencies.database import get_db
from ..dependencies.zoom import get_zoom_client
from ..exceptions import validation
from ..exceptions.validation import RemoteCalendarConnectionError
from ..l10n import l10n

router = APIRouter()
Expand Down Expand Up @@ -107,8 +108,27 @@ def create_my_calendar(
calendar: schemas.CalendarConnection,
db: Session = Depends(get_db),
subscriber: Subscriber = Depends(get_subscriber),
google_client: GoogleClient = Depends(get_google_client),
):
"""endpoint to add a new calendar connection for authenticated subscriber"""

# Test the connection first
if calendar.provider == CalendarProvider.google:
# I don't believe google cal touches this route, but just in case!
con = GoogleConnector(
db=db,
google_client=google_client,
calendar_id=calendar.user,
subscriber_id=subscriber.id,
google_tkn=subscriber.google_tkn,
)
else:
con = CalDavConnector(calendar.url, calendar.user, calendar.password)

# Make sure we can connect to the calendar before we save it
if not con.test_connection():
raise RemoteCalendarConnectionError()

# create calendar
try:
cal = repo.create_subscriber_calendar(db=db, calendar=calendar, subscriber_id=subscriber.id)
Expand Down
5 changes: 5 additions & 0 deletions backend/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,17 @@ def create_event(self, event, attendee, organizer):
def delete_event(self, start):
return True

@staticmethod
def test_connection(self):
return True

# Patch up the caldav constructor, and list_calendars
from appointment.controller.calendar import CalDavConnector
monkeypatch.setattr(CalDavConnector, "__init__", MockCaldavConnector.__init__)
monkeypatch.setattr(CalDavConnector, "list_calendars", MockCaldavConnector.list_calendars)
monkeypatch.setattr(CalDavConnector, "create_event", MockCaldavConnector.create_event)
monkeypatch.setattr(CalDavConnector, "delete_events", MockCaldavConnector.delete_event)
monkeypatch.setattr(CalDavConnector, "test_connection", MockCaldavConnector.test_connection)


def _patch_mailer(monkeypatch):
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ const call = createFetch({
}
return { options };
},
async onFetchError({ data, response, error }) {
updateDataOnError: true, // Needed to access the actual error message...
async onFetchError(context) {
const { data, response } = context;
// Catch any google refresh error that may occur
if (
data?.detail?.id === 'GOOGLE_REFRESH_ERROR'
Expand All @@ -86,11 +88,11 @@ const call = createFetch({
// Clear current user data, and ship them to the login screen!
await currentUser.reset();
await router.push('/login');
return;
return context;
}

// Pass the error along
return { data, response, error };
return context;
},
},
fetchOptions: {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/AppointmentCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{{ t("heading.createNewAppointment") }}
</div>
<alert-box
@close="appointmentCreationError = ''"
v-if="appointmentCreationError"
:title="t('label.appointmentCreationError')"
>
Expand Down Expand Up @@ -435,8 +436,7 @@ const createAppointment = async () => {

if (error.value) {
// Error message is in data
appointmentCreationError.value =
data.value.detail || t("error.unknownAppointmentError");
appointmentCreationError.value = data.value?.detail?.message || t("error.unknownAppointmentError");
// Open the form
emit("start");
return;
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/ScheduleCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<span>{{ t("heading.generalAvailability") }}</span>
<switch-toggle v-if="existing" class="mt-0.5" :active="schedule.active" no-legend @changed="toggleActive" />
</div>
<alert-box v-if="scheduleCreationError" :title="t('label.scheduleCreationError')">
<alert-box
@close="scheduleCreationError = ''"
v-if="scheduleCreationError"
:title="t('label.scheduleCreationError')"
>
{{ scheduleCreationError }}
</alert-box>

Expand Down Expand Up @@ -503,7 +507,7 @@ const saveSchedule = async (withConfirmation = true) => {

if (error.value) {
// error message is in data
scheduleCreationError.value = data.value.detail || t("error.unknownScheduleError");
scheduleCreationError.value = data.value?.detail?.message || t("error.unknownScheduleError");
// go back to the start
state.value = scheduleCreationState.details;
savingInProgress.value = false;
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/components/SettingsCalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
<div class="flex flex-col gap-8">
<div class="text-3xl text-gray-500 font-semibold">{{ t('heading.calendarSettings') }}</div>
<div class="pl-6 flex flex-col gap-6">
<alert-box title="Calendar Connect Error" v-if="calendarConnectError">{{calendarConnectError}}</alert-box>
<alert-box
@close="calendarConnectError = ''"
title="Calendar Connect Error"
v-if="calendarConnectError"
>{{calendarConnectError}}</alert-box>

<!-- list of possible calendars to connect -->
<calendar-management
Expand Down Expand Up @@ -327,7 +331,14 @@ const saveCalendar = async () => {

// add new caldav calendar
if (isCalDav.value && inputMode.value === inputModes.add) {
await call('cal').post(calendarInput.data);
const { error, data } = await call('cal').post(calendarInput.data).json();
if (error.value) {
calendarConnectError.value = data.value?.detail?.message;
loading.value = false;
// Show them the error message because I haven't thought this ux process through.
window.scrollTo(0, 0);
return;
}
}
// add all google calendars connected to given gmail address
if (isGoogle.value && inputMode.value === inputModes.add) {
Expand Down
10 changes: 4 additions & 6 deletions frontend/src/elements/AlertBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
text-white shadow-black/30"
role="alert"
:class="{
hidden: isHidden,
'bg-rose-600 dark:bg-rose-900': isError,
'bg-orange-400 dark:bg-orange-700': isAlert,
}"
>
<span class="flex rounded-full uppercase px-2 py-1 text-xs font-bold mr-3"
<span class="flex rounded-full uppercase px-2 py-1 text-center text-xs font-bold mr-3"
:class="{
'bg-rose-500 dark:bg-rose-800': isError,
'bg-orange-500 dark:bg-orange-800': isAlert,
Expand All @@ -21,14 +20,14 @@
<span class="block sm:inline ml-1">
<slot></slot>
</span>
<span class="ml-auto place-self-start" @click="onClose">
<span class="ml-auto" @click="emit('close')">
<icon-x class="h-6 w-6 stroke-1 fill-transparent stroke-white cursor-pointer" />
</span>
</div>
</template>

<script setup>
import { computed, ref } from 'vue';
import { computed } from 'vue';
import { IconX } from '@tabler/icons-vue';

const props = defineProps({
Expand All @@ -42,7 +41,6 @@ const props = defineProps({
const isError = computed(() => props.scheme === 'error');
const isAlert = computed(() => props.scheme === 'alert');

const isHidden = ref(false);
const onClose = () => { isHidden.value = true; };
const emit = defineEmits(['close']);

</script>
Loading