diff --git a/backend/src/appointment/database/models.py b/backend/src/appointment/database/models.py
index d8812167b..fb7f184e0 100644
--- a/backend/src/appointment/database/models.py
+++ b/backend/src/appointment/database/models.py
@@ -170,6 +170,7 @@ class Attendee(Base):
id = Column(Integer, primary_key=True, index=True)
email = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), index=True)
name = Column(StringEncryptedType(String, secret, AesEngine, "pkcs5", length=255), index=True)
+ timezone = Column(String(255), index=True)
slots = relationship("Slot", cascade="all,delete", back_populates="attendee")
diff --git a/backend/src/appointment/database/schemas.py b/backend/src/appointment/database/schemas.py
index a30bc4371..9885e0b51 100644
--- a/backend/src/appointment/database/schemas.py
+++ b/backend/src/appointment/database/schemas.py
@@ -30,6 +30,7 @@
class AttendeeBase(BaseModel):
email: str
name: str | None = None
+ timezone: str
class Attendee(AttendeeBase):
diff --git a/backend/src/appointment/migrations/versions/2024_04_18_0823-89e1197d980d_add_attendee_timezone.py b/backend/src/appointment/migrations/versions/2024_04_18_0823-89e1197d980d_add_attendee_timezone.py
new file mode 100644
index 000000000..5d70c2a18
--- /dev/null
+++ b/backend/src/appointment/migrations/versions/2024_04_18_0823-89e1197d980d_add_attendee_timezone.py
@@ -0,0 +1,24 @@
+"""add attendee timezone
+
+Revision ID: 89e1197d980d
+Revises: fadd0d1ef438
+Create Date: 2024-04-18 08:23:55.660065
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '89e1197d980d'
+down_revision = 'fadd0d1ef438'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.add_column('attendees', sa.Column('timezone', sa.String(255), index=True))
+
+
+def downgrade() -> None:
+ op.drop_column('attendees', 'timezone')
diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py
index f5e545ef4..b07b40fe3 100644
--- a/backend/src/appointment/routes/schedule.py
+++ b/backend/src/appointment/routes/schedule.py
@@ -220,7 +220,12 @@ def request_schedule_availability_slot(
# human readable date in subscribers timezone
# TODO: handle locale date representation
date = slot.start.replace(tzinfo=timezone.utc).astimezone(ZoneInfo(subscriber.timezone)).strftime("%c")
- date = f"{date}, {slot.duration} minutes"
+ date = f"{date}, {slot.duration} minutes ({subscriber.timezone})"
+
+ # human readable date in attendee timezone
+ # TODO: handle locale date representation
+ attendee_date = slot.start.replace(tzinfo=timezone.utc).astimezone(ZoneInfo(slot.attendee.timezone)).strftime("%c")
+ attendee_date = f"{attendee_date}, {slot.duration} minutes ({slot.attendee.timezone})"
# Create a pending appointment
attendee_name = slot.attendee.name if slot.attendee.name is not None else slot.attendee.email
@@ -246,7 +251,7 @@ def request_schedule_availability_slot(
background_tasks.add_task(send_confirmation_email, url=url, attendee=attendee, date=date, to=subscriber.email)
# Sending pending email to attendee
- background_tasks.add_task(send_pending_email, owner=subscriber, date=date, to=slot.attendee.email)
+ background_tasks.add_task(send_pending_email, owner=subscriber, date=attendee_date, to=slot.attendee.email)
# Mini version of slot, so we can grab the newly created slot id for tests
return schemas.SlotOut(
@@ -416,5 +421,9 @@ def decide_on_schedule_availability_slot(
return schemas.AvailabilitySlotAttendee(
slot=schemas.SlotBase(start=slot.start, duration=slot.duration),
- attendee=schemas.AttendeeBase(email=slot.attendee.email, name=slot.attendee.name)
+ attendee=schemas.AttendeeBase(
+ email=slot.attendee.email,
+ name=slot.attendee.name,
+ timezone=slot.attendee.timezone
+ )
)
diff --git a/backend/test/integration/test_appointment.py b/backend/test/integration/test_appointment.py
index a9f1c12e3..87d3c95a6 100644
--- a/backend/test/integration/test_appointment.py
+++ b/backend/test/integration/test_appointment.py
@@ -338,7 +338,7 @@ def test_attendee_selects_slot_of_unavailable_appointment(self, with_db, with_cl
response = with_client.put(
f"/apmt/public/{generated_appointment.slug}",
- json={"slot_id": generated_appointment.slots[-1].id, "attendee": {"email": "a", "name": "b"}},
+ json={"slot_id": generated_appointment.slots[-1].id, "attendee": {"email": "a", "name": "b", "timezone": "c"}},
)
assert response.status_code == 403, response.text
@@ -347,7 +347,7 @@ def test_attendee_selects_slot_of_missing_appointment(self, with_client, make_ap
response = with_client.put(
f"/apmt/public/{generated_appointment}",
- json={"slot_id": generated_appointment.slots[0].id, "attendee": {"email": "a", "name": "b"}},
+ json={"slot_id": generated_appointment.slots[0].id, "attendee": {"email": "a", "name": "b", "timezone": "c"}},
)
assert response.status_code == 404, response.text
@@ -356,7 +356,7 @@ def test_attendee_selects_missing_slot_of_existing_appointment(self, with_client
response = with_client.put(
f"/apmt/public/{generated_appointment.id}",
- json={"slot_id": generated_appointment.slots[0].id + 1, "attendee": {"email": "a", "name": "b"}},
+ json={"slot_id": generated_appointment.slots[0].id + 1, "attendee": {"email": "a", "name": "b", "timezone": "c"}},
)
assert response.status_code == 404, response.text
diff --git a/backend/test/integration/test_schedule.py b/backend/test/integration/test_schedule.py
index 469107eee..2e750d267 100644
--- a/backend/test/integration/test_schedule.py
+++ b/backend/test/integration/test_schedule.py
@@ -462,7 +462,8 @@ def bust_cached_events(self, all_calendars = False):
),
attendee=schemas.AttendeeBase(
email='hello@example.org',
- name='Greg'
+ name='Greg',
+ timezone='Europe/Berlin'
)
).model_dump(mode='json')
diff --git a/backend/test/unit/test_mailer.py b/backend/test/unit/test_mailer.py
index 25b75c3b2..edfb3a81c 100644
--- a/backend/test/unit/test_mailer.py
+++ b/backend/test/unit/test_mailer.py
@@ -17,7 +17,7 @@ def test_confirm(self, faker, with_l10n):
deny_url = 'https://example.org/no'
fake_email = 'to@example.org'
now = datetime.datetime.now()
- attendee = schemas.AttendeeBase(email=faker.email(), name=faker.name())
+ attendee = schemas.AttendeeBase(email=faker.email(), name=faker.name(), timezone='Europe/Berlin')
mailer = ConfirmationMail(confirm_url, deny_url, attendee, now, to=fake_email)
assert mailer.html()
diff --git a/frontend/src/components/BookingModal.vue b/frontend/src/components/BookingModal.vue
index 946ce240e..a3121ffca 100644
--- a/frontend/src/components/BookingModal.vue
+++ b/frontend/src/components/BookingModal.vue
@@ -79,9 +79,7 @@
diff --git a/frontend/src/elements/PrimaryButton.vue b/frontend/src/elements/PrimaryButton.vue
index 54cbe2ba9..ec950cfc8 100644
--- a/frontend/src/elements/PrimaryButton.vue
+++ b/frontend/src/elements/PrimaryButton.vue
@@ -36,7 +36,7 @@