diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index d6383edee..36f0fbcdc 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -45,6 +45,7 @@ jobs: deploy-iac: needs: detect-changes + if: needs.detect-changes.outputs.deploy-iac == 'true' environment: staging runs-on: ubuntu-latest @@ -118,6 +119,7 @@ jobs: needs: - detect-changes - deploy-iac + if: | always() && (needs.deploy-iac.result == 'success' || needs.deploy-iac.result == 'skipped') && @@ -197,6 +199,7 @@ jobs: needs: - detect-changes - deploy-iac + if: | always() && (needs.deploy-iac.result == 'success' || needs.deploy-iac.result == 'skipped') && @@ -279,9 +282,11 @@ jobs: - detect-changes - deploy-backend - deploy-frontend + if: | needs.detect-changes.outputs.deploy-backend == 'true' && needs.detect-changes.outputs.deploy-frontend == 'true' + environment: staging runs-on: ubuntu-latest steps: diff --git a/backend/.env.example b/backend/.env.example index 541c15619..0949ad5d7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -99,6 +99,8 @@ REDIS_DB=0 # No value = Python None REDIS_PASSWORD REDIS_USE_SSL=False +# Connect to a redis cluster instead of a single instance +REDIS_USE_CLUSTER=False # In minutes, the time a cached remote event will expire at. REDIS_EVENT_EXPIRE_TIME=15 diff --git a/backend/src/appointment/controller/calendar.py b/backend/src/appointment/controller/calendar.py index 4b981ed64..77ac7a451 100644 --- a/backend/src/appointment/controller/calendar.py +++ b/backend/src/appointment/controller/calendar.py @@ -4,13 +4,12 @@ """ import json import logging -import uuid import zoneinfo import os import caldav.lib.error import requests -from redis import Redis +from redis import Redis, RedisCluster from caldav import DAVClient from fastapi import BackgroundTasks from google.oauth2.credentials import Credentials @@ -29,11 +28,11 @@ class BaseConnector: - redis_instance: Redis | None + redis_instance: Redis | RedisCluster | None subscriber_id: int calendar_id: int - def __init__(self, subscriber_id: int, calendar_id: int | None, redis_instance: Redis | None = None): + def __init__(self, subscriber_id: int, calendar_id: int | None, redis_instance: Redis | RedisCluster | None = None): self.redis_instance = redis_instance self.subscriber_id = subscriber_id self.calendar_id = calendar_id diff --git a/backend/src/appointment/controller/mailer.py b/backend/src/appointment/controller/mailer.py index a0b07419d..12798bfa3 100644 --- a/backend/src/appointment/controller/mailer.py +++ b/backend/src/appointment/controller/mailer.py @@ -168,10 +168,10 @@ def text(self): class ConfirmationMail(Mailer): - def __init__(self, confirm_url, deny_url, attendee, date, *args, **kwargs): + def __init__(self, confirm_url, deny_url, attendee_name, attendee_email, date, *args, **kwargs): """init Mailer with confirmation specific defaults""" - self.attendee = attendee - self.attendee.name = self.attendee.name.title() + self.attendee_name = attendee_name + self.attendee_email = attendee_email self.date = date self.confirmUrl = confirm_url self.denyUrl = deny_url @@ -182,8 +182,8 @@ def __init__(self, confirm_url, deny_url, attendee, date, *args, **kwargs): def text(self): return l10n('confirm-mail-plain', { - 'attendee_name': self.attendee.name, - 'attendee_email': self.attendee.email, + 'attendee_name': self.attendee_name, + 'attendee_email': self.attendee_email, 'date': self.date, 'confirm_url': self.confirmUrl, 'deny_url': self.denyUrl, @@ -191,7 +191,8 @@ def text(self): def html(self): return get_template("confirm.jinja2").render( - attendee=self.attendee, + attendee_name=self.attendee_name, + attendee_email=self.attendee_email, date=self.date, confirm=self.confirmUrl, deny=self.denyUrl, @@ -199,9 +200,9 @@ def html(self): class RejectionMail(Mailer): - def __init__(self, owner, date, *args, **kwargs): + def __init__(self, owner_name, date, *args, **kwargs): """init Mailer with rejection specific defaults""" - self.owner = owner + self.owner_name = owner_name self.date = date default_kwargs = { "subject": l10n('reject-mail-subject') @@ -210,18 +211,18 @@ def __init__(self, owner, date, *args, **kwargs): def text(self): return l10n('reject-mail-plain', { - 'owner_name': self.owner.name, + 'owner_name': self.owner_name, 'date': self.date }) def html(self): - return get_template("rejected.jinja2").render(owner=self.owner, date=self.date) + return get_template("rejected.jinja2").render(owner_name=self.owner_name, date=self.date) class PendingRequestMail(Mailer): - def __init__(self, owner, date, *args, **kwargs): + def __init__(self, owner_name, date, *args, **kwargs): """init Mailer with pending specific defaults""" - self.owner = owner + self.owner_name = owner_name self.date = date default_kwargs = { "subject": l10n('pending-mail-subject') @@ -230,18 +231,19 @@ def __init__(self, owner, date, *args, **kwargs): def text(self): return l10n('pending-mail-plain', { - 'owner_name': self.owner.name, + 'owner_name': self.owner_name, 'date': self.date }) def html(self): - return get_template("pending.jinja2").render(owner=self.owner, date=self.date) + return get_template("pending.jinja2").render(owner_name=self.owner_name, date=self.date) class SupportRequestMail(Mailer): - def __init__(self, requestee, topic, details, *args, **kwargs): + def __init__(self, requestee_name, requestee_email, topic, details, *args, **kwargs): """init Mailer with support specific defaults""" - self.requestee = requestee + self.requestee_name = requestee_name + self.requestee_email = requestee_email self.topic = topic self.details = details default_kwargs = { @@ -251,14 +253,14 @@ def __init__(self, requestee, topic, details, *args, **kwargs): def text(self): return l10n('support-mail-plain', { - 'requestee_name': self.requestee.name, - 'requestee_email': self.requestee.email, + 'requestee_name': self.requestee_name, + 'requestee_email': self.requestee_email, 'topic': self.topic, 'details': self.details, }) def html(self): - return get_template("support.jinja2").render(requestee=self.requestee, topic=self.topic, details=self.details) + return get_template("support.jinja2").render(requestee_name=self.requestee_name, requestee_email=self.requestee_email, topic=self.topic, details=self.details) class InviteAccountMail(Mailer): diff --git a/backend/src/appointment/dependencies/database.py b/backend/src/appointment/dependencies/database.py index 6f6f435dc..67d5ecc62 100644 --- a/backend/src/appointment/dependencies/database.py +++ b/backend/src/appointment/dependencies/database.py @@ -1,6 +1,6 @@ import os -from redis import Redis +from redis import Redis, RedisCluster from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -29,17 +29,33 @@ def get_db(): db.close() -def get_redis() -> Redis | None: +def get_redis() -> Redis | RedisCluster | None: """Retrieves a redis instance or None if redis isn't available.""" # TODO: Create pool and simply grab instance? if os.getenv('REDIS_URL') is None: return None + host = os.getenv('REDIS_URL') + port = int(os.getenv('REDIS_PORT')) + db = os.getenv('REDIS_DB') + password = os.getenv('REDIS_PASSWORD') + ssl = os.getenv('REDIS_USE_SSL') + + if os.getenv('REDIS_USE_CLUSTER'): + return RedisCluster( + host=host, + port=port, + db=db, + password=password, + ssl=ssl, + decode_responses=True, + ) + return Redis( - host=os.getenv('REDIS_URL'), - port=os.getenv('REDIS_PORT'), - db=os.getenv('REDIS_DB'), - password=os.getenv('REDIS_PASSWORD'), - ssl=os.getenv('REDIS_USE_SSL'), + host=host, + port=port, + db=db, + password=password, + ssl=ssl, decode_responses=True, ) diff --git a/backend/src/appointment/routes/api.py b/backend/src/appointment/routes/api.py index 0e00a7351..8637ba733 100644 --- a/backend/src/appointment/routes/api.py +++ b/backend/src/appointment/routes/api.py @@ -5,7 +5,7 @@ import requests.exceptions import validators -from redis import Redis +from redis import Redis, RedisCluster from requests import HTTPError from sentry_sdk import capture_exception from sqlalchemy.exc import SQLAlchemyError @@ -316,7 +316,7 @@ def read_remote_events( db: Session = Depends(get_db), google_client: GoogleClient = Depends(get_google_client), subscriber: Subscriber = Depends(get_subscriber), - redis_instance: Redis | None = Depends(get_redis), + redis_instance: Redis | RedisCluster | None = Depends(get_redis), ): """endpoint to get events in a given date range from a remote calendar""" db_calendar = repo.calendar.get(db, calendar_id=id) @@ -587,7 +587,8 @@ def send_feedback( background_tasks.add_task( send_support_email, - requestee=subscriber, + requestee_name=subscriber.name, + requestee_email=subscriber.email, topic=form_data.topic, details=form_data.details, ) diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py index 8a2d5a814..05b89c741 100644 --- a/backend/src/appointment/routes/schedule.py +++ b/backend/src/appointment/routes/schedule.py @@ -254,10 +254,10 @@ def request_schedule_availability_slot( db.commit() # Sending confirmation email to owner - background_tasks.add_task(send_confirmation_email, url=url, attendee=attendee, date=date, to=subscriber.email) + background_tasks.add_task(send_confirmation_email, url=url, attendee_name=attendee.name, date=date, to=subscriber.email) # Sending pending email to attendee - background_tasks.add_task(send_pending_email, owner=subscriber, date=attendee_date, to=slot.attendee.email) + background_tasks.add_task(send_pending_email, owner_name=subscriber.name, 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( @@ -321,7 +321,7 @@ def decide_on_schedule_availability_slot( date = slot.start.replace(tzinfo=timezone.utc).astimezone(ZoneInfo(subscriber.timezone)).strftime("%c") date = f"{date}, {slot.duration} minutes" # send rejection information to bookee - background_tasks.add_task(send_rejection_email, owner=subscriber, date=date, to=slot.attendee.email) + background_tasks.add_task(send_rejection_email, owner_name=subscriber.name, date=date, to=slot.attendee.email) if slot.appointment_id: # delete the appointment, this will also delete the slot. diff --git a/backend/src/appointment/tasks/emails.py b/backend/src/appointment/tasks/emails.py index 326119fc9..a313df3d0 100644 --- a/backend/src/appointment/tasks/emails.py +++ b/backend/src/appointment/tasks/emails.py @@ -7,30 +7,31 @@ def send_invite_email(to, attachment): mail.send() -def send_confirmation_email(url, attendee, date, to): +def send_confirmation_email(url, attendee_name, attendee_email, date, to): # send confirmation mail to owner mail = ConfirmationMail( f"{url}/1", f"{url}/0", - attendee, + attendee_name, + attendee_email, date, to=to ) mail.send() -def send_pending_email(owner, date, to): +def send_pending_email(owner_name, date, to): mail = PendingRequestMail( - owner=owner, + owner_name=owner_name, date=date, to=to ) mail.send() -def send_rejection_email(owner, date, to): +def send_rejection_email(owner_name, date, to): mail = RejectionMail( - owner=owner, + owner_name=owner_name, date=date, to=to ) @@ -42,9 +43,10 @@ def send_zoom_meeting_failed_email(to, appointment_title): mail.send() -def send_support_email(requestee, topic, details): +def send_support_email(requestee_name, requestee_email, topic, details): mail = SupportRequestMail( - requestee=requestee, + requestee_name=requestee_name, + requestee_email=requestee_email, topic=topic, details=details, ) diff --git a/backend/src/appointment/templates/email/confirm.jinja2 b/backend/src/appointment/templates/email/confirm.jinja2 index 0dc857772..d078cb7a3 100644 --- a/backend/src/appointment/templates/email/confirm.jinja2 +++ b/backend/src/appointment/templates/email/confirm.jinja2 @@ -1,7 +1,7 @@
- {{ l10n('confirm-mail-html-heading', {'attendee_name': attendee.name, 'attendee_email': attendee.email, 'date': date}) }} + {{ l10n('confirm-mail-html-heading', {'attendee_name': attendee_name, 'attendee_email': attendee_email, 'date': date}) }}
{{ l10n('confirm-mail-html-confirm-text') }}
diff --git a/backend/src/appointment/templates/email/pending.jinja2 b/backend/src/appointment/templates/email/pending.jinja2
index 3ccc8b86b..2a11db83f 100644
--- a/backend/src/appointment/templates/email/pending.jinja2
+++ b/backend/src/appointment/templates/email/pending.jinja2
@@ -1,7 +1,7 @@
- {{ l10n('pending-mail-html-heading', {'owner_name': owner.name, 'date': date}) }} + {{ l10n('pending-mail-html-heading', {'owner_name': owner_name, 'date': date}) }}
{% include 'includes/footer.jinja2' %} diff --git a/backend/src/appointment/templates/email/rejected.jinja2 b/backend/src/appointment/templates/email/rejected.jinja2 index 06e0a87ea..34101c73b 100644 --- a/backend/src/appointment/templates/email/rejected.jinja2 +++ b/backend/src/appointment/templates/email/rejected.jinja2 @@ -1,7 +1,7 @@- {{ l10n('reject-mail-html-heading', {'owner_name': owner.name, 'date': date}) }} + {{ l10n('reject-mail-html-heading', {'owner_name': owner_name, 'date': date}) }}
{% include 'includes/footer.jinja2' %} diff --git a/backend/src/appointment/templates/email/support.jinja2 b/backend/src/appointment/templates/email/support.jinja2 index 50bd9263e..b0cb8697f 100644 --- a/backend/src/appointment/templates/email/support.jinja2 +++ b/backend/src/appointment/templates/email/support.jinja2 @@ -1,7 +1,7 @@- {{ l10n('support-mail-html-heading', {'requestee_name': requestee.name, 'requestee_email': requestee.email}) }} + {{ l10n('support-mail-html-heading', {'requestee_name': requestee_name, 'requestee_email': requestee_email}) }}
{{ l10n('support-mail-html-topic', {'topic': topic}) }}
diff --git a/backend/test/unit/test_mailer.py b/backend/test/unit/test_mailer.py
index edfb3a81c..2ad184c5e 100644
--- a/backend/test/unit/test_mailer.py
+++ b/backend/test/unit/test_mailer.py
@@ -19,7 +19,7 @@ def test_confirm(self, faker, with_l10n):
now = datetime.datetime.now()
attendee = schemas.AttendeeBase(email=faker.email(), name=faker.name(), timezone='Europe/Berlin')
- mailer = ConfirmationMail(confirm_url, deny_url, attendee, now, to=fake_email)
+ mailer = ConfirmationMail(confirm_url, deny_url, attendee.name, attendee.email, now, to=fake_email)
assert mailer.html()
assert mailer.text()
@@ -35,7 +35,7 @@ def test_reject(self, faker, with_l10n, make_pro_subscriber):
now = datetime.datetime.now()
fake_email = 'to@example.org'
- mailer = RejectionMail(owner=subscriber, date=now, to=fake_email)
+ mailer = RejectionMail(owner_name=subscriber.name, date=now, to=fake_email)
assert mailer.html()
assert mailer.text()
diff --git a/frontend/src/elements/CalendarEvent.vue b/frontend/src/elements/CalendarEvent.vue
index 589b84377..8f0ec8309 100644
--- a/frontend/src/elements/CalendarEvent.vue
+++ b/frontend/src/elements/CalendarEvent.vue
@@ -48,17 +48,19 @@
color: !eventData.tentative ? getAccessibleColor(eventData.calendar_color) : null,
}"
>
-