Skip to content

Commit

Permalink
Adjusted calendar settings flow (#381)
Browse files Browse the repository at this point in the history
* Adjusted calendar settings flow:
* You can now disconnect connected calendars without nuking them.
* You can now remove (delete) calendars that are unconnected.
* Sync still brings them back if they're from a google account.
* Schedule Settings will become inactive if the connected calendar is disconnected.
* Schedule Settings will be deleted if an unconnected calendar that is associated is deleted. (Cascade all from models.Calendar)
* Schedule Settings becomes active if a previously unconnected calendar becomes connected again.

* 🌐 Update lang strings

---------

Co-authored-by: Andreas Müller <[email protected]>
  • Loading branch information
MelissaAutumn and devmount authored Apr 30, 2024
1 parent 8bd9246 commit 82e8ea8
Show file tree
Hide file tree
Showing 10 changed files with 88 additions and 24 deletions.
6 changes: 6 additions & 0 deletions backend/src/appointment/database/repo/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ def exists(db: Session, schedule_id: int):
return True if get(db, schedule_id) is not None else False


def is_calendar_connected(db: Session, schedule_id: int) -> bool:
"""true if the schedule's calendar is connected"""
schedule: models.Schedule = get(db, schedule_id)
return schedule.calendar and schedule.calendar.connected


def update(db: Session, schedule: schemas.ScheduleBase, schedule_id: int):
"""update existing schedule by id"""
db_schedule = get(db, schedule_id)
Expand Down
1 change: 1 addition & 0 deletions backend/src/appointment/database/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ class Schedule(ScheduleBase):
time_created: datetime | None = None
time_updated: datetime | None = None
availabilities: list[Availability] = []
calendar: 'CalendarBase'

class Config:
from_attributes = True
Expand Down
15 changes: 11 additions & 4 deletions backend/src/appointment/routes/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
import secrets
from typing import Annotated

import requests.exceptions
import validators
Expand All @@ -18,7 +19,7 @@

# authentication
from ..controller.calendar import CalDavConnector, Tools, GoogleConnector
from fastapi import APIRouter, Depends, HTTPException, Body, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, Body, BackgroundTasks, Query, Request
from datetime import timedelta, timezone
from ..controller.apis.google_client import GoogleClient
from ..controller.auth import signed_url_by_subscriber
Expand Down Expand Up @@ -194,19 +195,25 @@ def update_my_calendar(


@router.post("/cal/{id}/connect", response_model=schemas.CalendarOut)
def connect_my_calendar(
@router.post("/cal/{id}/disconnect", response_model=schemas.CalendarOut)
def change_my_calendar_connection(
request: Request,
id: int,
db: Session = Depends(get_db),
subscriber: Subscriber = Depends(get_subscriber),
):
"""endpoint to update an existing calendar connection for authenticated subscriber"""
"""endpoint to update an existing calendar connection for authenticated subscriber
note this function handles both disconnect and connect (the double route is not a typo.)"""
if not repo.calendar.exists(db, calendar_id=id):
raise validation.CalendarNotFoundException()
if not repo.calendar.is_owned(db, calendar_id=id, subscriber_id=subscriber.id):
raise validation.CalendarNotAuthorizedException()

# If our path ends with /connect then connect the calendar, otherwise disconnect the calendar
connect = request.scope.get('path', '').endswith('/connect')

try:
cal = repo.calendar.update_connection(db=db, calendar_id=id, is_connected=True)
cal = repo.calendar.update_connection(db=db, calendar_id=id, is_connected=connect)
except HTTPException as e:
raise HTTPException(status_code=e.status_code, detail=e.detail)
return schemas.CalendarOut(id=cal.id, title=cal.title, color=cal.color, connected=cal.connected)
Expand Down
6 changes: 6 additions & 0 deletions backend/src/appointment/routes/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ def update_schedule(
"""endpoint to update an existing calendar connection for authenticated subscriber"""
if not repo.schedule.exists(db, schedule_id=id):
raise validation.ScheduleNotFoundException()
if not repo.calendar.is_connected(db, calendar_id=schedule.calendar_id):
raise validation.CalendarNotConnectedException()
if not repo.schedule.is_owned(db, schedule_id=id, subscriber_id=subscriber.id):
raise validation.ScheduleNotAuthorizedException()
if schedule.meeting_link_provider == MeetingLinkProviderType.zoom and subscriber.get_external_connection(ExternalConnectionType.zoom) is None:
Expand Down Expand Up @@ -108,6 +110,10 @@ def read_schedule_availabilities(
if not schedule.active:
raise validation.ScheduleNotActive()

# check if calendar is connected, if its not then its a schedule not active error
if not schedule.calendar or not schedule.calendar.connected:
raise validation.ScheduleNotActive()

calendars = repo.calendar.get_by_subscriber(db, subscriber.id, False)

if not calendars or len(calendars) == 0:
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CalendarManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
{{ t('label.editCalendar') }}
</button>
<button
v-if="cal.connected"
v-if="!cal.connected"
class="bg-transparent p-0.5 disabled:scale-100 disabled:opacity-50 disabled:shadow-none"
:disabled="loading"
@click="emit('remove', cal.id)"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ConfirmationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div class="text-2xl font-semibold text-teal-500">
{{ title }}
</div>
<div class="max-w-xs text-center">
<div class="max-w-sm text-center">
{{ message }}
</div>
<div class="flex gap-4">
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/ScheduleCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ const props = defineProps({
});
// check if existing schedule is given
const existing = computed(() => Boolean(props.schedule));
const existing = computed(() => Boolean(props.schedule) && Boolean(props.schedule.calendar.connected));
// schedule creation state indicating the current step
const state = ref(firstStep);
Expand Down Expand Up @@ -450,6 +450,12 @@ onMounted(() => {
.utc(true)
.tz(user.data.timezone ?? dj.tz.guess())
.format('HH:mm');
// Adjust the default calendar if the one attached is not connected.
const { calendar_id: calendarId } = scheduleInput.value;
if (!props.calendars[calendarId] || !props.calendars[calendarId].connected) {
scheduleInput.value.calendar_id = props.calendars[0]?.id;
}
} else {
scheduleInput.value = { ...defaultSchedule };
}
Expand Down
26 changes: 23 additions & 3 deletions frontend/src/components/SettingsCalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
:calendars="calendarStore.unconnectedCalendars"
:loading="loading"
@sync="syncCalendars"
@remove="deleteCalendar"
@modify="connectCalendar"
/>

Expand All @@ -24,7 +25,6 @@
:type="calendarManagementType.edit"
:calendars="calendarStore.connectedCalendars"
:loading="loading"
@remove="deleteCalendar"
@modify="editCalendar"
/>

Expand Down Expand Up @@ -162,7 +162,16 @@
class="w-full max-w-sm rounded-md"
/>
</label>
<div class="flex gap-4 self-end">
<div class="flex justify-between gap-4">
<div class="flex">
<caution-button
v-if="editMode"
:label="t('label.disconnect')"
class="text-sm"
@click="() => disconnectCalendar(calendarInput.id)"
/>
</div>
<div class="flex gap-4 self-end">
<secondary-button
:label="t('label.cancel')"
class="text-sm !text-teal-500"
Expand All @@ -182,6 +191,7 @@
:label="t('label.connectGoogleCalendar')"
@click="saveCalendar"
/>
</div>
</div>
</div>

Expand All @@ -192,8 +202,9 @@
:open="deleteCalendarModalOpen"
:title="t('label.calendarDeletion')"
:message="t('text.calendarDeletionWarning')"
:confirm-label="t('label.disconnect')"
:confirm-label="t('label.calendarDeletion')"
:cancel-label="t('label.cancel')"
:useCautionButton="true"
@confirm="deleteCalendarConfirm"
@close="closeModals"
></ConfirmationModal>
Expand All @@ -214,6 +225,7 @@ import PrimaryButton from '@/elements/PrimaryButton';
import SecondaryButton from '@/elements/SecondaryButton';
import ConfirmationModal from '@/components/ConfirmationModal.vue';
import { useCalendarStore } from '@/stores/calendar-store';
import CautionButton from '@/elements/CautionButton.vue';
// component constants
const { t } = useI18n({ useScope: 'global' });
Expand Down Expand Up @@ -289,6 +301,14 @@ const connectCalendar = async (id) => {
await call(`cal/${id}/connect`).post();
await refreshData();
await resetInput();
};
const disconnectCalendar = async (id) => {
loading.value = true;
await call(`cal/${id}/disconnect`).post();
await refreshData();
await resetInput();
};
const syncCalendars = async () => {
loading.value = true;
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"busy": "Beschäftigt",
"caldav": "CalDAV",
"calendar": "Kalender",
"calendarDeletion": "Kalendarverbindung trennen",
"calendarDeletion": "Kalendarverbindung entfernen",
"calendarUrl": "Kalender URL",
"cancel": "Abbrechen",
"checkAvailableSlots": "Verfügbare Zeiten anzeigen",
Expand Down Expand Up @@ -160,6 +160,7 @@
"logIn": "Anmelden",
"logOut": "Abmelden",
"loginToContinue": "Anmelden um fortzufahren",
"meetingDetails": "Meeting Details",
"message": "Nachricht",
"month": "Monat",
"myLink": "Mein Link",
Expand All @@ -168,6 +169,7 @@
"notConnected": "Nicht verbunden",
"notProvided": "Keine Angabe",
"notes": "Details",
"now": "Sofort",
"off": "Aus",
"on": "An",
"online": "Online",
Expand All @@ -183,10 +185,17 @@
"replies": "Antworten",
"requested": "Angefragt",
"restoreColumnOrder": "Spaltenreihenfolge wiederherstellen",
"revert": "Zurücksetzen",
"save": "Speichern",
"saveChanges": "Änderungen speichern",
"schedule": "Zeitplan",
"scheduleCreationError": "Fehler beim Speichern des Zeitplans",
"scheduleDetails": "Zeitplan Details",
"scheduleSettings": {
"availabilitySubHeading": "Lege deine Verfügbarkeit fest",
"meetingDetailsSubHeading": "Videos oder Notizen den Events hinzufügen",
"schedulingDetailsSubHeading": "Buchungsintervalle und Dauer festlegen"
},
"search": "Suche",
"searchAppointments": "Termine durchsuchen",
"secondaryTimeZone": "Sekundäre Zeitzone",
Expand Down Expand Up @@ -230,6 +239,7 @@
"biWeeklyCafeDates": "Zweiwöchentliche Café-Treffen…",
"emailAddress": "max.muster{'@'}beispiel.de",
"firstAndLastName": "Vor- und Nachname",
"mySchedule": "Mein Zeitplan",
"never": "Nie",
"writeHere": "Hier notieren…",
"zoomCom": "zoom.com…"
Expand Down Expand Up @@ -275,9 +285,17 @@
"recipientsCanScheduleBetween": "Empfänger können einen Termin zwischen {earliest} und {farthest} ab dem aktuellen Zeitpunkt wählen. ",
"refreshLinkNotice": "This will refresh your short link. Please note that any old short links will no longer work.",
"requestInformationSentToOwner": "Der Kalenderbesitzer wurde per E-Mail über deine Buchungsanfrage informiert.",
"scheduleSettings": {
"clickHereToConnect": "Hier einen Kalender verbinden",
"create": "Wähle einen Kalender unter Zeitplan Details und klicke Speichern!",
"formDirty": "Es gibt ungespeicherte Änderungen.",
"noCalendars": "Dein Zeitplan benötigt mindestens einen verbundenen Kalender.",
"notActive": "Der Zeitplan ist inaktiv, aktiviere ihn zum Bearbeiten oder Teilen."
},
"timesAreDisplayedInLocalTimezone": "Die Zeiten werden in deiner lokalen Zeitzone {timezone} angezeigt.",
"titleIsReadyForBookings": "{title} ist für Buchungen bereit",
"updateUsernameNotice": "Ein Ändern des Benutzernamens aktualisiert auch den Kurzlink. Alle alten Kurzlinks werden dann nicht mehr funktionieren."
"updateUsernameNotice": "Ein Ändern des Benutzernamens aktualisiert auch den Kurzlink. Alle alten Kurzlinks werden dann nicht mehr funktionieren.",
"videoLinkNotice": "Der Videolink verwendet denselben Link für alle Veranstaltungen, die im Rahmen deines Zeitplans erstellt werden. Bei der Erstellung von Zoom-Meetings wird für jede erstellte Veranstaltung ein neuer Zoom-Link erstellt."
},
"units": {
"minutes": "{value} Minuten",
Expand Down
24 changes: 12 additions & 12 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"busy": "Busy",
"caldav": "CalDAV",
"calendar": "Calendar",
"calendarDeletion": "Disconnect Calendar",
"calendarDeletion": "Remove Calendar",
"calendarUrl": "Calendar URL",
"cancel": "Cancel",
"checkAvailableSlots": "Check available slots",
Expand Down Expand Up @@ -152,7 +152,6 @@
"google": "Google",
"guest": "{count} guests | {count} guest | {count} guests",
"immediately": "instant",
"now": "now",
"inPerson": "In person",
"language": "Choose language",
"legal": "Legal",
Expand All @@ -170,6 +169,7 @@
"notConnected": "Not connected",
"notProvided": "Not Provided",
"notes": "Notes",
"now": "now",
"off": "Off",
"on": "On",
"online": "Online",
Expand All @@ -193,8 +193,8 @@
"scheduleDetails": "Scheduling Details",
"scheduleSettings": {
"availabilitySubHeading": "Set your availability days and times",
"schedulingDetailsSubHeading": "Set booking intervals and duration",
"meetingDetailsSubHeading": "Add video and/or notes to events"
"meetingDetailsSubHeading": "Add video and/or notes to events",
"schedulingDetailsSubHeading": "Set booking intervals and duration"
},
"search": "Search",
"searchAppointments": "Search bookings",
Expand Down Expand Up @@ -237,17 +237,17 @@
},
"placeholder": {
"biWeeklyCafeDates": "Bi-weekly Café Dates…",
"mySchedule": "My Schedule",
"emailAddress": "john.doe{'@'}example.com",
"firstAndLastName": "First and last name",
"mySchedule": "My Schedule",
"never": "never",
"writeHere": "Write here…",
"zoomCom": "zoom.com…"
},
"text": {
"accountDataNotice": "Download all of your data from Thunderbird Appointment.",
"accountDeletionWarning": "Your account and data on Thunderbird Appointment will be deleted. This does not impact your linked calendars. And you can create a new account with us anytime.",
"calendarDeletionWarning": "Disconnecting this calendar will remove all appointments and schedules from Thunderbird Appointment. Any confirmed events currently stored in your calendar will not be removed.",
"calendarDeletionWarning": "Removing this calendar will remove all appointments and schedules from Thunderbird Appointment. Any confirmed events currently stored in your calendar will not be removed.",
"chooseDateAndTime": "Choose when to meet.",
"connectZoom": "You can connect your Zoom account to enable instant meeting invites with your appointments.",
"contactRequestForm": "Please use the contact form below to send any feedback, questions, or concerns to our support team. If needed we will try and contact you for further information as soon as possible.",
Expand Down Expand Up @@ -282,16 +282,16 @@
"continueToFxa": "Enter your email above to continue to Mozilla Accounts"
},
"nameIsInvitingYou": "{name} is inviting you",
"recipientsCanScheduleBetween": "Recipients can schedule a {duration} appointment between {earliest} and {farthest} ahead of time.",
"refreshLinkNotice": "This refreshes your link. Your old links will no longer work.",
"requestInformationSentToOwner": "An information about this booking request has been emailed to the owner.",
"scheduleSettings": {
"clickHereToConnect": "Click here to connect a calendar!",
"create": "Select a calendar under Scheduling Details and click save to get started!",
"formDirty": "You have unsaved changes.",
"noCalendars": "Scheduling requires at least one connected calendar.",
"clickHereToConnect": "Click here to connect a calendar!",
"notActive": "Schedule is not active, turn on the toggle to edit or share.",
"formDirty": "You have unsaved changes."
"notActive": "Schedule is not active, turn on the toggle to edit or share."
},
"recipientsCanScheduleBetween": "Recipients can schedule a {duration} appointment between {earliest} and {farthest} ahead of time.",
"refreshLinkNotice": "This refreshes your link. Your old links will no longer work.",
"requestInformationSentToOwner": "An information about this booking request has been emailed to the owner.",
"timesAreDisplayedInLocalTimezone": "Times are displayed in your local timezone {timezone}.",
"titleIsReadyForBookings": "{title} is ready for bookings",
"updateUsernameNotice": "Changing your username will also change your link. Your old link will no longer work.",
Expand Down

0 comments on commit 82e8ea8

Please sign in to comment.