app.routes.bookings
Booking routing module.
Provides authenticated endpoints for submitting bookings, viewing bookings, and administering booking lifecycle transitions.
1""" 2Booking routing module. 3 4Provides authenticated endpoints for submitting bookings, viewing bookings, 5and administering booking lifecycle transitions. 6""" 7 8from __future__ import annotations 9 10from datetime import date, datetime, time 11from typing import Literal 12from uuid import UUID 13 14from fastapi import APIRouter, Depends, HTTPException, Query, Response, status 15from pydantic import BaseModel, ConfigDict 16from sqlmodel.ext.asyncio.session import AsyncSession 17 18from app.database import get_session 19from app.models import Booking, BookingStatus, RecurrenceFrequency, User 20from app.services.auth import current_active_user, require_admin 21from app.services.booking_service import ( 22 BookingConflictError, 23 BookingNotFoundError, 24 BookingServiceError, 25 BookingStateError, 26 get_all_bookings, 27 get_user_bookings, 28 process_booking_action, 29 submit_booking, 30) 31 32router = APIRouter(prefix="/api/bookings", tags=["bookings"]) 33 34 35class TimeSlotRead(BaseModel): 36 model_config = ConfigDict(from_attributes=True) 37 38 id: int 39 room_id: int 40 slot_date: date 41 start_time: time 42 end_time: time 43 status: str 44 booking_id: int | None 45 46 47class BookingRead(BaseModel): 48 model_config = ConfigDict(from_attributes=True) 49 50 id: int 51 userID: UUID 52 submittedByRole: str 53 roomID: int 54 status: str 55 recurrenceFrequency: str 56 recurrenceEndDate: date | None 57 createdAt: datetime 58 timeSlots: list[TimeSlotRead] 59 60 61class BookingCreate(BaseModel): 62 room_id: int 63 date: date 64 slot_ids: list[int] 65 recurrence_freq: Literal["none", "weekly"] = "none" 66 recurrence_end_date: date | None = None 67 68 69class BookingActionUpdate(BaseModel): 70 action: Literal["approve", "deny", "cancel"] 71 72 73def _translate_booking_error(exc: Exception) -> HTTPException: 74 if isinstance(exc, BookingConflictError): 75 return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) 76 if isinstance(exc, BookingNotFoundError): 77 return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) 78 if isinstance(exc, (BookingServiceError, BookingStateError, ValueError)): 79 return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) 80 raise exc 81 82 83@router.post("", response_model=BookingRead, status_code=status.HTTP_201_CREATED) 84async def create_booking( 85 booking_in: BookingCreate, 86 response: Response, 87 user: User = Depends(current_active_user), 88 session: AsyncSession = Depends(get_session), 89): 90 try: 91 booking = await session.run_sync( 92 lambda sync_session: submit_booking( 93 user=user, 94 room_id=booking_in.room_id, 95 slot_ids=booking_in.slot_ids, 96 recurrence_freq=booking_in.recurrence_freq, 97 recurrence_end_date=booking_in.recurrence_end_date, 98 session=sync_session, 99 ) 100 ) 101 except Exception as exc: 102 raise _translate_booking_error(exc) from exc 103 104 response.status_code = status.HTTP_201_CREATED 105 return booking 106 107 108@router.get("", response_model=list[BookingRead]) 109async def list_bookings( 110 status_filter: str | None = Query(default=None, alias="status"), 111 user: User = Depends(current_active_user), 112 session: AsyncSession = Depends(get_session), 113): 114 try: 115 if user.role == "admin": 116 return await session.run_sync( 117 lambda sync_session: get_all_bookings(sync_session, status_filter) 118 ) 119 return await session.run_sync( 120 lambda sync_session: get_user_bookings(user.id, sync_session) 121 ) 122 except Exception as exc: 123 raise _translate_booking_error(exc) from exc 124 125 126@router.patch("/{booking_id}", response_model=BookingRead) 127async def update_booking( 128 booking_id: int, 129 booking_update: BookingActionUpdate, 130 admin_user: User = Depends(require_admin), 131 session: AsyncSession = Depends(get_session), 132): 133 del admin_user 134 try: 135 booking = await session.run_sync( 136 lambda sync_session: process_booking_action( 137 booking_id, booking_update.action, sync_session 138 ) 139 ) 140 except Exception as exc: 141 raise _translate_booking_error(exc) from exc 142 143 return booking
36class TimeSlotRead(BaseModel): 37 model_config = ConfigDict(from_attributes=True) 38 39 id: int 40 room_id: int 41 slot_date: date 42 start_time: time 43 end_time: time 44 status: str 45 booking_id: int | None
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
__class_vars__: The names of the class variables defined on the model.
__private_attributes__: Metadata about the private attributes of the model.
__signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
__args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.
__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
48class BookingRead(BaseModel): 49 model_config = ConfigDict(from_attributes=True) 50 51 id: int 52 userID: UUID 53 submittedByRole: str 54 roomID: int 55 status: str 56 recurrenceFrequency: str 57 recurrenceEndDate: date | None 58 createdAt: datetime 59 timeSlots: list[TimeSlotRead]
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
__class_vars__: The names of the class variables defined on the model.
__private_attributes__: Metadata about the private attributes of the model.
__signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
__args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.
__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
62class BookingCreate(BaseModel): 63 room_id: int 64 date: date 65 slot_ids: list[int] 66 recurrence_freq: Literal["none", "weekly"] = "none" 67 recurrence_end_date: date | None = None
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
__class_vars__: The names of the class variables defined on the model.
__private_attributes__: Metadata about the private attributes of the model.
__signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
__args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.
__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
__class_vars__: The names of the class variables defined on the model.
__private_attributes__: Metadata about the private attributes of the model.
__signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
__args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.
__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
84@router.post("", response_model=BookingRead, status_code=status.HTTP_201_CREATED) 85async def create_booking( 86 booking_in: BookingCreate, 87 response: Response, 88 user: User = Depends(current_active_user), 89 session: AsyncSession = Depends(get_session), 90): 91 try: 92 booking = await session.run_sync( 93 lambda sync_session: submit_booking( 94 user=user, 95 room_id=booking_in.room_id, 96 slot_ids=booking_in.slot_ids, 97 recurrence_freq=booking_in.recurrence_freq, 98 recurrence_end_date=booking_in.recurrence_end_date, 99 session=sync_session, 100 ) 101 ) 102 except Exception as exc: 103 raise _translate_booking_error(exc) from exc 104 105 response.status_code = status.HTTP_201_CREATED 106 return booking
109@router.get("", response_model=list[BookingRead]) 110async def list_bookings( 111 status_filter: str | None = Query(default=None, alias="status"), 112 user: User = Depends(current_active_user), 113 session: AsyncSession = Depends(get_session), 114): 115 try: 116 if user.role == "admin": 117 return await session.run_sync( 118 lambda sync_session: get_all_bookings(sync_session, status_filter) 119 ) 120 return await session.run_sync( 121 lambda sync_session: get_user_bookings(user.id, sync_session) 122 ) 123 except Exception as exc: 124 raise _translate_booking_error(exc) from exc
127@router.patch("/{booking_id}", response_model=BookingRead) 128async def update_booking( 129 booking_id: int, 130 booking_update: BookingActionUpdate, 131 admin_user: User = Depends(require_admin), 132 session: AsyncSession = Depends(get_session), 133): 134 del admin_user 135 try: 136 booking = await session.run_sync( 137 lambda sync_session: process_booking_action( 138 booking_id, booking_update.action, sync_session 139 ) 140 ) 141 except Exception as exc: 142 raise _translate_booking_error(exc) from exc 143 144 return booking