app.models

Domain models for the room-booking system.

This package contains the SQLModel definitions representing the core entities of the application. All models are re-exported here for convenient importing and to ensure proper registration with SQLAlchemy's metadata before database initialization.

Exports:

  • User: Represents an authenticated person in the system.
  • Room: Represents a physical bookable space.
  • TimeSlot: Represents a specific bookable time window for a room.
  • Booking: Represents a confirmed reservation.
  • Notification: Represents a system alert or message.
 1"""
 2Domain models for the room-booking system.
 3
 4This package contains the `SQLModel` definitions representing the core entities
 5of the application. All models are re-exported here for convenient importing
 6and to ensure proper registration with SQLAlchemy's metadata before database
 7initialization.
 8
 9**Exports:**
10
11- `User`: Represents an authenticated person in the system.
12- `Room`: Represents a physical bookable space.
13- `TimeSlot`: Represents a specific bookable time window for a room.
14- `Booking`: Represents a confirmed reservation.
15- `Notification`: Represents a system alert or message.
16"""
17
18from .booking import (
19    Booking,
20    BookingStatus,
21    RecurrenceFrequency,
22    TimeSlot,
23    TimeslotStatus,
24)
25from .notification import Notification, NotificationType
26from .room import Room
27from .user import User, UserRole
28
29__all__ = [
30    "User",
31    "UserRole",
32    "Room",
33    "TimeSlot",
34    "TimeslotStatus",
35    "RecurrenceFrequency",
36    "BookingStatus",
37    "Booking",
38    "NotificationType",
39    "Notification",
40]
class User(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]):
32class User(SQLModelBaseUserDB, table=True):
33    """
34    Represents any authenticated person in the system.
35
36    This class inherits from `SQLModelBaseUserDB` which provides core authentication
37    attributes, and it adds a specific role attribute for access control.
38    """
39
40    name: str = Field(default="")
41    """The display name of the user. Defaults to empty string."""
42
43    role: UserRole = Field(default=UserRole.STUDENT)
44    """The system role of the user (e.g., student, teacher, admin). Defaults to `UserRole.STUDENT`."""
45
46    @field_validator("role", mode="before")
47    @classmethod
48    def validate_role(cls, v: object) -> UserRole:
49        """
50        Validates and coerces the incoming value for the user's role.
51        """
52        if isinstance(v, UserRole):
53            return v
54        try:
55            return UserRole(v)
56        except ValueError:
57            allowed = ", ".join(r.value for r in UserRole)
58            raise ValueError(f"Invalid role '{v}'. Must be one of: {allowed}")

Represents any authenticated person in the system.

This class inherits from SQLModelBaseUserDB which provides core authentication attributes, and it adds a specific role attribute for access control.

name: str

The display name of the user. Defaults to empty string.

role: UserRole

The system role of the user (e.g., student, teacher, admin). Defaults to UserRole.STUDENT.

@field_validator('role', mode='before')
@classmethod
def validate_role(cls, v: object) -> UserRole:
46    @field_validator("role", mode="before")
47    @classmethod
48    def validate_role(cls, v: object) -> UserRole:
49        """
50        Validates and coerces the incoming value for the user's role.
51        """
52        if isinstance(v, UserRole):
53            return v
54        try:
55            return UserRole(v)
56        except ValueError:
57            allowed = ", ".join(r.value for r in UserRole)
58            raise ValueError(f"Invalid role '{v}'. Must be one of: {allowed}")

Validates and coerces the incoming value for the user's role.

id: Annotated[uuid.UUID, UuidVersion(uuid_version=4)]
hashed_password: str
is_active: bool
is_superuser: bool
is_verified: bool
email: pydantic.networks.EmailStr
class UserRole(builtins.str, enum.Enum):
19class UserRole(str, enum.Enum):
20    """
21    Enumeration of allowed roles for a User in the room-booking system.
22    """
23
24    STUDENT = "student"
25    """A standard user with base booking privileges."""
26    TEACHER = "teacher"
27    """An instructor user with elevated booking privileges."""
28    ADMIN = "admin"
29    """A system administrator with full access rights."""

Enumeration of allowed roles for a User in the room-booking system.

STUDENT = <UserRole.STUDENT: 'student'>

A standard user with base booking privileges.

TEACHER = <UserRole.TEACHER: 'teacher'>

An instructor user with elevated booking privileges.

ADMIN = <UserRole.ADMIN: 'admin'>

A system administrator with full access rights.

class Room(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]):
17class Room(SQLModel, table=True):
18    """
19    Represents a bookable physical space at the campus.
20    """
21
22    id: Optional[int] = Field(default=None, primary_key=True)
23    """The primary key of the room."""
24
25    name: str = Field(sa_column=Column(String, unique=True, nullable=False))
26    """The human-readable name of the room (e.g., `"A-203"`). Must be unique."""
27
28    capacity: int = Field(nullable=False, ge=1)
29    """The maximum number of people the room can accommodate. Must be `>= 1`."""
30
31    time_slots: List["TimeSlot"] = Relationship(back_populates="room")  # type: ignore # noqa: F821

Represents a bookable physical space at the campus.

id: Optional[int]

The primary key of the room.

name: str

The human-readable name of the room (e.g., "A-203"). Must be unique.

capacity: int

The maximum number of people the room can accommodate. Must be >= 1.

time_slots: sqlalchemy.orm.base.Mapped[typing.List[ForwardRef('TimeSlot')]]
class TimeSlot(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]):
131class TimeSlot(SQLModel, table=True):
132    """
133    Represents any room request or timeslot unit in the system.
134    """
135
136    id: int | None = Field(default=None, primary_key=True)
137    """The primary key for the timeslot."""
138    room_id: int = Field(foreign_key="room.id", nullable=False)
139    """Foreign key linking to the associated `Room`."""
140    slot_date: date = Field(nullable=False)
141    """The specific date of the timeslot."""
142    start_time: time = Field(nullable=False)
143    """The start time of the timeslot window."""
144    end_time: time = Field(nullable=False)
145    """The end time of the timeslot window."""
146    status: TimeslotStatus = Field(default=TimeslotStatus.AVAILABLE)
147    """Current status of the slot (`available`, `held`, `booked`)."""
148    booking_id: int | None = Field(default=None, foreign_key="booking.id")
149    """Foreign key linking to a confirmed `Booking`."""
150    booking: Optional["Booking"] = Relationship(back_populates="timeSlots")  # noqa: F821
151    """The Booking this timeslot belongs to, if any."""
152
153    room: Optional["Room"] = Relationship(back_populates="time_slots")  # noqa: F821
154
155    def hold(self):
156        """
157        Temporarily reserve the room timeslot.
158        """
159        if self.status != TimeslotStatus.AVAILABLE:
160            raise ValueError("Only available timeslots can be held.")
161        self.status = TimeslotStatus.HELD
162
163    def book(self):
164        """
165        Confirm a reservation for the room timeslot.
166        """
167        if self.status != TimeslotStatus.HELD:
168            raise ValueError("Only held timeslots can be booked.")
169        self.status = TimeslotStatus.BOOKED
170
171    def release(self):
172        """
173        Cancel a reservation or hold on the room timeslot, freeing it.
174        """
175        if self.status != TimeslotStatus.HELD and self.status != TimeslotStatus.BOOKED:
176            raise ValueError("Only held or booked timeslots can be released.")
177        self.status = TimeslotStatus.AVAILABLE

Represents any room request or timeslot unit in the system.

id: int | None

The primary key for the timeslot.

room_id: int

Foreign key linking to the associated Room.

slot_date: datetime.date

The specific date of the timeslot.

start_time: datetime.time

The start time of the timeslot window.

end_time: datetime.time

The end time of the timeslot window.

status: TimeslotStatus

Current status of the slot (available, held, booked).

booking_id: int | None

Foreign key linking to a confirmed Booking.

booking: sqlalchemy.orm.base.Mapped[typing.Optional[Booking]]

The Booking this timeslot belongs to, if any.

room: sqlalchemy.orm.base.Mapped[typing.Optional[ForwardRef('Room')]]
def hold(self):
155    def hold(self):
156        """
157        Temporarily reserve the room timeslot.
158        """
159        if self.status != TimeslotStatus.AVAILABLE:
160            raise ValueError("Only available timeslots can be held.")
161        self.status = TimeslotStatus.HELD

Temporarily reserve the room timeslot.

def book(self):
163    def book(self):
164        """
165        Confirm a reservation for the room timeslot.
166        """
167        if self.status != TimeslotStatus.HELD:
168            raise ValueError("Only held timeslots can be booked.")
169        self.status = TimeslotStatus.BOOKED

Confirm a reservation for the room timeslot.

def release(self):
171    def release(self):
172        """
173        Cancel a reservation or hold on the room timeslot, freeing it.
174        """
175        if self.status != TimeslotStatus.HELD and self.status != TimeslotStatus.BOOKED:
176            raise ValueError("Only held or booked timeslots can be released.")
177        self.status = TimeslotStatus.AVAILABLE

Cancel a reservation or hold on the room timeslot, freeing it.

class TimeslotStatus(builtins.str, enum.Enum):
118class TimeslotStatus(str, Enum):
119    """
120    Allowed states for a room timeslot in the room-booking system.
121    """
122
123    AVAILABLE = "available"
124    """The timeslot is free to be held or booked."""
125    HELD = "held"
126    """The timeslot is temporarily reserved."""
127    BOOKED = "booked"
128    """The timeslot is fully confirmed and reserved."""

Allowed states for a room timeslot in the room-booking system.

AVAILABLE = <TimeslotStatus.AVAILABLE: 'available'>

The timeslot is free to be held or booked.

HELD = <TimeslotStatus.HELD: 'held'>

The timeslot is temporarily reserved.

BOOKED = <TimeslotStatus.BOOKED: 'booked'>

The timeslot is fully confirmed and reserved.

class RecurrenceFrequency(builtins.str, enum.Enum):
33class RecurrenceFrequency(str, Enum):
34    """
35    Allowed recurrence frequency values.
36    """
37
38    NONE = "none"
39    WEEKLY = "weekly"

Allowed recurrence frequency values.

NONE = <RecurrenceFrequency.NONE: 'none'>
WEEKLY = <RecurrenceFrequency.WEEKLY: 'weekly'>
class BookingStatus(builtins.str, enum.Enum):
22class BookingStatus(str, Enum):
23    """
24    Allowed lifecycle statuses for any given booking.
25    """
26
27    PENDING = "pending"
28    APPROVED = "approved"
29    DENIED = "denied"
30    CANCELLED = "cancelled"

Allowed lifecycle statuses for any given booking.

PENDING = <BookingStatus.PENDING: 'pending'>
APPROVED = <BookingStatus.APPROVED: 'approved'>
DENIED = <BookingStatus.DENIED: 'denied'>
CANCELLED = <BookingStatus.CANCELLED: 'cancelled'>
class Booking(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]):
 42class Booking(SQLModel, table=True):
 43    """
 44    Represents the room booking request made by a given user.
 45    """
 46
 47    id: Optional[int] = Field(default=None, primary_key=True)
 48    userID: UUID = Field(foreign_key="user.id", nullable=False)
 49    submittedByRole: UserRole = Field(default=UserRole.STUDENT, nullable=False)
 50    roomID: int = Field(foreign_key="room.id", nullable=False)
 51    status: BookingStatus = Field(default=BookingStatus.PENDING)
 52    recurrenceFrequency: RecurrenceFrequency = Field(default=RecurrenceFrequency.NONE)
 53    recurrenceEndDate: Optional[date] = Field(default=None, nullable=True)
 54    createdAt: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
 55
 56    # One Booking -> many TimeSlots
 57    timeSlots: List["TimeSlot"] = Relationship(back_populates="booking")  # noqa: F821
 58
 59    @field_validator("status", mode="before")
 60    @classmethod
 61    def validate_status(cls, a: object) -> BookingStatus:
 62        """
 63        Ensure the status value is a valid BookingStatus.
 64        """
 65        if isinstance(a, BookingStatus):
 66            return a
 67        try:
 68            return BookingStatus(a)
 69        except ValueError:
 70            allowed = ", ".join(t.value for t in BookingStatus)
 71            raise ValueError(f"Invalid status '{a}'. Must be one of: {allowed}")
 72
 73    @field_validator("recurrenceFrequency", mode="before")
 74    @classmethod
 75    def validate_recurrence_frequency(cls, a: object) -> RecurrenceFrequency:
 76        """
 77        Ensure the recurrenceFrequency value is a valid RecurrenceFrequency.
 78        """
 79        if isinstance(a, RecurrenceFrequency):
 80            return a
 81        try:
 82            return RecurrenceFrequency(a)
 83        except ValueError:
 84            allowed = ", ".join(t.value for t in RecurrenceFrequency)
 85            raise ValueError(f"Invalid recurrence '{a}'. Must be one of: {allowed}")
 86
 87    @field_validator("submittedByRole", mode="before")
 88    @classmethod
 89    def validate_submitted_by_role(cls, a: object) -> UserRole:
 90        """
 91        Ensure the submittedByRole value is a valid UserRole.
 92        """
 93        if isinstance(a, UserRole):
 94            return a
 95        try:
 96            return UserRole(a)
 97        except ValueError:
 98            allowed = ", ".join(role.value for role in UserRole)
 99            raise ValueError(
100                f"Invalid submittedByRole '{a}'. Must be one of: {allowed}"
101            )
102
103    @model_validator(mode="after")
104    def validate_recurrence_end_date(self) -> "Booking":
105        """
106        Weekly bookings must supply a recurrenceEndDate.
107        """
108        if (
109            self.recurrenceFrequency == RecurrenceFrequency.WEEKLY
110            and self.recurrenceEndDate is None
111        ):
112            raise ValueError(
113                "recurrenceEndDate must not be None when recurrenceFrequency is 'weekly'."
114            )
115        return self

Represents the room booking request made by a given user.

id: Optional[int]
userID: uuid.UUID
submittedByRole: UserRole
roomID: int
status: BookingStatus
recurrenceFrequency: RecurrenceFrequency
recurrenceEndDate: Optional[datetime.date]
createdAt: datetime.datetime
timeSlots: sqlalchemy.orm.base.Mapped[typing.List[TimeSlot]]
@field_validator('status', mode='before')
@classmethod
def validate_status(cls, a: object) -> BookingStatus:
59    @field_validator("status", mode="before")
60    @classmethod
61    def validate_status(cls, a: object) -> BookingStatus:
62        """
63        Ensure the status value is a valid BookingStatus.
64        """
65        if isinstance(a, BookingStatus):
66            return a
67        try:
68            return BookingStatus(a)
69        except ValueError:
70            allowed = ", ".join(t.value for t in BookingStatus)
71            raise ValueError(f"Invalid status '{a}'. Must be one of: {allowed}")

Ensure the status value is a valid BookingStatus.

@field_validator('recurrenceFrequency', mode='before')
@classmethod
def validate_recurrence_frequency(cls, a: object) -> RecurrenceFrequency:
73    @field_validator("recurrenceFrequency", mode="before")
74    @classmethod
75    def validate_recurrence_frequency(cls, a: object) -> RecurrenceFrequency:
76        """
77        Ensure the recurrenceFrequency value is a valid RecurrenceFrequency.
78        """
79        if isinstance(a, RecurrenceFrequency):
80            return a
81        try:
82            return RecurrenceFrequency(a)
83        except ValueError:
84            allowed = ", ".join(t.value for t in RecurrenceFrequency)
85            raise ValueError(f"Invalid recurrence '{a}'. Must be one of: {allowed}")

Ensure the recurrenceFrequency value is a valid RecurrenceFrequency.

@field_validator('submittedByRole', mode='before')
@classmethod
def validate_submitted_by_role(cls, a: object) -> UserRole:
 87    @field_validator("submittedByRole", mode="before")
 88    @classmethod
 89    def validate_submitted_by_role(cls, a: object) -> UserRole:
 90        """
 91        Ensure the submittedByRole value is a valid UserRole.
 92        """
 93        if isinstance(a, UserRole):
 94            return a
 95        try:
 96            return UserRole(a)
 97        except ValueError:
 98            allowed = ", ".join(role.value for role in UserRole)
 99            raise ValueError(
100                f"Invalid submittedByRole '{a}'. Must be one of: {allowed}"
101            )

Ensure the submittedByRole value is a valid UserRole.

@model_validator(mode='after')
def validate_recurrence_end_date(self) -> Booking:
103    @model_validator(mode="after")
104    def validate_recurrence_end_date(self) -> "Booking":
105        """
106        Weekly bookings must supply a recurrenceEndDate.
107        """
108        if (
109            self.recurrenceFrequency == RecurrenceFrequency.WEEKLY
110            and self.recurrenceEndDate is None
111        ):
112            raise ValueError(
113                "recurrenceEndDate must not be None when recurrenceFrequency is 'weekly'."
114            )
115        return self

Weekly bookings must supply a recurrenceEndDate.

class NotificationType(builtins.str, enum.Enum):
20class NotificationType(str, enum.Enum):
21    """
22    Allowed notification type values.
23    """
24
25    APPROVED = "approved"
26    DENIED = "denied"
27    CANCELLED = "cancelled"

Allowed notification type values.

APPROVED = <NotificationType.APPROVED: 'approved'>
DENIED = <NotificationType.DENIED: 'denied'>
CANCELLED = <NotificationType.CANCELLED: 'cancelled'>
class Notification(sqlalchemy.orm.decl_api._DynamicAttributesType, sqlalchemy.inspection.Inspectable[sqlalchemy.orm.mapper.Mapper[typing.Any]]):
30class Notification(SQLModel, table=True):
31    """
32    Represents an in-app notification sent to a user about their booking.
33    """
34
35    id: Optional[int] = Field(default=None, primary_key=True)
36    userID: UUID = Field(foreign_key="user.id", nullable=False)
37    bookingID: int = Field(foreign_key="booking.id", nullable=False)
38    message: str = Field(nullable=False)
39    type: NotificationType = Field(nullable=False)
40    isRead: bool = Field(default=False)
41    createdAt: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
42
43    @field_validator("type", mode="before")
44    @classmethod
45    def validate_type(cls, a: object) -> NotificationType:
46        """
47        Ensure the type value is a valid NotificationType.
48        """
49        if isinstance(a, NotificationType):
50            return a
51        try:
52            return NotificationType(a)
53        except ValueError:
54            allowed = ", ".join(t.value for t in NotificationType)
55            raise ValueError(
56                f"Invalid Notification Type '{a}'. Must be one of: {allowed}"
57            )

Represents an in-app notification sent to a user about their booking.

id: Optional[int]
userID: uuid.UUID
bookingID: int
message: str
isRead: bool
createdAt: datetime.datetime
@field_validator('type', mode='before')
@classmethod
def validate_type(cls, a: object) -> NotificationType:
43    @field_validator("type", mode="before")
44    @classmethod
45    def validate_type(cls, a: object) -> NotificationType:
46        """
47        Ensure the type value is a valid NotificationType.
48        """
49        if isinstance(a, NotificationType):
50            return a
51        try:
52            return NotificationType(a)
53        except ValueError:
54            allowed = ", ".join(t.value for t in NotificationType)
55            raise ValueError(
56                f"Invalid Notification Type '{a}'. Must be one of: {allowed}"
57            )

Ensure the type value is a valid NotificationType.