From 2e2d3e37338413ff16852b190f2bc672fab32f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 18 Apr 2024 14:55:16 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A8=20Move=20repository=20function?= =?UTF-8?q?s=20to=20own=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/create_invite_codes.py | 2 +- .../appointment/controller/apis/fxa_client.py | 2 +- .../controller/apis/google_client.py | 2 +- .../controller/apis/zoom_client.py | 2 +- .../src/appointment/controller/calendar.py | 2 +- backend/src/appointment/controller/data.py | 26 +- backend/src/appointment/database/repo.py | 694 ------------------ .../appointment/database/repo/appointment.py | 91 +++ .../src/appointment/database/repo/attendee.py | 36 + .../src/appointment/database/repo/calendar.py | 135 ++++ .../database/repo/external_connection.py | 78 ++ .../src/appointment/database/repo/invite.py | 71 ++ .../src/appointment/database/repo/schedule.py | 65 ++ backend/src/appointment/database/repo/slot.py | 122 +++ .../appointment/database/repo/subscriber.py | 121 +++ backend/src/appointment/dependencies/auth.py | 4 +- .../src/appointment/exceptions/validation.py | 2 +- backend/src/appointment/routes/api.py | 98 +-- backend/src/appointment/routes/auth.py | 16 +- backend/src/appointment/routes/google.py | 10 +- backend/src/appointment/routes/invite.py | 18 +- backend/src/appointment/routes/schedule.py | 62 +- backend/src/appointment/routes/webhooks.py | 4 +- backend/src/appointment/routes/zoom.py | 8 +- backend/test/conftest.py | 2 +- backend/test/factory/appointment_factory.py | 4 +- backend/test/factory/calendar_factory.py | 4 +- .../factory/external_connection_factory.py | 2 +- backend/test/factory/schedule_factory.py | 2 +- backend/test/factory/slot_factory.py | 2 +- backend/test/factory/subscriber_factory.py | 2 +- backend/test/integration/test_auth.py | 2 +- backend/test/integration/test_profile.py | 2 +- backend/test/integration/test_schedule.py | 2 +- backend/test/integration/test_webhooks.py | 18 +- backend/test/unit/test_auth_dependency.py | 2 +- 36 files changed, 870 insertions(+), 845 deletions(-) delete mode 100644 backend/src/appointment/database/repo.py create mode 100644 backend/src/appointment/database/repo/appointment.py create mode 100644 backend/src/appointment/database/repo/attendee.py create mode 100644 backend/src/appointment/database/repo/calendar.py create mode 100644 backend/src/appointment/database/repo/external_connection.py create mode 100644 backend/src/appointment/database/repo/invite.py create mode 100644 backend/src/appointment/database/repo/schedule.py create mode 100644 backend/src/appointment/database/repo/slot.py create mode 100644 backend/src/appointment/database/repo/subscriber.py diff --git a/backend/src/appointment/commands/create_invite_codes.py b/backend/src/appointment/commands/create_invite_codes.py index 38a3dd0d6..275f6f4b0 100644 --- a/backend/src/appointment/commands/create_invite_codes.py +++ b/backend/src/appointment/commands/create_invite_codes.py @@ -10,7 +10,7 @@ def run(n: int): _, session = get_engine_and_session() db = session() - codes = repo.generate_invite_codes(db, n) + codes = repo.invite.generate_codes(db, n) db.close() diff --git a/backend/src/appointment/controller/apis/fxa_client.py b/backend/src/appointment/controller/apis/fxa_client.py index 0230f193c..affb9b33f 100644 --- a/backend/src/appointment/controller/apis/fxa_client.py +++ b/backend/src/appointment/controller/apis/fxa_client.py @@ -135,7 +135,7 @@ def token_saver(self, token): if self.subscriber_id is None: return - repo.update_subscriber_external_connection_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa) + repo.external_connection.update_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa) def get_profile(self): """Retrieve the user's profile information""" diff --git a/backend/src/appointment/controller/apis/google_client.py b/backend/src/appointment/controller/apis/google_client.py index e301866bd..e43073ed5 100644 --- a/backend/src/appointment/controller/apis/google_client.py +++ b/backend/src/appointment/controller/apis/google_client.py @@ -169,7 +169,7 @@ def sync_calendars(self, db, subscriber_id: int, token): # add calendar try: - repo.update_or_create_subscriber_calendar( + repo.calendar.update_or_create( db=db, calendar=cal, calendar_url=calendar.get("id"), diff --git a/backend/src/appointment/controller/apis/zoom_client.py b/backend/src/appointment/controller/apis/zoom_client.py index b2770fc73..aed0d35f0 100644 --- a/backend/src/appointment/controller/apis/zoom_client.py +++ b/backend/src/appointment/controller/apis/zoom_client.py @@ -61,7 +61,7 @@ def token_saver(self, token): return # get_db is a generator function, retrieve the only yield - repo.update_subscriber_external_connection_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.zoom) + repo.external_connection.update_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.zoom) def get_me(self): return self.client.get(f'{self.OAUTH_REQUEST_URL}/users/me').json() diff --git a/backend/src/appointment/controller/calendar.py b/backend/src/appointment/controller/calendar.py index 035fc4d9c..4b981ed64 100644 --- a/backend/src/appointment/controller/calendar.py +++ b/backend/src/appointment/controller/calendar.py @@ -567,7 +567,7 @@ def existing_events_for_schedule( # handle calendar events for calendar in calendars: if calendar.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() diff --git a/backend/src/appointment/controller/data.py b/backend/src/appointment/controller/data.py index ce5852de5..c3ef7b6a8 100644 --- a/backend/src/appointment/controller/data.py +++ b/backend/src/appointment/controller/data.py @@ -37,14 +37,14 @@ def model_to_csv_buffer(models): def download(db, subscriber: Subscriber): """Generate a zip file of csvs that contain a copy of the subscriber's information.""" - attendees = repo.get_attendees_by_subscriber(db, subscriber_id=subscriber.id) - appointments = repo.get_appointments_by_subscriber(db, subscriber_id=subscriber.id) - calendars = repo.get_calendars_by_subscriber(db, subscriber_id=subscriber.id) + attendees = repo.attendee.get_by_subscriber(db, subscriber_id=subscriber.id) + appointments = repo.appointment.get_by_subscriber(db, subscriber_id=subscriber.id) + calendars = repo.calendar.get_by_subscriber(db, subscriber_id=subscriber.id) subscribers = [subscriber] - slots = repo.get_slots_by_subscriber(db, subscriber_id=subscriber.id) + slots = repo.slot.get_by_subscriber(db, subscriber_id=subscriber.id) external_connections = subscriber.external_connections - schedules = repo.get_schedules_by_subscriber(db, subscriber.id) - availability = [repo.get_availability_by_schedule(db, schedule.id) for schedule in schedules] + schedules = repo.schedule.get_by_subscriber(db, subscriber.id) + availability = [repo.schedule.get_availability(db, schedule.id) for schedule in schedules] # Convert models to csv attendee_buffer = model_to_csv_buffer(attendees) @@ -79,21 +79,21 @@ def download(db, subscriber: Subscriber): def delete_account(db, subscriber: Subscriber): # Ok nuke everything (thanks cascade=all,delete) - repo.delete_subscriber(db, subscriber) + repo.subscriber.delete(db, subscriber) # Make sure we actually nuked the subscriber - if repo.get_subscriber(db, subscriber.id) is not None: + if repo.subscriber.get(db, subscriber.id) is not None: raise AccountDeletionSubscriberFail( subscriber.id, "There was a problem deleting your data. This incident has been logged and your data will manually be removed.", ) empty_check = [ - len(repo.get_attendees_by_subscriber(db, subscriber.id)), - len(repo.get_slots_by_subscriber(db, subscriber.id)), - len(repo.get_appointments_by_subscriber(db, subscriber.id)), - len(repo.get_calendars_by_subscriber(db, subscriber.id)), - len(repo.get_schedules_by_subscriber(db, subscriber.id)) + len(repo.attendee.get_by_subscriber(db, subscriber.id)), + len(repo.slot.get_by_subscriber(db, subscriber.id)), + len(repo.appointment.get_by_subscriber(db, subscriber.id)), + len(repo.calendar.get_by_subscriber(db, subscriber.id)), + len(repo.schedule.get_by_subscriber(db, subscriber.id)) ] # Check if we have any left-over subscriber data diff --git a/backend/src/appointment/database/repo.py b/backend/src/appointment/database/repo.py deleted file mode 100644 index 28285f0e1..000000000 --- a/backend/src/appointment/database/repo.py +++ /dev/null @@ -1,694 +0,0 @@ -"""Module: repo - -Repository providing CRUD functions for all database models. -""" -import os -import re -import uuid - -from datetime import timedelta, datetime - -from fastapi import HTTPException -from sqlalchemy.orm import Session -from . import models, schemas -from .schemas import ExternalConnection -from ..controller.auth import sign_url - - -"""ATTENDEES repository functions -""" - - -def get_attendees_by_subscriber(db: Session, subscriber_id: int): - """For use with the data download. Get attendees by subscriber id.""" - # We need to walk through Calendars to attach Appointments, and Appointments to get Slots - slots = ( - db.query(models.Slot) - .join(models.Appointment) - .join(models.Calendar) - .filter(models.Calendar.owner_id == subscriber_id) - .filter(models.Appointment.calendar_id == models.Calendar.id) - .filter(models.Slot.appointment_id == models.Appointment.id) - .all() - ) - - attendee_ids = list(map(lambda slot: slot.attendee_id if slot.attendee_id is not None else None, slots)) - attendee_ids = filter(lambda attendee: attendee is not None, attendee_ids) - return db.query(models.Attendee).filter(models.Attendee.id.in_(attendee_ids)).all() - - -def delete_attendees_by_subscriber(db: Session, subscriber_id: int): - """Delete all attendees by subscriber""" - attendees = get_attendees_by_subscriber(db, subscriber_id) - - for attendee in attendees: - db.delete(attendee) - db.commit() - - return True - - -""" SUBSCRIBERS repository functions -""" - - -def get_subscriber(db: Session, subscriber_id: int) -> models.Subscriber | None: - """retrieve subscriber by id""" - return db.get(models.Subscriber, subscriber_id) - - -def get_subscriber_by_email(db: Session, email: str): - """retrieve subscriber by email""" - return db.query(models.Subscriber).filter(models.Subscriber.email == email).first() - - -def get_subscriber_by_username(db: Session, username: str): - """retrieve subscriber by username""" - return db.query(models.Subscriber).filter(models.Subscriber.username == username).first() - - -def get_subscriber_by_appointment(db: Session, appointment_id: int): - """retrieve appointment by subscriber username and appointment slug (public)""" - if appointment_id: - return ( - db.query(models.Subscriber) - .join(models.Calendar) - .join(models.Appointment) - .filter(models.Appointment.id == appointment_id) - .first() - ) - return None - - -def get_subscriber_by_google_state(db: Session, state: str): - """retrieve subscriber by google state, you'll have to manually check the google_state_expire_at!""" - if state is None: - return None - return db.query(models.Subscriber).filter(models.Subscriber.google_state == state).first() - - -def create_subscriber(db: Session, subscriber: schemas.SubscriberBase): - """create new subscriber""" - db_subscriber = models.Subscriber(**subscriber.dict()) - db.add(db_subscriber) - db.commit() - db.refresh(db_subscriber) - return db_subscriber - - -def update_subscriber(db: Session, data: schemas.SubscriberIn, subscriber_id: int): - """update all subscriber attributes, they can edit themselves""" - db_subscriber = get_subscriber(db, subscriber_id) - for key, value in data: - if value is not None: - setattr(db_subscriber, key, value) - db.commit() - db.refresh(db_subscriber) - return db_subscriber - - -def delete_subscriber(db: Session, subscriber: models.Subscriber): - """Delete a subscriber by subscriber id""" - db.delete(subscriber) - db.commit() - return True - - -def get_connections_limit(db: Session, subscriber_id: int): - """return the number of allowed connections for given subscriber or -1 for unlimited connections""" - # db_subscriber = get_subscriber(db, subscriber_id) - # mapping = { - # models.SubscriberLevel.basic: int(os.getenv("TIER_BASIC_CALENDAR_LIMIT")), - # models.SubscriberLevel.plus: int(os.getenv("TIER_PLUS_CALENDAR_LIMIT")), - # models.SubscriberLevel.pro: int(os.getenv("TIER_PRO_CALENDAR_LIMIT")), - # models.SubscriberLevel.admin: -1, - # } - # return mapping[db_subscriber.level] - - # No limit right now! - return -1 - - -def verify_subscriber_link(db: Session, url: str): - """Check if a given url is a valid signed subscriber profile link - Return subscriber if valid. - """ - # Look for a followed by an optional signature that ends the string - pattern = r"[\/]([\w\d\-_\.\@]+)[\/]?([\w\d]*)[\/]?$" - match = re.findall(pattern, url) - - if match is None or len(match) == 0: - return False - - # Flatten - match = match[0] - clean_url = url - - username = match[0] - signature = None - if len(match) > 1: - signature = match[1] - clean_url = clean_url.replace(signature, "") - - subscriber = get_subscriber_by_username(db, username) - if not subscriber: - return False - - clean_url_with_short_link = clean_url + f"{subscriber.short_link_hash}" - signed_signature = sign_url(clean_url_with_short_link) - - # Verify the signature matches the incoming one - if signed_signature == signature: - return subscriber - return False - - -""" CALENDAR repository functions -""" - - -def calendar_exists(db: Session, calendar_id: int): - """true if calendar of given id exists""" - return True if db.get(models.Calendar, calendar_id) is not None else False - - -def calendar_is_owned(db: Session, calendar_id: int, subscriber_id: int): - """check if calendar belongs to subscriber""" - return ( - db.query(models.Calendar) - .filter(models.Calendar.id == calendar_id, models.Calendar.owner_id == subscriber_id) - .first() - is not None - ) - - -def get_calendar(db: Session, calendar_id: int): - """retrieve calendar by id""" - return db.get(models.Calendar, calendar_id) - - -def calendar_is_connected(db: Session, calendar_id: int): - """true if calendar of given id exists""" - return get_calendar(db, calendar_id).connected - - -def get_calendar_by_url(db: Session, url: str): - """retrieve calendar by calendar url""" - return db.query(models.Calendar).filter(models.Calendar.url == url).first() - - -def get_calendars_by_subscriber(db: Session, subscriber_id: int, include_unconnected: bool = True): - """retrieve list of calendars by owner id""" - query = db.query(models.Calendar).filter(models.Calendar.owner_id == subscriber_id) - - if not include_unconnected: - query = query.filter(models.Calendar.connected == 1) - - return query.all() - - -def create_subscriber_calendar(db: Session, calendar: schemas.CalendarConnection, subscriber_id: int): - """create new calendar for owner, if not already existing""" - db_calendar = models.Calendar(**calendar.dict(), owner_id=subscriber_id) - subscriber_calendars = get_calendars_by_subscriber(db, subscriber_id) - subscriber_calendar_urls = [c.url for c in subscriber_calendars] - # check if subscriber already holds this calendar by url - if db_calendar.url in subscriber_calendar_urls: - raise HTTPException(status_code=403, detail="Calendar already exists") - # add new calendar - db.add(db_calendar) - db.commit() - db.refresh(db_calendar) - return db_calendar - - -def update_subscriber_calendar(db: Session, calendar: schemas.CalendarConnection, calendar_id: int): - """update existing calendar by id""" - db_calendar = get_calendar(db, calendar_id) - - # list of all attributes that must never be updated - # # because they have dedicated update functions for security reasons - ignore = ["connected", "connected_at"] - # list of all attributes that will keep their current value if None is passed - keep_if_none = ["password"] - - for key, value in calendar: - # skip update, if attribute is ignored or current value should be kept if given value is falsey/empty - if key in ignore or (key in keep_if_none and (not value or len(str(value)) == 0)): - continue - - setattr(db_calendar, key, value) - - db.commit() - db.refresh(db_calendar) - return db_calendar - - -def update_subscriber_calendar_connection(db: Session, is_connected: bool, calendar_id: int): - """Updates the connected status of a calendar""" - db_calendar = get_calendar(db, calendar_id) - # check subscription limitation on connecting - if is_connected: - subscriber_calendars = get_calendars_by_subscriber(db, db_calendar.owner_id) - connected_calendars = [calendar for calendar in subscriber_calendars if calendar.connected] - limit = get_connections_limit(db=db, subscriber_id=db_calendar.owner_id) - if limit > 0 and len(connected_calendars) >= limit: - raise HTTPException( - status_code=403, detail="Allowed number of connected calendars has been reached for this subscription" - ) - if not db_calendar.connected: - db_calendar.connected_at = datetime.now() - elif db_calendar.connected and is_connected is False: - db_calendar.connected_at = None - db_calendar.connected = is_connected - db.commit() - db.refresh(db_calendar) - return db_calendar - - -def update_or_create_subscriber_calendar( - db: Session, calendar: schemas.CalendarConnection, calendar_url: str, subscriber_id: int -): - """update or create a subscriber calendar""" - subscriber_calendar = get_calendar_by_url(db, calendar_url) - - if subscriber_calendar is None: - return create_subscriber_calendar(db, calendar, subscriber_id) - - return update_subscriber_calendar(db, calendar, subscriber_calendar.id) - - -def delete_subscriber_calendar(db: Session, calendar_id: int): - """remove existing calendar by id""" - db_calendar = get_calendar(db, calendar_id) - db.delete(db_calendar) - db.commit() - return db_calendar - - -def delete_subscriber_calendar_by_subscriber_id(db: Session, subscriber_id: int): - """Delete all calendars by subscriber""" - calendars = get_calendars_by_subscriber(db, subscriber_id=subscriber_id) - for calendar in calendars: - delete_subscriber_calendar(db, calendar_id=calendar.id) - return True - - -""" APPOINTMENT repository functions -""" - - -def create_calendar_appointment(db: Session, appointment: schemas.AppointmentFull, slots: list[schemas.SlotBase] = []): - """create new appointment with slots for calendar""" - db_appointment = models.Appointment(**appointment.dict()) - db.add(db_appointment) - db.commit() - db.refresh(db_appointment) - if len(slots) > 0: - add_appointment_slots(db, slots, db_appointment.id) - return db_appointment - - -def get_appointment(db: Session, appointment_id: int) -> models.Appointment|None: - """retrieve appointment by id (private)""" - if appointment_id: - return db.get(models.Appointment, appointment_id) - return None - - -def get_public_appointment(db: Session, slug: str): - """retrieve appointment by appointment slug (public)""" - if slug: - return db.query(models.Appointment).filter(models.Appointment.slug == slug).first() - return None - - -def get_appointments_by_subscriber(db: Session, subscriber_id: int): - """retrieve list of appointments by owner id""" - return db.query(models.Appointment).join(models.Calendar).filter(models.Calendar.owner_id == subscriber_id).all() - - -def appointment_is_owned(db: Session, appointment_id: int, subscriber_id: int): - """check if appointment belongs to subscriber""" - db_appointment = get_appointment(db, appointment_id) - return calendar_is_owned(db, db_appointment.calendar_id, subscriber_id) - - -def appointment_has_slot(db: Session, appointment_id: int, slot_id: int): - """check if appointment belongs to subscriber""" - db_slot = get_slot(db, slot_id) - return db_slot and db_slot.appointment_id == appointment_id - - -def update_calendar_appointment( - db: Session, - appointment: schemas.AppointmentFull, - slots: list[schemas.SlotBase], - appointment_id: int, -): - """update existing appointment by id""" - db_appointment = get_appointment(db, appointment_id) - for key, value in appointment: - setattr(db_appointment, key, value) - db.commit() - db.refresh(db_appointment) - delete_appointment_slots(db, appointment_id) - add_appointment_slots(db, slots, appointment_id) - return db_appointment - - -def delete_calendar_appointment(db: Session, appointment_id: int): - """remove existing appointment by id""" - db_appointment = get_appointment(db, appointment_id) - db.delete(db_appointment) - db.commit() - return db_appointment - - -def delete_calendar_appointments_by_subscriber_id(db: Session, subscriber_id: int): - """Delete all appointments by subscriber""" - appointments = get_appointments_by_subscriber(db, subscriber_id=subscriber_id) - for appointment in appointments: - delete_calendar_appointment(db, appointment_id=appointment.id) - return True - - -def update_appointment_status(db: Session, appointment_id: int, status: models.AppointmentStatus): - appointment = get_appointment(db, appointment_id) - if not appointment: - return False - - appointment.status = status - db.commit() - - -""" SLOT repository functions -""" - - -def get_slot(db: Session, slot_id: int) -> models.Slot | None: - """retrieve slot by id""" - if slot_id: - return db.get(models.Slot, slot_id) - return None - - -def get_slots_by_subscriber(db: Session, subscriber_id: int): - """retrieve slot by subscriber id""" - - # We need to walk through Calendars to attach Appointments, and Appointments to get Slots - return ( - db.query(models.Slot) - .join(models.Appointment) - .join(models.Calendar) - .filter(models.Calendar.owner_id == subscriber_id) - .filter(models.Appointment.calendar_id == models.Calendar.id) - .filter(models.Slot.appointment_id == models.Appointment.id) - .all() - ) - - -def add_appointment_slots(db: Session, slots: list[schemas.SlotBase], appointment_id: int): - """create new slots for appointment of given id""" - for slot in slots: - db_slot = models.Slot(**slot.dict()) - db_slot.appointment_id = appointment_id - db.add(db_slot) - db.commit() - return slots - - -def add_schedule_slot(db: Session, slot: schemas.SlotBase, schedule_id: int): - """create new slot for schedule of given id""" - db_slot = models.Slot(**slot.dict()) - db_slot.schedule_id = schedule_id - db.add(db_slot) - db.commit() - db.refresh(db_slot) - return db_slot - - -def schedule_slot_exists(db: Session, slot: schemas.SlotBase, schedule_id: int): - """check if given slot already exists for schedule of given id""" - db_slot = ( - db.query(models.Slot) - .filter(models.Slot.schedule_id == schedule_id) - .filter(models.Slot.start == slot.start) - .filter(models.Slot.duration == slot.duration) - .filter(models.Slot.booking_status != models.BookingStatus.none) - .first() - ) - return db_slot is not None - - -def book_slot(db: Session, slot_id: int) -> models.Slot | None: - """update booking status for slot of given id""" - db_slot = get_slot(db, slot_id) - db_slot.booking_status = models.BookingStatus.booked - db.commit() - db.refresh(db_slot) - return db_slot - - -def delete_appointment_slots(db: Session, appointment_id: int): - """delete all slots for appointment of given id""" - return db.query(models.Slot).filter(models.Slot.appointment_id == appointment_id).delete() - - -def delete_appointment_slots_by_subscriber_id(db: Session, subscriber_id: int): - """Delete all slots by subscriber""" - slots = get_slots_by_subscriber(db, subscriber_id) - - for slot in slots: - db.delete(slot) - db.commit() - - return True - - -def update_slot(db: Session, slot_id: int, attendee: schemas.Attendee): - """update existing slot by id and create corresponding attendee""" - # create attendee - db_attendee = models.Attendee(**attendee.dict()) - db.add(db_attendee) - db.commit() - db.refresh(db_attendee) - # update slot - db_slot = get_slot(db, slot_id) - # TODO: additionally handle subscriber_id here for already logged in users - setattr(db_slot, "attendee_id", db_attendee.id) - db.commit() - return db_attendee - - -def delete_slot(db: Session, slot_id: int): - """remove existing slot by id""" - db_slot = get_slot(db, slot_id) - db.delete(db_slot) - db.commit() - return db_slot - - -def slot_is_available(db: Session, slot_id: int): - """check if slot is still available for booking""" - slot = get_slot(db, slot_id) - if slot.schedule: - return slot and slot.booking_status == models.BookingStatus.requested - return False - - -"""SCHEDULES repository functions -""" - - -def create_calendar_schedule(db: Session, schedule: schemas.ScheduleBase): - """create new schedule with slots for calendar""" - db_schedule = models.Schedule(**schedule.dict()) - db.add(db_schedule) - db.commit() - db.refresh(db_schedule) - return db_schedule - - -def get_schedules_by_subscriber(db: Session, subscriber_id: int): - """Get schedules by subscriber id""" - return ( - db.query(models.Schedule) - .join(models.Calendar, models.Schedule.calendar_id == models.Calendar.id) - .filter(models.Calendar.owner_id == subscriber_id) - .all() - ) - - -def get_schedule(db: Session, schedule_id: int): - """retrieve schedule by id""" - if schedule_id: - return db.get(models.Schedule, schedule_id) - return None - - -def schedule_is_owned(db: Session, schedule_id: int, subscriber_id: int): - """check if the given schedule belongs to subscriber""" - schedules = get_schedules_by_subscriber(db, subscriber_id) - return any(s.id == schedule_id for s in schedules) - - -def schedule_exists(db: Session, schedule_id: int): - """true if schedule of given id exists""" - return True if get_schedule(db, schedule_id) is not None else False - - -def update_calendar_schedule(db: Session, schedule: schemas.ScheduleBase, schedule_id: int): - """update existing schedule by id""" - db_schedule = get_schedule(db, schedule_id) - for key, value in schedule: - setattr(db_schedule, key, value) - db.commit() - db.refresh(db_schedule) - return db_schedule - - -def get_availability_by_schedule(db: Session, schedule_id: int): - """retrieve availability by schedule id""" - return db.query(models.Availability).filter(models.Availability.schedule_id == schedule_id).all() - - -def schedule_has_slot(db: Session, schedule_id: int, slot_id: int): - """check if slot belongs to schedule""" - db_slot = get_slot(db, slot_id) - return db_slot and db_slot.schedule_id == schedule_id - - -"""INVITES repository functions -""" - - -def get_invite_by_code(db: Session, code: str): - """retrieve invite by code""" - return db.query(models.Invite).filter(models.Invite.code == code).first() - - -def generate_invite_codes(db: Session, n: int): - """generate n invite codes and return the list of created invite objects""" - codes = [str(uuid.uuid4()) for _ in range(n)] - db_invites = [] - for code in codes: - invite = schemas.Invite(code=code) - db_invite = models.Invite(**invite.dict()) - db.add(db_invite) - db.commit() - db_invites.append(db_invite) - return db_invites - - -def invite_code_exists(db: Session, code: str): - """true if invite code exists""" - return True if get_invite_by_code(db, code) is not None else False - - -def invite_code_is_used(db: Session, code: str): - """true if invite code is assigned to a user""" - db_invite = get_invite_by_code(db, code) - return db_invite.is_used - - -def invite_code_is_revoked(db: Session, code: str): - """true if invite code is revoked""" - db_invite = get_invite_by_code(db, code) - return db_invite.is_revoked - - -def invite_code_is_available(db: Session, code: str): - """true if invite code exists and can still be used""" - db_invite = get_invite_by_code(db, code) - return db_invite and db_invite.is_available - - -def use_invite_code(db: Session, code: str, subscriber_id: int): - """assign given subscriber to an invite""" - db_invite = get_invite_by_code(db, code) - if db_invite and db_invite.is_available: - db_invite.subscriber_id = subscriber_id - db.commit() - db.refresh(db_invite) - return True - else: - return False - - -def revoke_invite_code(db: Session, code: str): - """set existing invite code status to revoked""" - db_invite = get_invite_by_code(db, code) - db_invite.status = models.InviteStatus.revoked - db.commit() - db.refresh(db_invite) - return True - - -"""External Connections repository functions -""" - - -def create_subscriber_external_connection(db: Session, external_connection: ExternalConnection): - db_external_connection = models.ExternalConnections( - **external_connection.dict() - ) - db.add(db_external_connection) - db.commit() - db.refresh(db_external_connection) - return db_external_connection - - -def update_subscriber_external_connection_token(db: Session, token: str, subscriber_id: int, type: models.ExternalConnectionType, type_id: str | None = None): - db_results = get_external_connections_by_type(db, subscriber_id, type, type_id) - if db_results is None or len(db_results) == 0: - return None - - db_external_connection = db_results[0] - db_external_connection.token = token - db.commit() - db.refresh(db_external_connection) - return db_external_connection - - -def delete_external_connections_by_type_id(db: Session, subscriber_id: int, type: models.ExternalConnectionType, type_id: str): - connections = get_external_connections_by_type(db, subscriber_id, type, type_id) - - # There should be one by type id, but just in case.. - for connection in connections: - db.delete(connection) - db.commit() - - return True - - -def get_external_connections_by_type(db: Session, subscriber_id: int, type: models.ExternalConnectionType, type_id: str | None = None) -> list[models.ExternalConnections] | None: - """Return a subscribers external connections by type, and optionally type id""" - query = ( - db.query(models.ExternalConnections) - .filter(models.ExternalConnections.owner_id == subscriber_id) - .filter(models.ExternalConnections.type == type) - ) - - if type_id is not None: - query = query.filter(models.ExternalConnections.type_id == type_id) - - result = query.all() - - return result - - -def get_subscriber_by_fxa_uid(db: Session, type_id: str): - """Return a subscriber from a fxa profile uid""" - query = ( - db.query(models.ExternalConnections) - .filter(models.ExternalConnections.type == models.ExternalConnectionType.fxa) - .filter(models.ExternalConnections.type_id == type_id) - ) - - result = query.first() - - if result is not None: - return result.owner - - return None diff --git a/backend/src/appointment/database/repo/appointment.py b/backend/src/appointment/database/repo/appointment.py new file mode 100644 index 000000000..1ff231bc0 --- /dev/null +++ b/backend/src/appointment/database/repo/appointment.py @@ -0,0 +1,91 @@ +"""Module: repo.appointment + +Repository providing CRUD functions for appointment database models. +""" + +from sqlalchemy.orm import Session +from .. import models, schemas, repo + + +def create(db: Session, appointment: schemas.AppointmentFull, slots: list[schemas.SlotBase] = []): + """create new appointment with slots for calendar""" + db_appointment = models.Appointment(**appointment.dict()) + db.add(db_appointment) + db.commit() + db.refresh(db_appointment) + if len(slots) > 0: + repo.slot.add_for_appointment(db, slots, db_appointment.id) + return db_appointment + + +def get(db: Session, appointment_id: int) -> models.Appointment|None: + """retrieve appointment by id (private)""" + if appointment_id: + return db.get(models.Appointment, appointment_id) + return None + + +def get_public(db: Session, slug: str): + """retrieve appointment by appointment slug (public)""" + if slug: + return db.query(models.Appointment).filter(models.Appointment.slug == slug).first() + return None + + +def get_by_subscriber(db: Session, subscriber_id: int): + """retrieve list of appointments by owner id""" + return db.query(models.Appointment).join(models.Calendar).filter(models.Calendar.owner_id == subscriber_id).all() + + +def is_owned(db: Session, appointment_id: int, subscriber_id: int): + """check if appointment belongs to subscriber""" + db_appointment = get(db, appointment_id) + return repo.calendar.is_owned(db, db_appointment.calendar_id, subscriber_id) + + +def has_slot(db: Session, appointment_id: int, slot_id: int): + """check if appointment belongs to subscriber""" + db_slot = repo.slot.get(db, slot_id) + return db_slot and db_slot.appointment_id == appointment_id + + +def update( + db: Session, + appointment: schemas.AppointmentFull, + slots: list[schemas.SlotBase], + appointment_id: int, +): + """update existing appointment by id""" + db_appointment = get(db, appointment_id) + for key, value in appointment: + setattr(db_appointment, key, value) + db.commit() + db.refresh(db_appointment) + repo.slot.delete(db, appointment_id) + repo.slot.add_for_appointment(db, slots, appointment_id) + return db_appointment + + +def delete(db: Session, appointment_id: int): + """remove existing appointment by id""" + db_appointment = get(db, appointment_id) + db.delete(db_appointment) + db.commit() + return db_appointment + + +def delete_by_subscriber(db: Session, subscriber_id: int): + """Delete all appointments by subscriber""" + appointments = get_by_subscriber(db, subscriber_id=subscriber_id) + for appointment in appointments: + delete(db, appointment_id=appointment.id) + return True + + +def update_status(db: Session, appointment_id: int, status: models.AppointmentStatus): + appointment = get(db, appointment_id) + if not appointment: + return False + + appointment.status = status + db.commit() diff --git a/backend/src/appointment/database/repo/attendee.py b/backend/src/appointment/database/repo/attendee.py new file mode 100644 index 000000000..82e8ae017 --- /dev/null +++ b/backend/src/appointment/database/repo/attendee.py @@ -0,0 +1,36 @@ +"""Module: repo.attendee + +Repository providing CRUD functions for attendee database models. +""" + +from sqlalchemy.orm import Session +from .. import models + + +def get_by_subscriber(db: Session, subscriber_id: int): + """For use with the data download. Get attendees by subscriber id.""" + # We need to walk through Calendars to attach Appointments, and Appointments to get Slots + slots = ( + db.query(models.Slot) + .join(models.Appointment) + .join(models.Calendar) + .filter(models.Calendar.owner_id == subscriber_id) + .filter(models.Appointment.calendar_id == models.Calendar.id) + .filter(models.Slot.appointment_id == models.Appointment.id) + .all() + ) + + attendee_ids = list(map(lambda slot: slot.attendee_id if slot.attendee_id is not None else None, slots)) + attendee_ids = filter(lambda attendee: attendee is not None, attendee_ids) + return db.query(models.Attendee).filter(models.Attendee.id.in_(attendee_ids)).all() + + +def delete_by_subscriber(db: Session, subscriber_id: int): + """Delete all attendees by subscriber""" + attendees = get_by_subscriber(db, subscriber_id) + + for attendee in attendees: + db.delete(attendee) + db.commit() + + return True diff --git a/backend/src/appointment/database/repo/calendar.py b/backend/src/appointment/database/repo/calendar.py new file mode 100644 index 000000000..7ead589e5 --- /dev/null +++ b/backend/src/appointment/database/repo/calendar.py @@ -0,0 +1,135 @@ +"""Module: repo.calendar + +Repository providing CRUD functions for calendar database models. +""" + +from datetime import datetime + +from fastapi import HTTPException +from sqlalchemy.orm import Session +from .. import models, schemas, repo + + +def exists(db: Session, calendar_id: int): + """true if calendar of given id exists""" + return True if db.get(models.Calendar, calendar_id) is not None else False + + +def is_owned(db: Session, calendar_id: int, subscriber_id: int): + """check if calendar belongs to subscriber""" + return ( + db.query(models.Calendar) + .filter(models.Calendar.id == calendar_id, models.Calendar.owner_id == subscriber_id) + .first() + is not None + ) + + +def get(db: Session, calendar_id: int): + """retrieve calendar by id""" + return db.get(models.Calendar, calendar_id) + + +def is_connected(db: Session, calendar_id: int): + """true if calendar of given id exists""" + return get(db, calendar_id).connected + + +def get_by_url(db: Session, url: str): + """retrieve calendar by calendar url""" + return db.query(models.Calendar).filter(models.Calendar.url == url).first() + + +def get_by_subscriber(db: Session, subscriber_id: int, include_unconnected: bool = True): + """retrieve list of calendars by owner id""" + query = db.query(models.Calendar).filter(models.Calendar.owner_id == subscriber_id) + + if not include_unconnected: + query = query.filter(models.Calendar.connected == 1) + + return query.all() + + +def create(db: Session, calendar: schemas.CalendarConnection, subscriber_id: int): + """create new calendar for owner, if not already existing""" + db_calendar = models.Calendar(**calendar.dict(), owner_id=subscriber_id) + subscriber_calendars = get_by_subscriber(db, subscriber_id) + subscriber_calendar_urls = [c.url for c in subscriber_calendars] + # check if subscriber already holds this calendar by url + if db_calendar.url in subscriber_calendar_urls: + raise HTTPException(status_code=403, detail="Calendar already exists") + # add new calendar + db.add(db_calendar) + db.commit() + db.refresh(db_calendar) + return db_calendar + + +def update(db: Session, calendar: schemas.CalendarConnection, calendar_id: int): + """update existing calendar by id""" + db_calendar = get(db, calendar_id) + + # list of all attributes that must never be updated + # # because they have dedicated update functions for security reasons + ignore = ["connected", "connected_at"] + # list of all attributes that will keep their current value if None is passed + keep_if_none = ["password"] + + for key, value in calendar: + # skip update, if attribute is ignored or current value should be kept if given value is falsey/empty + if key in ignore or (key in keep_if_none and (not value or len(str(value)) == 0)): + continue + + setattr(db_calendar, key, value) + + db.commit() + db.refresh(db_calendar) + return db_calendar + + +def update_connection(db: Session, is_connected: bool, calendar_id: int): + """Updates the connected status of a calendar""" + db_calendar = get(db, calendar_id) + # check subscription limitation on connecting + if is_connected: + subscriber_calendars = get_by_subscriber(db, db_calendar.owner_id) + connected_calendars = [calendar for calendar in subscriber_calendars if calendar.connected] + limit = repo.subscriber.get_connections_limit(db=db, subscriber_id=db_calendar.owner_id) + if limit > 0 and len(connected_calendars) >= limit: + raise HTTPException( + status_code=403, detail="Allowed number of connected calendars has been reached for this subscription" + ) + if not db_calendar.connected: + db_calendar.connected_at = datetime.now() + elif db_calendar.connected and is_connected is False: + db_calendar.connected_at = None + db_calendar.connected = is_connected + db.commit() + db.refresh(db_calendar) + return db_calendar + + +def update_or_create(db: Session, calendar: schemas.CalendarConnection, calendar_url: str, subscriber_id: int): + """update or create a subscriber calendar""" + subscriber_calendar = get_by_url(db, calendar_url) + + if subscriber_calendar is None: + return create(db, calendar, subscriber_id) + + return update(db, calendar, subscriber_calendar.id) + + +def delete(db: Session, calendar_id: int): + """remove existing calendar by id""" + db_calendar = get(db, calendar_id) + db.delete(db_calendar) + db.commit() + return db_calendar + + +def delete_by_subscriber(db: Session, subscriber_id: int): + """Delete all calendars by subscriber""" + calendars = get_by_subscriber(db, subscriber_id=subscriber_id) + for calendar in calendars: + delete(db, calendar_id=calendar.id) + return True diff --git a/backend/src/appointment/database/repo/external_connection.py b/backend/src/appointment/database/repo/external_connection.py new file mode 100644 index 000000000..27e0c99c6 --- /dev/null +++ b/backend/src/appointment/database/repo/external_connection.py @@ -0,0 +1,78 @@ +"""Module: repo.external_connection + +Repository providing CRUD functions for external_connection database models. +""" + + +from sqlalchemy.orm import Session +from .. import models, repo +from ..schemas import ExternalConnection + + +"""External Connections repository functions +""" + + +def create(db: Session, external_connection: ExternalConnection): + db_external_connection = models.ExternalConnections( + **external_connection.dict() + ) + db.add(db_external_connection) + db.commit() + db.refresh(db_external_connection) + return db_external_connection + + +def update_token(db: Session, token: str, subscriber_id: int, type: models.ExternalConnectionType, type_id: str | None = None): + db_results = get_by_type(db, subscriber_id, type, type_id) + if db_results is None or len(db_results) == 0: + return None + + db_external_connection = db_results[0] + db_external_connection.token = token + db.commit() + db.refresh(db_external_connection) + return db_external_connection + + +def delete_by_type(db: Session, subscriber_id: int, type: models.ExternalConnectionType, type_id: str): + connections = get_by_type(db, subscriber_id, type, type_id) + + # There should be one by type id, but just in case.. + for connection in connections: + db.delete(connection) + db.commit() + + return True + + +def get_by_type(db: Session, subscriber_id: int, type: models.ExternalConnectionType, type_id: str | None = None) -> list[models.ExternalConnections] | None: + """Return a subscribers external connections by type, and optionally type id""" + query = ( + db.query(models.ExternalConnections) + .filter(models.ExternalConnections.owner_id == subscriber_id) + .filter(models.ExternalConnections.type == type) + ) + + if type_id is not None: + query = query.filter(models.ExternalConnections.type_id == type_id) + + result = query.all() + + return result + + +def get_subscriber_by_fxa_uid(db: Session, type_id: str): + """Return a subscriber from a fxa profile uid""" + query = ( + db.query(models.ExternalConnections) + .filter(models.ExternalConnections.type == models.ExternalConnectionType.fxa) + .filter(models.ExternalConnections.type_id == type_id) + ) + + result = query.first() + + if result is not None: + return result.owner + + return None diff --git a/backend/src/appointment/database/repo/invite.py b/backend/src/appointment/database/repo/invite.py new file mode 100644 index 000000000..baa0edd00 --- /dev/null +++ b/backend/src/appointment/database/repo/invite.py @@ -0,0 +1,71 @@ +"""Module: repo.invite + +Repository providing CRUD functions for invite database models. +""" + +import uuid + +from sqlalchemy.orm import Session +from .. import models, schemas + + +def get_by_code(db: Session, code: str): + """retrieve invite by code""" + return db.query(models.Invite).filter(models.Invite.code == code).first() + + +def generate_codes(db: Session, n: int): + """generate n invite codes and return the list of created invite objects""" + codes = [str(uuid.uuid4()) for _ in range(n)] + db_invites = [] + for code in codes: + invite = schemas.Invite(code=code) + db_invite = models.Invite(**invite.dict()) + db.add(db_invite) + db.commit() + db_invites.append(db_invite) + return db_invites + + +def code_exists(db: Session, code: str): + """true if invite code exists""" + return True if get_by_code(db, code) is not None else False + + +def code_is_used(db: Session, code: str): + """true if invite code is assigned to a user""" + db_invite = get_by_code(db, code) + return db_invite.is_used + + +def code_is_revoked(db: Session, code: str): + """true if invite code is revoked""" + db_invite = get_by_code(db, code) + return db_invite.is_revoked + + +def code_is_available(db: Session, code: str): + """true if invite code exists and can still be used""" + db_invite = get_by_code(db, code) + return db_invite and db_invite.is_available + + +def use_code(db: Session, code: str, subscriber_id: int): + """assign given subscriber to an invite""" + db_invite = get_by_code(db, code) + if db_invite and db_invite.is_available: + db_invite.subscriber_id = subscriber_id + db.commit() + db.refresh(db_invite) + return True + else: + return False + + +def revoke_code(db: Session, code: str): + """set existing invite code status to revoked""" + db_invite = get_by_code(db, code) + db_invite.status = models.InviteStatus.revoked + db.commit() + db.refresh(db_invite) + return True diff --git a/backend/src/appointment/database/repo/schedule.py b/backend/src/appointment/database/repo/schedule.py new file mode 100644 index 000000000..cb98c749b --- /dev/null +++ b/backend/src/appointment/database/repo/schedule.py @@ -0,0 +1,65 @@ +"""Module: repo.schedule + +Repository providing CRUD functions for schedule database models. +""" + +from sqlalchemy.orm import Session +from .. import models, schemas, repo + + +def create(db: Session, schedule: schemas.ScheduleBase): + """create a new schedule with slots for calendar""" + db_schedule = models.Schedule(**schedule.dict()) + db.add(db_schedule) + db.commit() + db.refresh(db_schedule) + return db_schedule + + +def get_by_subscriber(db: Session, subscriber_id: int): + """Get schedules by subscriber id""" + return ( + db.query(models.Schedule) + .join(models.Calendar, models.Schedule.calendar_id == models.Calendar.id) + .filter(models.Calendar.owner_id == subscriber_id) + .all() + ) + + +def get(db: Session, schedule_id: int): + """retrieve schedule by id""" + if schedule_id: + return db.get(models.Schedule, schedule_id) + return None + + +def is_owned(db: Session, schedule_id: int, subscriber_id: int): + """check if the given schedule belongs to subscriber""" + schedules = get_by_subscriber(db, subscriber_id) + return any(s.id == schedule_id for s in schedules) + + +def exists(db: Session, schedule_id: int): + """true if schedule of given id exists""" + return True if get(db, schedule_id) is not None else False + + +def update(db: Session, schedule: schemas.ScheduleBase, schedule_id: int): + """update existing schedule by id""" + db_schedule = get(db, schedule_id) + for key, value in schedule: + setattr(db_schedule, key, value) + db.commit() + db.refresh(db_schedule) + return db_schedule + + +def get_availability(db: Session, schedule_id: int): + """retrieve availability by schedule id""" + return db.query(models.Availability).filter(models.Availability.schedule_id == schedule_id).all() + + +def has_slot(db: Session, schedule_id: int, slot_id: int): + """check if slot belongs to schedule""" + db_slot = repo.slot.get(db, slot_id) + return db_slot and db_slot.schedule_id == schedule_id diff --git a/backend/src/appointment/database/repo/slot.py b/backend/src/appointment/database/repo/slot.py new file mode 100644 index 000000000..1e5af4014 --- /dev/null +++ b/backend/src/appointment/database/repo/slot.py @@ -0,0 +1,122 @@ +"""Module: repo.slot + +Repository providing CRUD functions for slot database models. +""" + +from sqlalchemy.orm import Session +from .. import models, schemas, repo + + +""" SLOT repository functions +""" + + +def get(db: Session, slot_id: int) -> models.Slot | None: + """retrieve slot by id""" + if slot_id: + return db.get(models.Slot, slot_id) + return None + + +def get_by_subscriber(db: Session, subscriber_id: int): + """retrieve list of slots by subscriber id""" + + # We need to walk through Calendars to attach Appointments, and Appointments to get Slots + return ( + db.query(models.Slot) + .join(models.Appointment) + .join(models.Calendar) + .filter(models.Calendar.owner_id == subscriber_id) + .filter(models.Appointment.calendar_id == models.Calendar.id) + .filter(models.Slot.appointment_id == models.Appointment.id) + .all() + ) + + +def add_for_appointment(db: Session, slots: list[schemas.SlotBase], appointment_id: int): + """create new slots for appointment of given id""" + for slot in slots: + db_slot = models.Slot(**slot.dict()) + db_slot.appointment_id = appointment_id + db.add(db_slot) + db.commit() + return slots + + +def add_for_schedule(db: Session, slot: schemas.SlotBase, schedule_id: int): + """create new slot for schedule of given id""" + db_slot = models.Slot(**slot.dict()) + db_slot.schedule_id = schedule_id + db.add(db_slot) + db.commit() + db.refresh(db_slot) + return db_slot + + +def exists_on_schedule(db: Session, slot: schemas.SlotBase, schedule_id: int): + """check if given slot already exists for schedule of given id""" + db_slot = ( + db.query(models.Slot) + .filter(models.Slot.schedule_id == schedule_id) + .filter(models.Slot.start == slot.start) + .filter(models.Slot.duration == slot.duration) + .filter(models.Slot.booking_status != models.BookingStatus.none) + .first() + ) + return db_slot is not None + + +def book(db: Session, slot_id: int) -> models.Slot | None: + """update booking status for slot of given id""" + db_slot = get(db, slot_id) + db_slot.booking_status = models.BookingStatus.booked + db.commit() + db.refresh(db_slot) + return db_slot + + +def delete_all_for_appointment(db: Session, appointment_id: int): + """delete all slots for appointment of given id""" + return db.query(models.Slot).filter(models.Slot.appointment_id == appointment_id).delete() + + +def delete_all_for_subscriber(db: Session, subscriber_id: int): + """Delete all slots by subscriber""" + slots = get_by_subscriber(db, subscriber_id) + + for slot in slots: + db.delete(slot) + db.commit() + + return True + + +def update(db: Session, slot_id: int, attendee: schemas.Attendee): + """update existing slot by id and create corresponding attendee""" + # create attendee + db_attendee = models.Attendee(**attendee.dict()) + db.add(db_attendee) + db.commit() + db.refresh(db_attendee) + # update slot + db_slot = get(db, slot_id) + # TODO: additionally handle subscriber_id here for already logged in users + setattr(db_slot, "attendee_id", db_attendee.id) + db.commit() + return db_attendee + + +def delete(db: Session, slot_id: int): + """remove existing slot by id""" + db_slot = get(db, slot_id) + db.delete(db_slot) + db.commit() + return db_slot + + +def is_available(db: Session, slot_id: int): + """check if slot is still available for booking""" + slot = get(db, slot_id) + if slot.schedule: + return slot and slot.booking_status == models.BookingStatus.requested + return False diff --git a/backend/src/appointment/database/repo/subscriber.py b/backend/src/appointment/database/repo/subscriber.py new file mode 100644 index 000000000..8d8fba1f8 --- /dev/null +++ b/backend/src/appointment/database/repo/subscriber.py @@ -0,0 +1,121 @@ +"""Module: repo.subscriber + +Repository providing CRUD functions for subscriber database models. +""" + +import re + +from sqlalchemy.orm import Session +from .. import models, schemas +from ...controller.auth import sign_url + + +def get(db: Session, subscriber_id: int) -> models.Subscriber | None: + """retrieve subscriber by id""" + return db.get(models.Subscriber, subscriber_id) + + +def get_by_email(db: Session, email: str): + """retrieve subscriber by email""" + return db.query(models.Subscriber).filter(models.Subscriber.email == email).first() + + +def get_by_username(db: Session, username: str): + """retrieve subscriber by username""" + return db.query(models.Subscriber).filter(models.Subscriber.username == username).first() + + +def get_by_appointment(db: Session, appointment_id: int): + """retrieve appointment by subscriber username and appointment slug (public)""" + if appointment_id: + return ( + db.query(models.Subscriber) + .join(models.Calendar) + .join(models.Appointment) + .filter(models.Appointment.id == appointment_id) + .first() + ) + return None + + +def get_by_google_state(db: Session, state: str): + """retrieve subscriber by google state, you'll have to manually check the google_state_expire_at!""" + if state is None: + return None + return db.query(models.Subscriber).filter(models.Subscriber.google_state == state).first() + + +def create(db: Session, subscriber: schemas.SubscriberBase): + """create new subscriber""" + db_subscriber = models.Subscriber(**subscriber.dict()) + db.add(db_subscriber) + db.commit() + db.refresh(db_subscriber) + return db_subscriber + + +def update(db: Session, data: schemas.SubscriberIn, subscriber_id: int): + """update all subscriber attributes, they can edit themselves""" + db_subscriber = get(db, subscriber_id) + for key, value in data: + if value is not None: + setattr(db_subscriber, key, value) + db.commit() + db.refresh(db_subscriber) + return db_subscriber + + +def delete(db: Session, subscriber: models.Subscriber): + """Delete a subscriber by subscriber id""" + db.delete(subscriber) + db.commit() + return True + + +def get_connections_limit(db: Session, subscriber_id: int): + """return the number of allowed connections for given subscriber or -1 for unlimited connections""" + # db_subscriber = get(db, subscriber_id) + # mapping = { + # models.SubscriberLevel.basic: int(os.getenv("TIER_BASIC_CALENDAR_LIMIT")), + # models.SubscriberLevel.plus: int(os.getenv("TIER_PLUS_CALENDAR_LIMIT")), + # models.SubscriberLevel.pro: int(os.getenv("TIER_PRO_CALENDAR_LIMIT")), + # models.SubscriberLevel.admin: -1, + # } + # return mapping[db_subscriber.level] + + # No limit right now! + return -1 + + +def verify_link(db: Session, url: str): + """Check if a given url is a valid signed subscriber profile link + Return subscriber if valid. + """ + # Look for a followed by an optional signature that ends the string + pattern = r"[\/]([\w\d\-_\.\@]+)[\/]?([\w\d]*)[\/]?$" + match = re.findall(pattern, url) + + if match is None or len(match) == 0: + return False + + # Flatten + match = match[0] + clean_url = url + + username = match[0] + signature = None + if len(match) > 1: + signature = match[1] + clean_url = clean_url.replace(signature, "") + + subscriber = get_by_username(db, username) + if not subscriber: + return False + + clean_url_with_short_link = clean_url + f"{subscriber.short_link_hash}" + signed_signature = sign_url(clean_url_with_short_link) + + # Verify the signature matches the incoming one + if signed_signature == signature: + return subscriber + return False diff --git a/backend/src/appointment/dependencies/auth.py b/backend/src/appointment/dependencies/auth.py index bf4a338ac..50436e9c0 100644 --- a/backend/src/appointment/dependencies/auth.py +++ b/backend/src/appointment/dependencies/auth.py @@ -26,7 +26,7 @@ def get_user_from_token(db, token: str): raise InvalidTokenException() id = sub.replace('uid-', '') - subscriber = repo.get_subscriber(db, int(id)) + subscriber = repo.subscriber.get(db, int(id)) # Token has been expired by us - temp measure to avoid spinning a refresh system, or a deny list for this issue if subscriber is None: @@ -60,7 +60,7 @@ def get_subscriber_from_signed_url( db: Session = Depends(get_db), ): """Retrieve a subscriber based off a signed url from the body. Requires `url` param to be used in the request.""" - subscriber = repo.verify_subscriber_link(db, url) + subscriber = repo.subscriber.verify_link(db, url) if not subscriber: raise validation.InvalidLinkException diff --git a/backend/src/appointment/exceptions/validation.py b/backend/src/appointment/exceptions/validation.py index 05ac99841..e939b2977 100644 --- a/backend/src/appointment/exceptions/validation.py +++ b/backend/src/appointment/exceptions/validation.py @@ -30,7 +30,7 @@ def get_msg(self): class InvalidLinkException(APIException): - """Raise when verify_subscriber_link fails""" + """Raise when subscriber.verify_link fails""" id_code = 'INVALID_LINK' status_code = 400 diff --git a/backend/src/appointment/routes/api.py b/backend/src/appointment/routes/api.py index f490ed355..bf82179d9 100644 --- a/backend/src/appointment/routes/api.py +++ b/backend/src/appointment/routes/api.py @@ -46,10 +46,10 @@ def update_me( data: schemas.SubscriberIn, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber) ): """endpoint to update data of authenticated subscriber""" - if subscriber.username != data.username and repo.get_subscriber_by_username(db, data.username): + if subscriber.username != data.username and repo.subscriber.get_by_username(db, data.username): raise HTTPException(status_code=403, detail=l10n('username-not-available')) - me = repo.update_subscriber(db=db, data=data, subscriber_id=subscriber.id) + me = repo.subscriber.update(db=db, data=data, subscriber_id=subscriber.id) return schemas.SubscriberBase( username=me.username, email=me.email, name=me.name, level=me.level, timezone=me.timezone ) @@ -60,7 +60,7 @@ def read_my_calendars( db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber), only_connected: bool = True ): """get all calendar connections of authenticated subscriber""" - calendars = repo.get_calendars_by_subscriber( + calendars = repo.calendar.get_by_subscriber( db, subscriber_id=subscriber.id, include_unconnected=not only_connected ) return [schemas.CalendarOut(id=c.id, title=c.title, color=c.color, connected=c.connected) for c in calendars] @@ -69,7 +69,7 @@ def read_my_calendars( @router.get("/me/appointments", response_model=list[schemas.AppointmentWithCalendarOut]) def read_my_appointments(db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """get all appointments of authenticated subscriber""" - appointments = repo.get_appointments_by_subscriber(db, subscriber_id=subscriber.id) + appointments = repo.appointment.get_by_subscriber(db, subscriber_id=subscriber.id) # Mix in calendar title and color. # Note because we `__dict__` any relationship values won't be carried over, so don't forget to manually add those! appointments = map( @@ -87,7 +87,7 @@ def get_my_signature(subscriber: Subscriber = Depends(get_subscriber)): @router.post("/me/signature") def refresh_signature(db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """Refresh a subscriber's signed short link""" - repo.update_subscriber( + repo.subscriber.update( db, schemas.SubscriberAuth( email=subscriber.email, username=subscriber.username, short_link_hash=secrets.token_hex(32) @@ -101,7 +101,7 @@ def refresh_signature(db: Session = Depends(get_db), subscriber: Subscriber = De @router.post("/verify/signature", deprecated=True) def verify_signature(url: str = Body(..., embed=True), db: Session = Depends(get_db)): """Verify a signed short link""" - if repo.verify_subscriber_link(db, url): + if repo.subscriber.verify_link(db, url): return True raise validation.InvalidLinkException() @@ -118,7 +118,7 @@ def create_my_calendar( # Test the connection first if calendar.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -149,7 +149,7 @@ def create_my_calendar( # create calendar try: - cal = repo.create_subscriber_calendar(db=db, calendar=calendar, subscriber_id=subscriber.id) + cal = repo.calendar.create(db=db, calendar=calendar, subscriber_id=subscriber.id) 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) @@ -158,11 +158,11 @@ def create_my_calendar( @router.get("/cal/{id}", response_model=schemas.CalendarConnectionOut) def read_my_calendar(id: int, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """endpoint to get a calendar from db""" - cal = repo.get_calendar(db, calendar_id=id) + cal = repo.calendar.get(db, calendar_id=id) if cal is None: raise validation.CalendarNotFoundException() - if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id): + if not repo.calendar.is_owned(db, calendar_id=id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() return schemas.CalendarConnectionOut( @@ -184,12 +184,12 @@ def update_my_calendar( subscriber: Subscriber = Depends(get_subscriber), ): """endpoint to update an existing calendar connection for authenticated subscriber""" - if not repo.calendar_exists(db, calendar_id=id): + 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): + if not repo.calendar.is_owned(db, calendar_id=id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() - cal = repo.update_subscriber_calendar(db=db, calendar=calendar, calendar_id=id) + cal = repo.calendar.update(db=db, calendar=calendar, calendar_id=id) return schemas.CalendarOut(id=cal.id, title=cal.title, color=cal.color, connected=cal.connected) @@ -200,13 +200,13 @@ def connect_my_calendar( subscriber: Subscriber = Depends(get_subscriber), ): """endpoint to update an existing calendar connection for authenticated subscriber""" - if not repo.calendar_exists(db, calendar_id=id): + 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): + if not repo.calendar.is_owned(db, calendar_id=id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() try: - cal = repo.update_subscriber_calendar_connection(db=db, calendar_id=id, is_connected=True) + cal = repo.calendar.update_connection(db=db, calendar_id=id, is_connected=True) 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) @@ -215,12 +215,12 @@ def connect_my_calendar( @router.delete("/cal/{id}", response_model=schemas.CalendarOut) def delete_my_calendar(id: int, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """endpoint to remove a calendar from db""" - if not repo.calendar_exists(db, calendar_id=id): + 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): + if not repo.calendar.is_owned(db, calendar_id=id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() - cal = repo.delete_subscriber_calendar(db=db, calendar_id=id) + cal = repo.calendar.delete(db=db, calendar_id=id) return schemas.CalendarOut(id=cal.id, title=cal.title, color=cal.color, connected=cal.connected) @@ -233,7 +233,7 @@ def read_remote_calendars( ): """endpoint to get calendars from a remote CalDAV server""" if connection.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -277,7 +277,7 @@ def sync_remote_calendars( # TODO: Also handle CalDAV connections external_connection = utils.list_first( - repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -312,13 +312,13 @@ def read_remote_events( redis_instance: Redis | None = Depends(get_redis), ): """endpoint to get events in a given date range from a remote calendar""" - db_calendar = repo.get_calendar(db, calendar_id=id) + db_calendar = repo.calendar.get(db, calendar_id=id) if db_calendar is None: raise validation.CalendarNotFoundException() if db_calendar.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -358,25 +358,25 @@ def create_my_calendar_appointment( a_s: schemas.AppointmentSlots, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber) ): """endpoint to add a new appointment with slots for a given calendar""" - if not repo.calendar_exists(db, calendar_id=a_s.appointment.calendar_id): + if not repo.calendar.exists(db, calendar_id=a_s.appointment.calendar_id): raise validation.CalendarNotFoundException() - if not repo.calendar_is_owned(db, calendar_id=a_s.appointment.calendar_id, subscriber_id=subscriber.id): + if not repo.calendar.is_owned(db, calendar_id=a_s.appointment.calendar_id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() - if not repo.calendar_is_connected(db, calendar_id=a_s.appointment.calendar_id): + if not repo.calendar.is_connected(db, calendar_id=a_s.appointment.calendar_id): raise validation.CalendarNotConnectedException() if a_s.appointment.meeting_link_provider == MeetingLinkProviderType.zoom and subscriber.get_external_connection(ExternalConnectionType.zoom) is None: raise validation.ZoomNotConnectedException() - return repo.create_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots) + return repo.appointment.create(db=db, appointment=a_s.appointment, slots=a_s.slots) @router.get("/apmt/{id}", response_model=schemas.Appointment, deprecated=True) def read_my_appointment(id: str, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """endpoint to get an appointment from db by id""" - db_appointment = repo.get_appointment(db, appointment_id=id) + db_appointment = repo.appointment.get(db, appointment_id=id) if db_appointment is None: raise validation.AppointmentNotFoundException() - if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): + if not repo.appointment.is_owned(db, appointment_id=id, subscriber_id=subscriber.id): raise validation.AppointmentNotAuthorizedException() return db_appointment @@ -390,36 +390,36 @@ def update_my_appointment( subscriber: Subscriber = Depends(get_subscriber), ): """endpoint to update an existing appointment with slots""" - db_appointment = repo.get_appointment(db, appointment_id=id) + db_appointment = repo.appointment.get(db, appointment_id=id) if db_appointment is None: raise validation.AppointmentNotFoundException() - if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): + if not repo.appointment.is_owned(db, appointment_id=id, subscriber_id=subscriber.id): raise validation.AppointmentNotAuthorizedException() - return repo.update_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots, appointment_id=id) + return repo.appointment.update(db=db, appointment=a_s.appointment, slots=a_s.slots, appointment_id=id) @router.delete("/apmt/{id}", response_model=schemas.Appointment, deprecated=True) def delete_my_appointment(id: int, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """endpoint to remove an appointment from db""" - db_appointment = repo.get_appointment(db, appointment_id=id) + db_appointment = repo.appointment.get(db, appointment_id=id) if db_appointment is None: raise validation.AppointmentNotFoundException() - if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): + if not repo.appointment.is_owned(db, appointment_id=id, subscriber_id=subscriber.id): raise validation.AppointmentNotAuthorizedException() - return repo.delete_calendar_appointment(db=db, appointment_id=id) + return repo.appointment.delete(db=db, appointment_id=id) @router.get("/apmt/public/{slug}", response_model=schemas.AppointmentOut, deprecated=True) def read_public_appointment(slug: str, db: Session = Depends(get_db)): """endpoint to retrieve an appointment from db via public link and only expose necessary data""" - a = repo.get_public_appointment(db, slug=slug) + a = repo.appointment.get_public(db, slug=slug) if a is None: raise validation.AppointmentNotFoundException() - s = repo.get_subscriber_by_appointment(db=db, appointment_id=a.id) + s = repo.subscriber.get_by_appointment(db=db, appointment_id=a.id) if s is None: raise validation.SubscriberNotFoundException() slots = [ @@ -439,23 +439,23 @@ def update_public_appointment_slot( google_client: GoogleClient = Depends(get_google_client), ): """endpoint to update a time slot for an appointment via public link and create an event in remote calendar""" - db_appointment = repo.get_public_appointment(db, slug=slug) + db_appointment = repo.appointment.get_public(db, slug=slug) if db_appointment is None: raise validation.AppointmentNotFoundException() - db_calendar = repo.get_calendar(db, calendar_id=db_appointment.calendar_id) + db_calendar = repo.calendar.get(db, calendar_id=db_appointment.calendar_id) if db_calendar is None: raise validation.CalendarNotFoundException() - if not repo.appointment_has_slot(db, appointment_id=db_appointment.id, slot_id=s_a.slot_id): + if not repo.appointment.has_slot(db, appointment_id=db_appointment.id, slot_id=s_a.slot_id): raise validation.SlotNotFoundException() - if not repo.slot_is_available(db, slot_id=s_a.slot_id): + if not repo.slot.is_available(db, slot_id=s_a.slot_id): raise validation.SlotAlreadyTakenException() if not validators.email(s_a.attendee.email): raise HTTPException(status_code=400, detail=l10n('slot-invalid-email')) - slot = repo.get_slot(db=db, slot_id=s_a.slot_id) + slot = repo.slot.get(db=db, slot_id=s_a.slot_id) # grab the subscriber - organizer = repo.get_subscriber_by_appointment(db=db, appointment_id=db_appointment.id) + organizer = repo.subscriber.get_by_appointment(db=db, appointment_id=db_appointment.id) location_url = db_appointment.location_url @@ -509,7 +509,7 @@ def update_public_appointment_slot( # create remote event if db_calendar.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, organizer.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, organizer.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -537,7 +537,7 @@ def update_public_appointment_slot( con.create_event(event=event, attendee=s_a.attendee, organizer=organizer, organizer_email=organizer_email) # update appointment slot data - repo.update_slot(db=db, slot_id=s_a.slot_id, attendee=s_a.attendee) + repo.slot.update(db=db, slot_id=s_a.slot_id, attendee=s_a.attendee) # send mail with .ics attachment to attendee Tools().send_vevent(background_tasks, db_appointment, slot, organizer, s_a.attendee) @@ -548,18 +548,18 @@ def update_public_appointment_slot( @router.get("/apmt/serve/ics/{slug}/{slot_id}", response_model=schemas.FileDownload) def public_appointment_serve_ics(slug: str, slot_id: int, db: Session = Depends(get_db)): """endpoint to serve ICS file for time slot to download""" - db_appointment = repo.get_public_appointment(db, slug=slug) + db_appointment = repo.appointment.get_public(db, slug=slug) if db_appointment is None: raise validation.AppointmentNotFoundException() - if not repo.appointment_has_slot(db, appointment_id=db_appointment.id, slot_id=slot_id): + if not repo.appointment.has_slot(db, appointment_id=db_appointment.id, slot_id=slot_id): raise validation.SlotNotFoundException() - slot = repo.get_slot(db=db, slot_id=slot_id) + slot = repo.slot.get(db=db, slot_id=slot_id) if slot is None: raise validation.SlotNotFoundException() - organizer = repo.get_subscriber_by_appointment(db=db, appointment_id=db_appointment.id) + organizer = repo.subscriber.get_by_appointment(db=db, appointment_id=db_appointment.id) return schemas.FileDownload( name="invite", diff --git a/backend/src/appointment/routes/auth.py b/backend/src/appointment/routes/auth.py index 01371d6e1..d67513658 100644 --- a/backend/src/appointment/routes/auth.py +++ b/backend/src/appointment/routes/auth.py @@ -111,14 +111,14 @@ def fxa_callback( raise HTTPException(400, l10n('email-mismatch')) # Check if we have an existing fxa connection by profile's uid - fxa_subscriber = repo.get_subscriber_by_fxa_uid(db, profile['uid']) + fxa_subscriber = repo.external_connection.get_subscriber_by_fxa_uid(db, profile['uid']) # Also look up the subscriber (in case we have an existing account that's not tied to a given fxa account) - subscriber = repo.get_subscriber_by_email(db, email) + subscriber = repo.subscriber.get_by_email(db, email) new_subscriber_flow = not fxa_subscriber and not subscriber if new_subscriber_flow: - subscriber = repo.create_subscriber(db, schemas.SubscriberBase( + subscriber = repo.subscriber.create(db, schemas.SubscriberBase( email=email, username=email, timezone=timezone, @@ -135,9 +135,9 @@ def fxa_callback( ) if not fxa_subscriber: - repo.create_subscriber_external_connection(db, external_connection_schema) + repo.external_connection.create(db, external_connection_schema) else: - repo.update_subscriber_external_connection_token(db, json.dumps(creds), subscriber.id, + repo.external_connection.update_token(db, json.dumps(creds), subscriber.id, external_connection_schema.type, external_connection_schema.type_id) @@ -155,7 +155,7 @@ def fxa_callback( data.name = profile['displayName'] if 'displayName' in profile else profile['email'].split('@')[0] data.username = profile['email'] - repo.update_subscriber(db, data, subscriber.id) + repo.subscriber.update(db, data, subscriber.id) # Generate our jwt token, we only store the username on the token access_token_expires = timedelta(minutes=float(os.getenv('JWT_EXPIRE_IN_MINS'))) @@ -175,7 +175,7 @@ def token( raise HTTPException(status_code=405) """Retrieve an access token from a given username and password.""" - subscriber = repo.get_subscriber_by_username(db, form_data.username) + subscriber = repo.subscriber.get_by_username(db, form_data.username) if not subscriber or subscriber.password is None: raise HTTPException(status_code=403, detail=l10n('invalid-credentials')) @@ -232,7 +232,7 @@ def me( # if os.getenv('AUTH_SCHEME') != 'password': # raise HTTPException(status_code=405) # -# subscriber = repo.create_subscriber(db, schemas.SubscriberBase( +# subscriber = repo.subscriber.create(db, schemas.SubscriberBase( # email=email, # username=email, # name=email.split('@')[0], diff --git a/backend/src/appointment/routes/google.py b/backend/src/appointment/routes/google.py index b076d5741..cbf92632f 100644 --- a/backend/src/appointment/routes/google.py +++ b/backend/src/appointment/routes/google.py @@ -59,7 +59,7 @@ def google_callback( return google_callback_error(l10n('google-auth-fail')) subscriber_id = request.session.get('google_oauth_subscriber_id') - subscriber = repo.get_subscriber(db, subscriber_id) + subscriber = repo.subscriber.get(db, subscriber_id) # Clear session keys request.session.pop('google_oauth_state') @@ -76,10 +76,10 @@ def google_callback( if google_id is None: return google_callback_error(l10n('google-auth-fail')) - external_connection = repo.get_external_connections_by_type(db, subscriber.id, ExternalConnectionType.google, google_id) + external_connection = repo.external_connection.get_by_type(db, subscriber.id, ExternalConnectionType.google, google_id) # Create an artificial limit of one google account per account, mainly because we didn't plan for multiple accounts! - remainder = list(filter(lambda ec: ec.type_id != google_id, repo.get_external_connections_by_type(db, subscriber.id, ExternalConnectionType.google))) + remainder = list(filter(lambda ec: ec.type_id != google_id, repo.external_connection.get_by_type(db, subscriber.id, ExternalConnectionType.google))) if len(remainder) > 0: return google_callback_error(l10n('google-only-one')) @@ -94,9 +94,9 @@ def google_callback( token=creds.to_json() ) - repo.create_subscriber_external_connection(db, external_connection_schema) + repo.external_connection.create(db, external_connection_schema) else: - repo.update_subscriber_external_connection_token(db, creds.to_json(), subscriber.id, + repo.external_connection.update_token(db, creds.to_json(), subscriber.id, ExternalConnectionType.google, google_id) error_occurred = google_client.sync_calendars(db, subscriber_id=subscriber.id, token=creds) diff --git a/backend/src/appointment/routes/invite.py b/backend/src/appointment/routes/invite.py index 0b9dce736..29eef5b8e 100644 --- a/backend/src/appointment/routes/invite.py +++ b/backend/src/appointment/routes/invite.py @@ -16,7 +16,7 @@ def generate_invite_codes(n: int, db: Session = Depends(get_db)): raise NotImplementedError """endpoint to generate n invite codes""" - return repo.generate_invite_codes(db, n) + return repo.invite.generate_codes(db, n) @router.put("/redeem/{code}") @@ -24,22 +24,22 @@ def use_invite_code(code: str, db: Session = Depends(get_db)): raise NotImplementedError """endpoint to create a new subscriber and update the corresponding invite""" - if not repo.invite_code_exists(db, code): + if not repo.invite.code_exists(db, code): raise validation.InviteCodeNotFoundException() - if not repo.invite_code_is_available(db, code): + if not repo.invite.code_is_available(db, code): raise validation.InviteCodeNotAvailableException() # TODO: get email from admin panel email = 'placeholder@mozilla.org' - subscriber = repo.create_subscriber(db, schemas.SubscriberBase(email=email, username=email)) - return repo.use_invite_code(db, code, subscriber.id) + subscriber = repo.subscriber.create(db, schemas.SubscriberBase(email=email, username=email)) + return repo.invite.use_code(db, code, subscriber.id) @router.put("/revoke/{code}") -def use_invite_code(code: str, db: Session = Depends(get_db)): +def revoke_invite_code(code: str, db: Session = Depends(get_db)): raise NotImplementedError """endpoint to revoke a given invite code and mark in unavailable""" - if not repo.invite_code_exists(db, code): + if not repo.invite.code_exists(db, code): raise validation.InviteCodeNotFoundException() - if not repo.invite_code_is_available(db, code): + if not repo.invite.code_is_available(db, code): raise validation.InviteCodeNotAvailableException() - return repo.revoke_invite_code(db, code) + return repo.invite.revoke_code(db, code) diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py index 4563457c8..2305f37b0 100644 --- a/backend/src/appointment/routes/schedule.py +++ b/backend/src/appointment/routes/schedule.py @@ -38,19 +38,19 @@ def create_calendar_schedule( subscriber: Subscriber = Depends(get_subscriber), ): """endpoint to add a new schedule for a given calendar""" - if not repo.calendar_exists(db, calendar_id=schedule.calendar_id): + if not repo.calendar.exists(db, calendar_id=schedule.calendar_id): raise validation.CalendarNotFoundException() - if not repo.calendar_is_owned(db, calendar_id=schedule.calendar_id, subscriber_id=subscriber.id): + if not repo.calendar.is_owned(db, calendar_id=schedule.calendar_id, subscriber_id=subscriber.id): raise validation.CalendarNotAuthorizedException() - if not repo.calendar_is_connected(db, calendar_id=schedule.calendar_id): + if not repo.calendar.is_connected(db, calendar_id=schedule.calendar_id): raise validation.CalendarNotConnectedException() - return repo.create_calendar_schedule(db=db, schedule=schedule) + return repo.schedule.create(db=db, schedule=schedule) @router.get("/", response_model=list[schemas.Schedule]) def read_schedules(db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)): """Gets all of the available schedules for the logged in subscriber""" - return repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) + return repo.schedule.get_by_subscriber(db, subscriber_id=subscriber.id) @router.get("/{id}", response_model=schemas.Schedule, deprecated=True) @@ -60,10 +60,10 @@ def read_schedule( subscriber: Subscriber = Depends(get_subscriber), ): """Gets information regarding a specific schedule""" - schedule = repo.get_schedule(db, schedule_id=id) + schedule = repo.schedule.get(db, schedule_id=id) if schedule is None: raise validation.ScheduleNotFoundException() - if not repo.schedule_is_owned(db, schedule_id=id, subscriber_id=subscriber.id): + if not repo.schedule.is_owned(db, schedule_id=id, subscriber_id=subscriber.id): raise validation.ScheduleNotAuthorizedException() return schedule @@ -76,13 +76,13 @@ def update_schedule( subscriber: Subscriber = Depends(get_subscriber), ): """endpoint to update an existing calendar connection for authenticated subscriber""" - if not repo.schedule_exists(db, schedule_id=id): + if not repo.schedule.exists(db, schedule_id=id): raise validation.ScheduleNotFoundException() - if not repo.schedule_is_owned(db, schedule_id=id, subscriber_id=subscriber.id): + 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: raise validation.ZoomNotConnectedException() - return repo.update_calendar_schedule(db=db, schedule=schedule, schedule_id=id) + return repo.schedule.update(db=db, schedule=schedule, schedule_id=id) @router.post("/public/availability", response_model=schemas.AppointmentOut) @@ -97,7 +97,7 @@ def read_schedule_availabilities( if subscriber.timezone is None: raise validation.ScheduleNotFoundException() - schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) + schedules = repo.schedule.get_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule @@ -108,7 +108,7 @@ def read_schedule_availabilities( if not schedule.active: raise validation.ScheduleNotActive() - calendars = repo.get_calendars_by_subscriber(db, subscriber.id, False) + calendars = repo.calendar.get_by_subscriber(db, subscriber.id, False) if not calendars or len(calendars) == 0: raise validation.CalendarNotFoundException() @@ -147,7 +147,7 @@ def request_schedule_availability_slot( if subscriber.timezone is None: raise validation.ScheduleNotFoundException() - schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) + schedules = repo.schedule.get_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule @@ -159,18 +159,18 @@ def request_schedule_availability_slot( raise validation.ScheduleNotFoundException() # get calendar - db_calendar = repo.get_calendar(db, calendar_id=schedule.calendar_id) + db_calendar = repo.calendar.get(db, calendar_id=schedule.calendar_id) if db_calendar is None: raise validation.CalendarNotFoundException() # check if slot still available, might already be taken at this time slot = schemas.SlotBase(**s_a.slot.dict()) - if repo.schedule_slot_exists(db, slot, schedule.id): + if repo.slot.exists_on_schedule(db, slot, schedule.id): raise validation.SlotAlreadyTakenException() # We need to verify that the time is actually available on the remote calendar if db_calendar.provider == CalendarProvider.google: - external_connection = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -196,7 +196,7 @@ def request_schedule_availability_slot( # Ok we need to clear the cache for all calendars, because we need to recheck them. con.bust_cached_events(True) - calendars = repo.get_calendars_by_subscriber(db, subscriber.id, False) + calendars = repo.calendar.get_by_subscriber(db, subscriber.id, False) existing_remote_events = Tools.existing_events_for_schedule(schedule, calendars, subscriber, google_client, db, redis) has_collision = Tools.events_roll_up_difference([slot], existing_remote_events) @@ -209,10 +209,10 @@ def request_schedule_availability_slot( slot.booking_tkn = token slot.booking_expires_at = datetime.now() + timedelta(days=1) slot.booking_status = BookingStatus.requested - slot = repo.add_schedule_slot(db, slot, schedule.id) + slot = repo.slot.add_for_schedule(db, slot, schedule.id) # create attendee for this slot - attendee = repo.update_slot(db, slot.id, s_a.attendee) + attendee = repo.slot.update(db, slot.id, s_a.attendee) # generate confirm and deny links with encoded booking token and signed owner url url = f"{signed_url_by_subscriber(subscriber)}/confirm/{slot.id}/{token}" @@ -232,7 +232,7 @@ def request_schedule_availability_slot( subscriber_name = subscriber.name if subscriber.name is not None else subscriber.email title = f"Appointment - {subscriber_name} and {attendee_name}" - appointment = repo.create_calendar_appointment(db, schemas.AppointmentFull( + appointment = repo.appointment.create(db, schemas.AppointmentFull( title=title, details=schedule.details, calendar_id=db_calendar.id, @@ -274,7 +274,7 @@ def decide_on_schedule_availability_slot( if confirmed: create an event in remote calendar and send invitation mail TODO: if denied: send information mail to bookee """ - subscriber = repo.verify_subscriber_link(db, data.owner_url) + subscriber = repo.subscriber.verify_link(db, data.owner_url) if not subscriber: raise validation.InvalidLinkException() @@ -282,7 +282,7 @@ def decide_on_schedule_availability_slot( if subscriber.timezone is None: raise validation.ScheduleNotFoundException() - schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) + schedules = repo.schedule.get_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule except IndexError: @@ -293,16 +293,16 @@ def decide_on_schedule_availability_slot( raise validation.ScheduleNotFoundException() # get calendar - calendar = repo.get_calendar(db, calendar_id=schedule.calendar_id) + calendar = repo.calendar.get(db, calendar_id=schedule.calendar_id) if calendar is None: raise validation.CalendarNotFoundException() # get slot and check if slot exists and is not booked yet and token is the same - slot = repo.get_slot(db, data.slot_id) + slot = repo.slot.get(db, data.slot_id) if ( not slot - or not repo.slot_is_available(db, slot.id) - or not repo.schedule_has_slot(db, schedule.id, slot.id) + or not repo.slot.is_available(db, slot.id) + or not repo.schedule.has_slot(db, schedule.id, slot.id) or slot.booking_tkn != data.slot_token ): raise validation.SlotNotFoundException() @@ -319,10 +319,10 @@ def decide_on_schedule_availability_slot( if slot.appointment_id: # delete the appointment, this will also delete the slot. - repo.delete_calendar_appointment(db, slot.appointment_id) + repo.appointment.delete(db, slot.appointment_id) else: # delete the scheduled slot to make the time available again - repo.delete_slot(db, slot.id) + repo.slot.delete(db, slot.id) # otherwise, confirm slot and create event else: @@ -363,7 +363,7 @@ def decide_on_schedule_availability_slot( else: title = slot.appointment.title # Update the appointment to closed - repo.update_appointment_status(db, slot.appointment_id, models.AppointmentStatus.closed) + repo.appointment.update_status(db, slot.appointment_id, models.AppointmentStatus.closed) event = schemas.Event( title=title, @@ -382,7 +382,7 @@ def decide_on_schedule_availability_slot( # create remote event if calendar.provider == CalendarProvider.google: - external_connection: ExternalConnection|None = utils.list_first(repo.get_external_connections_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) + external_connection: ExternalConnection|None = utils.list_first(repo.external_connection.get_by_type(db, subscriber.id, schemas.ExternalConnectionType.google)) if external_connection is None or external_connection.token is None: raise RemoteCalendarConnectionError() @@ -415,7 +415,7 @@ def decide_on_schedule_availability_slot( raise EventCouldNotBeAccepted # Book the slot at the end - slot = repo.book_slot(db, slot.id) + slot = repo.slot.book(db, slot.id) Tools().send_vevent(background_tasks, slot.appointment, slot, subscriber, slot.attendee) diff --git a/backend/src/appointment/routes/webhooks.py b/backend/src/appointment/routes/webhooks.py index 5a0417070..2bdf8dea6 100644 --- a/backend/src/appointment/routes/webhooks.py +++ b/backend/src/appointment/routes/webhooks.py @@ -24,7 +24,7 @@ def fxa_process( ): """Main for webhooks regarding fxa""" - subscriber: models.Subscriber = repo.get_subscriber_by_fxa_uid(db, decoded_token.get('sub')) + subscriber: models.Subscriber = repo.external_connection.get_subscriber_by_fxa_uid(db, decoded_token.get('sub')) if not subscriber: logging.warning("Webhook event received for non-existent user.") return @@ -58,7 +58,7 @@ def fxa_process( try: profile = fxa_client.get_profile() # Update profile with fxa info - repo.update_subscriber(db, schemas.SubscriberIn( + repo.subscriber.update(db, schemas.SubscriberIn( avatar_url=profile['avatar'], name=subscriber.name, username=subscriber.username diff --git a/backend/src/appointment/routes/zoom.py b/backend/src/appointment/routes/zoom.py index 0c54d50f8..f9e448a2d 100644 --- a/backend/src/appointment/routes/zoom.py +++ b/backend/src/appointment/routes/zoom.py @@ -47,7 +47,7 @@ def zoom_callback( raise HTTPException(400, l10n('oauth-error')) # Retrieve the user id set at the start of the zoom oauth process - subscriber = repo.get_subscriber(db, request.session['zoom_user_id']) + subscriber = repo.subscriber.get(db, request.session['zoom_user_id']) # Clear zoom session keys request.session.pop('zoom_state') @@ -69,8 +69,8 @@ def zoom_callback( token=json.dumps(creds) ) - if len(repo.get_external_connections_by_type(db, subscriber.id, external_connection_schema.type, external_connection_schema.type_id)) == 0: - repo.create_subscriber_external_connection(db, external_connection_schema) + if len(repo.external_connection.get_by_type(db, subscriber.id, external_connection_schema.type, external_connection_schema.type_id)) == 0: + repo.external_connection.create(db, external_connection_schema) return RedirectResponse(f"{os.getenv('FRONTEND_URL', 'http://localhost:8080')}/settings/account") @@ -84,7 +84,7 @@ def disconnect_account( zoom_connection = subscriber.get_external_connection(ExternalConnectionType.zoom) if zoom_connection: - repo.delete_external_connections_by_type_id(db, subscriber.id, zoom_connection.type, zoom_connection.type_id) + repo.external_connection.delete_by_type(db, subscriber.id, zoom_connection.type, zoom_connection.type_id) else: return False diff --git a/backend/test/conftest.py b/backend/test/conftest.py index e83c1c74f..61405d2af 100644 --- a/backend/test/conftest.py +++ b/backend/test/conftest.py @@ -160,7 +160,7 @@ def override_get_subscriber(request : Request): if 'authorization' not in request.headers: raise InvalidTokenException - return repo.get_subscriber_by_email(with_db(), os.getenv('TEST_USER_EMAIL')) + return repo.subscriber.get_by_email(with_db(), os.getenv('TEST_USER_EMAIL')) def override_get_google_client(): return None diff --git a/backend/test/factory/appointment_factory.py b/backend/test/factory/appointment_factory.py index 6397f3339..57ae6cefc 100644 --- a/backend/test/factory/appointment_factory.py +++ b/backend/test/factory/appointment_factory.py @@ -24,7 +24,7 @@ def _make_appointment(calendar_id=FAKER_RANDOM_VALUE, slots=FAKER_RANDOM_VALUE ): with with_db() as db: - appointment = repo.create_calendar_appointment(db, schemas.AppointmentFull( + appointment = repo.appointment.create(db, schemas.AppointmentFull( title=title if factory_has_value(title) else fake.name(), details=details if factory_has_value(details) else fake.sentence(), duration=duration if factory_has_value(duration) else fake.pyint(15, 60), @@ -44,7 +44,7 @@ def _make_appointment(calendar_id=FAKER_RANDOM_VALUE, if not factory_has_value(slots): make_appointment_slot(appointment_id=appointment.id) else: - repo.add_appointment_slots(db, slots, appointment.id) + repo.slot.add_for_appointment(db, slots, appointment.id) # Refresh our appointment now that is has slot data db.refresh(appointment) diff --git a/backend/test/factory/calendar_factory.py b/backend/test/factory/calendar_factory.py index e573d0451..c038382cc 100644 --- a/backend/test/factory/calendar_factory.py +++ b/backend/test/factory/calendar_factory.py @@ -11,7 +11,7 @@ def make_caldav_calendar(with_db): def _make_caldav_calendar(subscriber_id=TEST_USER_ID, url=FAKER_RANDOM_VALUE, title=FAKER_RANDOM_VALUE, color=FAKER_RANDOM_VALUE, connected=False, user=FAKER_RANDOM_VALUE, password=FAKER_RANDOM_VALUE): with with_db() as db: title = title if factory_has_value(title) else fake.name() - return repo.create_subscriber_calendar(db, schemas.CalendarConnection( + return repo.calendar.create(db, schemas.CalendarConnection( title=title, color=color if factory_has_value(color) else fake.color(), connected=connected, @@ -32,7 +32,7 @@ def _make_google_calendar(subscriber_id=TEST_USER_ID, title=FAKER_RANDOM_VALUE, with with_db() as db: title = title if factory_has_value(title) else fake.name() id = id if factory_has_value(id) else fake.uuid4() - return repo.create_subscriber_calendar(db, schemas.CalendarConnection( + return repo.calendar.create(db, schemas.CalendarConnection( title=title, color=color if factory_has_value(color) else fake.color(), connected=connected, diff --git a/backend/test/factory/external_connection_factory.py b/backend/test/factory/external_connection_factory.py index 55776ff71..dc2b4e3b3 100644 --- a/backend/test/factory/external_connection_factory.py +++ b/backend/test/factory/external_connection_factory.py @@ -14,7 +14,7 @@ def _make_external_connections(subscriber_id, type_id=FAKER_RANDOM_VALUE, token=FAKER_RANDOM_VALUE): with with_db() as db: - return repo.create_subscriber_external_connection(db, schemas.ExternalConnection( + return repo.external_connection.create(db, schemas.ExternalConnection( owner_id=subscriber_id, name=name if factory_has_value(name) else fake.name(), type=type if factory_has_value(type) else fake.random_element( diff --git a/backend/test/factory/schedule_factory.py b/backend/test/factory/schedule_factory.py index 03d22b6c1..d6c7bf870 100644 --- a/backend/test/factory/schedule_factory.py +++ b/backend/test/factory/schedule_factory.py @@ -24,7 +24,7 @@ def _make_schedule(calendar_id=FAKER_RANDOM_VALUE, slot_duration=FAKER_RANDOM_VALUE, ): with with_db() as db: - return repo.create_calendar_schedule(db, schemas.ScheduleBase( + return repo.schedule.create(db, schemas.ScheduleBase( active=active, name=name if factory_has_value(name) else fake.name(), location_url=location_url if factory_has_value(location_url) else fake.url(), diff --git a/backend/test/factory/slot_factory.py b/backend/test/factory/slot_factory.py index b7c18a7c0..fda08151c 100644 --- a/backend/test/factory/slot_factory.py +++ b/backend/test/factory/slot_factory.py @@ -18,7 +18,7 @@ def _make_appointment_slot(appointment_id=None, meeting_link_id=None, meeting_link_url=None): with with_db() as db: - return repo.add_appointment_slots(db, [schemas.SlotBase( + return repo.slot.add_for_appointment(db, [schemas.SlotBase( start=start if factory_has_value(start) else fake.date_time(), duration=duration if factory_has_value(duration) else fake.pyint(15, 60), attendee_id=attendee_id, diff --git a/backend/test/factory/subscriber_factory.py b/backend/test/factory/subscriber_factory.py index e6351e7b1..461dca74e 100644 --- a/backend/test/factory/subscriber_factory.py +++ b/backend/test/factory/subscriber_factory.py @@ -11,7 +11,7 @@ def make_subscriber(with_db): def _make_subscriber(level, name=FAKER_RANDOM_VALUE, username=FAKER_RANDOM_VALUE, email=FAKER_RANDOM_VALUE, password=None): with with_db() as db: - subscriber = repo.create_subscriber(db, schemas.SubscriberBase( + subscriber = repo.subscriber.create(db, schemas.SubscriberBase( name=name if factory_has_value(name) else fake.name(), username=username if factory_has_value(username) else fake.name().replace(' ', '_'), email=email if factory_has_value(email) else fake.email(), diff --git a/backend/test/integration/test_auth.py b/backend/test/integration/test_auth.py index 3904be9c0..1bed6976f 100644 --- a/backend/test/integration/test_auth.py +++ b/backend/test/integration/test_auth.py @@ -82,7 +82,7 @@ def test_fxa_callback(self, with_db, with_client, monkeypatch): assert response.status_code == 307, response.text with with_db() as db: - subscriber = repo.get_subscriber_by_email(db, FXA_CLIENT_PATCH.get('subscriber_email')) + subscriber = repo.subscriber.get_by_email(db, FXA_CLIENT_PATCH.get('subscriber_email')) assert subscriber assert subscriber.avatar_url == FXA_CLIENT_PATCH.get('subscriber_avatar_url') assert subscriber.name == FXA_CLIENT_PATCH.get('subscriber_display_name') diff --git a/backend/test/integration/test_profile.py b/backend/test/integration/test_profile.py index 23464e8c2..cd4dbdd08 100644 --- a/backend/test/integration/test_profile.py +++ b/backend/test/integration/test_profile.py @@ -31,7 +31,7 @@ def test_update_me(self, with_db, with_client): # Confirm the data was saved with with_db() as db: - subscriber = repo.get_subscriber_by_email(db, os.getenv('TEST_USER_EMAIL')) + subscriber = repo.subscriber.get_by_email(db, os.getenv('TEST_USER_EMAIL')) assert subscriber.username == "test" assert subscriber.name == "Test Account" assert subscriber.timezone == "Europe/Berlin" diff --git a/backend/test/integration/test_schedule.py b/backend/test/integration/test_schedule.py index 2e750d267..cc5b02321 100644 --- a/backend/test/integration/test_schedule.py +++ b/backend/test/integration/test_schedule.py @@ -508,5 +508,5 @@ def bust_cached_events(self, all_calendars = False): # Look up the slot with with_db() as db: - slot = repo.get_slot(db, slot_id) + slot = repo.slot.get(db, slot_id) assert slot.appointment_id diff --git a/backend/test/integration/test_webhooks.py b/backend/test/integration/test_webhooks.py index f5f328a20..3faa572b1 100644 --- a/backend/test/integration/test_webhooks.py +++ b/backend/test/integration/test_webhooks.py @@ -40,7 +40,7 @@ def override_get_webhook_auth(): with freeze_time('Aug 13th 2019'): # Update the external connection time to match our freeze_time with with_db() as db: - fxa_connection = repo.get_external_connections_by_type(db, subscriber_id, models.ExternalConnectionType.fxa, FXA_USER_ID)[0] + fxa_connection = repo.external_connection.get_by_type(db, subscriber_id, models.ExternalConnectionType.fxa, FXA_USER_ID)[0] fxa_connection.time_updated = datetime.datetime.now() db.add(fxa_connection) db.commit() @@ -51,19 +51,19 @@ def override_get_webhook_auth(): assert response.status_code == 200, response.text with with_db() as db: - subscriber = repo.get_subscriber(db, subscriber_id) + subscriber = repo.subscriber.get(db, subscriber_id) assert subscriber.minimum_valid_iat_time is not None # Update the external connection time to match our current time # This will make the change password event out of date with with_db() as db: - fxa_connection = repo.get_external_connections_by_type(db, subscriber_id, models.ExternalConnectionType.fxa, FXA_USER_ID)[0] + fxa_connection = repo.external_connection.get_by_type(db, subscriber_id, models.ExternalConnectionType.fxa, FXA_USER_ID)[0] fxa_connection.time_updated = datetime.datetime.now() db.add(fxa_connection) db.commit() # Reset our minimum_valid_iat_time, so we can ensure it stays None - subscriber = repo.get_subscriber(db, subscriber_id) + subscriber = repo.subscriber.get(db, subscriber_id) subscriber.minimum_valid_iat_time = None db.add(subscriber) db.commit() @@ -74,7 +74,7 @@ def override_get_webhook_auth(): ) assert response.status_code == 200, response.text with with_db() as db: - subscriber = repo.get_subscriber(db, subscriber_id) + subscriber = repo.subscriber.get(db, subscriber_id) assert subscriber.minimum_valid_iat_time is None def test_fxa_process_change_primary_email(self, with_db, with_client, make_pro_subscriber, make_external_connections): @@ -119,7 +119,7 @@ def override_get_webhook_auth(): # Refresh the subscriber and test minimum_valid_iat_time (they should be logged out), and email address with with_db() as db: - subscriber = repo.get_subscriber(db, subscriber_id) + subscriber = repo.subscriber.get(db, subscriber_id) assert subscriber.email == NEW_EMAIL assert subscriber.minimum_valid_iat_time is not None @@ -160,6 +160,6 @@ def override_get_webhook_auth(): with with_db() as db: # Make sure everything we created is gone. A more exhaustive check is done in the delete account test - assert repo.get_subscriber(db, subscriber.id) is None - assert repo.get_calendar(db, calendar.id) is None - assert repo.get_appointment(db, appointment.id) is None + assert repo.subscriber.get(db, subscriber.id) is None + assert repo.calendar.get(db, calendar.id) is None + assert repo.appointment.get(db, appointment.id) is None diff --git a/backend/test/unit/test_auth_dependency.py b/backend/test/unit/test_auth_dependency.py index 9d6824788..4bf26a22d 100644 --- a/backend/test/unit/test_auth_dependency.py +++ b/backend/test/unit/test_auth_dependency.py @@ -50,7 +50,7 @@ def test_get_user_from_token(self, with_db, with_l10n, make_pro_subscriber): with freeze_time("Jan 10th 2024"): with with_db() as db: # We need to pull down the subscriber in this db session, otherwise we can't save it. - subscriber = repo.get_subscriber(db, subscriber.id) + subscriber = repo.subscriber.get(db, subscriber.id) subscriber.minimum_valid_iat_time = datetime.datetime.now(datetime.UTC) db.add(subscriber) db.commit() From 170d729fb2a4bc6e600cda53cc3f1247035bfd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Fri, 19 Apr 2024 16:48:37 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A7=AA=20Test=20repo=20module=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/appointment/database/repo/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/src/appointment/database/repo/__init__.py diff --git a/backend/src/appointment/database/repo/__init__.py b/backend/src/appointment/database/repo/__init__.py new file mode 100644 index 000000000..8bcd7e2d2 --- /dev/null +++ b/backend/src/appointment/database/repo/__init__.py @@ -0,0 +1 @@ +from . import appointment, attendee, calendar, external_connection, invite, schedule, slot, subscriber