Files
org-parking/database/models.py

273 lines
10 KiB
Python

"""
SQLAlchemy ORM Models
Clean, focused data models for parking management
"""
import enum
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index, Enum, Date, DateTime, Boolean
from sqlalchemy.orm import relationship, declarative_base
from datetime import datetime, date
Base = declarative_base()
class UserRole(str, enum.Enum):
ADMIN = "admin"
MANAGER = "manager"
EMPLOYEE = "employee"
class PresenceStatus(str, enum.Enum):
PRESENT = "present"
REMOTE = "remote"
ABSENT = "absent"
BUSINESS_TRIP = "business_trip"
class NotificationType(str, enum.Enum):
PRESENCE_REMINDER = "presence_reminder"
WEEKLY_PARKING = "weekly_parking"
DAILY_PARKING = "daily_parking"
PARKING_CHANGE = "parking_change"
class WeekDay(enum.IntEnum):
# Matches Python's calendar (0=Monday)? No!
# The current DB convention in ManagerWeeklyClosingDay seems to be 0=Sunday based on comment:
# "0=Sunday, 1=Monday, ..., 6=Saturday"
# To keep consistency with existing logic comments, we'll stick to that,
# OR we can switch to standard Python (0=Monday).
# Plan said: "IntEnum matching DB convention (0=Sunday, 1=Monday, ...)"
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
class Office(Base):
"""Organization units that have parking spots"""
__tablename__ = "offices"
id = Column(Text, primary_key=True)
name = Column(Text, nullable=False)
parking_quota = Column(Integer, default=0)
spot_prefix = Column(Text) # Letter prefix: A, B, C
# Booking Window Settings (Batch Assignment)
booking_window_enabled = Column(Boolean, default=False)
booking_window_end_hour = Column(Integer, default=18) # 0-23
booking_window_end_minute = Column(Integer, default=0) # 0-59
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
users = relationship("User", back_populates="office")
closing_days = relationship("OfficeClosingDay", back_populates="office", cascade="all, delete-orphan")
weekly_closing_days = relationship("OfficeWeeklyClosingDay", back_populates="office", cascade="all, delete-orphan")
spots = relationship("OfficeSpot", back_populates="office", cascade="all, delete-orphan")
class User(Base):
"""Application users"""
__tablename__ = "users"
id = Column(Text, primary_key=True)
email = Column(Text, unique=True, nullable=False)
password_hash = Column(Text)
name = Column(Text)
role = Column(Enum(UserRole, values_callable=lambda obj: [e.value for e in obj]), nullable=False, default=UserRole.EMPLOYEE)
office_id = Column(Text, ForeignKey("offices.id")) # Which office this user belongs to
# User preferences
week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ... (Matches WeekDay logic)
# Notification preferences
notify_weekly_parking = Column(Boolean, default=True) # Weekly parking summary (Friday at 12)
notify_daily_parking = Column(Boolean, default=True) # Daily parking reminder
notify_daily_parking_hour = Column(Integer, default=8) # Hour for daily reminder (0-23)
notify_daily_parking_minute = Column(Integer, default=0) # Minute for daily reminder (0-59)
notify_parking_changes = Column(Boolean, default=True) # Immediate notification on assignment changes
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
office = relationship("Office", back_populates="users")
presences = relationship("UserPresence", back_populates="user", cascade="all, delete-orphan")
assignments = relationship("DailyParkingAssignment", back_populates="user", foreign_keys="DailyParkingAssignment.user_id")
__table_args__ = (
Index('idx_user_email', 'email'),
Index('idx_user_office', 'office_id'),
)
class UserPresence(Base):
"""Daily presence records"""
__tablename__ = "user_presences"
id = Column(Text, primary_key=True)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
date = Column(Date, nullable=False)
status = Column(Enum(PresenceStatus, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # present, remote, absent
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="presences")
__table_args__ = (
Index('idx_presence_user_date', 'user_id', 'date', unique=True),
Index('idx_presence_date', 'date'),
)
class DailyParkingAssignment(Base):
"""Parking spot assignments per day - spots belong to offices"""
__tablename__ = "daily_parking_assignments"
id = Column(Text, primary_key=True)
date = Column(Date, nullable=False)
spot_id = Column(Text, ForeignKey("office_spots.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, ForeignKey("users.id", ondelete="SET NULL"))
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) # Office that owns the spot
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="assignments", foreign_keys=[user_id])
office = relationship("Office")
spot = relationship("OfficeSpot", back_populates="assignments")
__table_args__ = (
Index('idx_assignment_office_date', 'office_id', 'date'),
Index('idx_assignment_user', 'user_id'),
Index('idx_assignment_date_spot', 'date', 'spot_id'),
)
class OfficeClosingDay(Base):
"""Specific date closing days for an office's parking pool (holidays, special closures)"""
__tablename__ = "office_closing_days"
id = Column(Text, primary_key=True)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
date = Column(Date, nullable=False)
end_date = Column(Date)
reason = Column(Text)
# Relationships
office = relationship("Office", back_populates="closing_days")
__table_args__ = (
Index('idx_closing_office_date', 'office_id', 'date', unique=True),
)
class OfficeWeeklyClosingDay(Base):
"""Weekly recurring closing days for an office's parking pool (e.g., Saturday and Sunday)"""
__tablename__ = "office_weekly_closing_days"
id = Column(Text, primary_key=True)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday (Matches WeekDay Enum logic)
# Relationships
office = relationship("Office", back_populates="weekly_closing_days")
__table_args__ = (
Index('idx_weekly_closing_office_day', 'office_id', 'weekday', unique=True),
)
class ParkingGuarantee(Base):
"""Users guaranteed a parking spot when present (set by office manager)"""
__tablename__ = "parking_guarantees"
id = Column(Text, primary_key=True)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
start_date = Column(Date) # Optional (null = no start limit)
end_date = Column(Date) # Optional (null = no end limit)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
office = relationship("Office")
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_guarantee_office_user', 'office_id', 'user_id', unique=True),
)
class ParkingExclusion(Base):
"""Users excluded from parking assignment (set by office manager)"""
__tablename__ = "parking_exclusions"
id = Column(Text, primary_key=True)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
start_date = Column(Date) # Optional
end_date = Column(Date) # Optional
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
office = relationship("Office")
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_exclusion_office_user', 'office_id', 'user_id'),
)
class NotificationLog(Base):
"""Log of sent notifications to prevent duplicates"""
__tablename__ = "notification_logs"
id = Column(Text, primary_key=True)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False)
reference_date = Column(Text) # The date/week this notification refers to (YYYY-MM-DD or YYYY-Www) - keeping as Text for flexibility
sent_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
Index('idx_notification_user_type_date', 'user_id', 'notification_type', 'reference_date'),
)
notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # parking_change
subject = Column(Text, nullable=False)
body = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
sent_at = Column(DateTime) # null = not sent yet
__table_args__ = (
Index('idx_queue_pending', 'sent_at'),
)
class OfficeSpot(Base):
"""Specific parking spot definitions (e.g., A1, A2) linked to an office"""
__tablename__ = "office_spots"
id = Column(Text, primary_key=True)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
name = Column(Text, nullable=False) # Display name: A1, A2, etc.
spot_number = Column(Integer, nullable=False) # Numeric part for sorting/filtering (1, 2, 3...)
is_unavailable = Column(Boolean, default=False) # If spot is temporarily out of service
# Relationships
office = relationship("Office", back_populates="spots")
assignments = relationship("DailyParkingAssignment", back_populates="spot", cascade="all, delete-orphan")
__table_args__ = (
Index('idx_office_spot_number', 'office_id', 'spot_number', unique=True),
Index('idx_office_spot_name', 'office_id', 'name', unique=True),
)