From 4f6b2b08eff4a98f4b06a2ace4783090c2728a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Fri, 17 Jan 2025 12:47:34 +0100 Subject: [PATCH 01/20] =?UTF-8?q?=E2=9E=95=20Extend=20subscriber=20model?= =?UTF-8?q?=20with=20additional=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/appointment/database/models.py | 20 +++++++++-- backend/src/appointment/database/schemas.py | 4 +++ ..._add_config_fields_to_subscribers_table.py | 33 +++++++++++++++++++ backend/src/appointment/routes/api.py | 4 ++- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 backend/src/appointment/migrations/versions/2025_01_15_1340-4a15d01919b8_add_config_fields_to_subscribers_table.py diff --git a/backend/src/appointment/database/models.py b/backend/src/appointment/database/models.py index 08595cdaa..2a8abc9ed 100644 --- a/backend/src/appointment/database/models.py +++ b/backend/src/appointment/database/models.py @@ -85,6 +85,17 @@ class InviteStatus(enum.Enum): revoked = 2 # The code is no longer valid and cannot be used for sign up anymore +class ColorScheme(enum.Enum): + system = 'system' + dark = 'dark' + light = 'light' + + +class TimeMode(enum.Enum): + h12 = 12 + h24 = 24 + + def encrypted_type(column_type, length: int = 255, **kwargs) -> StringEncryptedType: """Helper to reduce visual noise when creating model columns""" return StringEncryptedType(column_type, secret, AesEngine, 'pkcs5', length=length, **kwargs) @@ -134,12 +145,15 @@ class Subscriber(HasSoftDelete, Base): name = Column(encrypted_type(String), index=True) level = Column(Enum(SubscriberLevel), default=SubscriberLevel.basic, index=True) - language = Column(encrypted_type(String), nullable=False, default=FALLBACK_LOCALE, index=True) - timezone = Column(encrypted_type(String), index=True) avatar_url = Column(encrypted_type(String, length=2048), index=False) - short_link_hash = Column(encrypted_type(String), index=False) + # General settings + language = Column(encrypted_type(String), nullable=False, default=FALLBACK_LOCALE, index=True) + timezone = Column(encrypted_type(String), index=True) + color_scheme = Column(Enum(ColorScheme), default=ColorScheme.system, nullable=False, index=True) + time_mode = Column(Enum(TimeMode), default=TimeMode.h24, nullable=False, index=True) + # Only accept the times greater than the one specified in the `iat` claim of the jwt token minimum_valid_iat_time = Column('minimum_valid_iat_time', encrypted_type(DateTime)) diff --git a/backend/src/appointment/database/schemas.py b/backend/src/appointment/database/schemas.py index c0d5e94a8..c45bad1b0 100644 --- a/backend/src/appointment/database/schemas.py +++ b/backend/src/appointment/database/schemas.py @@ -27,6 +27,8 @@ ExternalConnectionType, MeetingLinkProviderType, InviteStatus, + ColorScheme, + TimeMode, ) from .. import utils, defines @@ -299,6 +301,8 @@ class SubscriberIn(BaseModel): avatar_url: str | None = None secondary_email: str | None = None language: str | None = FALLBACK_LOCALE + color_scheme: ColorScheme = ColorScheme.system + time_mode: TimeMode = TimeMode.h24 class SubscriberBase(SubscriberIn): diff --git a/backend/src/appointment/migrations/versions/2025_01_15_1340-4a15d01919b8_add_config_fields_to_subscribers_table.py b/backend/src/appointment/migrations/versions/2025_01_15_1340-4a15d01919b8_add_config_fields_to_subscribers_table.py new file mode 100644 index 000000000..eb7cf72ee --- /dev/null +++ b/backend/src/appointment/migrations/versions/2025_01_15_1340-4a15d01919b8_add_config_fields_to_subscribers_table.py @@ -0,0 +1,33 @@ +"""Add config fields to subscribers table + +Revision ID: 4a15d01919b8 +Revises: 0c99f6a02f3b +Create Date: 2025-01-15 13:40:12.022117 + +""" +import os +from alembic import op +import sqlalchemy as sa +from sqlalchemy_utils import StringEncryptedType +from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine +from appointment.database.models import ColorScheme, TimeMode + + +def secret(): + return os.getenv('DB_SECRET') + +# revision identifiers, used by Alembic. +revision = '4a15d01919b8' +down_revision = '0c99f6a02f3b' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column('subscribers', sa.Column('color_scheme', sa.Enum(ColorScheme), default=ColorScheme.system, nullable=False, index=True)) + op.add_column('subscribers', sa.Column('time_mode', sa.Enum(TimeMode), default=TimeMode.h24, nullable=False, index=True)) + + +def downgrade() -> None: + op.drop_column('subscribers', 'color_scheme') + op.drop_column('subscribers', 'time_mode') diff --git a/backend/src/appointment/routes/api.py b/backend/src/appointment/routes/api.py index 99a4d96df..5385482d4 100644 --- a/backend/src/appointment/routes/api.py +++ b/backend/src/appointment/routes/api.py @@ -82,7 +82,9 @@ def update_me( is_setup=me.is_setup, avatar_url=me.avatar_url, schedule_links=schedule_links_by_subscriber(db, subscriber), - unique_hash=me.unique_hash + unique_hash=me.unique_hash, + color_scheme=me.color_scheme, + time_mode=me.time_mode, ) From 3cb51f008058e742a14d9989d1970ce16a87da29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Fri, 17 Jan 2025 13:04:32 +0100 Subject: [PATCH 02/20] =?UTF-8?q?=E2=9E=95=20Extend=20user=20store=20with?= =?UTF-8?q?=20additional=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.vue | 2 +- frontend/src/components/BookingModal.vue | 4 +- frontend/src/components/FTUE/SetupProfile.vue | 2 +- .../src/components/FTUE/SetupSchedule.vue | 2 +- frontend/src/components/ScheduleCreation.vue | 8 +- frontend/src/components/SettingsGeneral.vue | 62 +++--------- frontend/src/locales/de.json | 2 +- frontend/src/models.ts | 34 +++++-- frontend/src/stores/schedule-store.ts | 6 +- frontend/src/stores/user-store.ts | 94 +++++++++++++++++-- frontend/src/views/ProfileView.vue | 2 +- frontend/test/stores/user-store.test.js | 2 +- 12 files changed, 137 insertions(+), 83 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e2a837138..89461f09f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -29,7 +29,7 @@ import { useAppointmentStore } from '@/stores/appointment-store'; import { useScheduleStore } from '@/stores/schedule-store'; // component constants -const currentUser = useUserStore(); // data: { username, email, name, level, timezone, id } +const currentUser = useUserStore(); const apiUrl = inject(apiUrlKey); const route = useRoute(); const routeName = typeof route.name === 'string' ? route.name : ''; diff --git a/frontend/src/components/BookingModal.vue b/frontend/src/components/BookingModal.vue index b59334283..aac30861b 100644 --- a/frontend/src/components/BookingModal.vue +++ b/frontend/src/components/BookingModal.vue @@ -70,8 +70,8 @@ onMounted(() => { if (user.exists()) { attendee.name = user.data.name; attendee.email = user.data.preferredEmail; - if (user.data.timezone !== null) { - attendee.timezone = user.data.timezone; + if (user.data.settings.timezone !== null) { + attendee.timezone = user.data.settings.timezone; } } }); diff --git a/frontend/src/components/FTUE/SetupProfile.vue b/frontend/src/components/FTUE/SetupProfile.vue index a25fe9a21..a015fd520 100644 --- a/frontend/src/components/FTUE/SetupProfile.vue +++ b/frontend/src/components/FTUE/SetupProfile.vue @@ -29,7 +29,7 @@ const timezoneOptions = Intl.supportedValuesOf('timeZone').map((timezone: string const formRef = ref(); const fullName = ref(user.data?.name ?? ''); const username = ref(user.data?.username ?? ''); -const timezone = ref(user.data.timezone ?? dj.tz.guess()); +const timezone = ref(user.data.settings.timezone ?? dj.tz.guess()); const isLoading = ref(false); // Form validation diff --git a/frontend/src/components/FTUE/SetupSchedule.vue b/frontend/src/components/FTUE/SetupSchedule.vue index ce2f2b12e..a4174bf3d 100644 --- a/frontend/src/components/FTUE/SetupSchedule.vue +++ b/frontend/src/components/FTUE/SetupSchedule.vue @@ -98,7 +98,7 @@ const onSubmit = async () => { farthest_booking: 20160, start_date: dj().format(DateFormatStrings.QalendarFullDay), details: schedule.value?.details ?? '', - timezone: user.data.timezone, + timezone: user.data.settings.timezone, }; const data = schedules.value.length > 0 diff --git a/frontend/src/components/ScheduleCreation.vue b/frontend/src/components/ScheduleCreation.vue index e5cb43d08..1e4405bcb 100644 --- a/frontend/src/components/ScheduleCreation.vue +++ b/frontend/src/components/ScheduleCreation.vue @@ -274,15 +274,15 @@ const savingInProgress = ref(false); const saveSchedule = async (withConfirmation = true) => { savingInProgress.value = true; // build data object for post request - const obj = { ...scheduleInput.value, timezone: user.data.timezone }; + const obj = { ...scheduleInput.value, timezone: user.data.settings.timezone }; // convert local input times to utc times obj.start_time = dj(`${dj().format('YYYY-MM-DD')}T${obj.start_time}:00`) - .tz(user.data.timezone ?? dj.tz.guess(), true) + .tz(user.data.settings.timezone ?? dj.tz.guess(), true) .utc() .format('HH:mm'); obj.end_time = dj(`${dj().format('YYYY-MM-DD')}T${obj.end_time}:00`) - .tz(user.data.timezone ?? dj.tz.guess(), true) + .tz(user.data.settings.timezone ?? dj.tz.guess(), true) .utc() .format('HH:mm'); // Update the start_date with the current date @@ -517,7 +517,7 @@ watch( {{ t("label.timeZone") }}
-
{{ user.data.timezone ?? dj.tz.guess() }}
+
{{ user.data.settings.timezone ?? dj.tz.guess() }}
{{ t('label.edit') }} diff --git a/frontend/src/components/SettingsGeneral.vue b/frontend/src/components/SettingsGeneral.vue index 5c07b6a91..95bc8dd6b 100644 --- a/frontend/src/components/SettingsGeneral.vue +++ b/frontend/src/components/SettingsGeneral.vue @@ -1,44 +1,32 @@