Files
org-parking/database/models.py
Stefano Manfredi c74a0ed350 Initial commit: Parking Manager
Features:
- Manager-centric parking spot management
- Fair assignment algorithm (parking/presence ratio)
- Presence tracking calendar
- Closing days (specific & weekly recurring)
- Guarantees and exclusions
- Authelia/LLDAP integration for SSO

Stack:
- FastAPI backend
- SQLite database
- Vanilla JS frontend
- Docker deployment
2025-11-26 23:37:50 +00:00

233 lines
8.7 KiB
Python

"""
SQLAlchemy ORM Models
Clean, focused data models for parking management
"""
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index
from sqlalchemy.orm import relationship, declarative_base
Base = declarative_base()
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(Text, nullable=False, default="employee") # admin, manager, employee
office_id = Column(Text, ForeignKey("offices.id"))
# Manager-specific fields (only relevant for role='manager')
manager_parking_quota = Column(Integer, default=0) # Number of spots this manager controls
manager_spot_prefix = Column(Text) # Letter prefix for spots: A, B, C, etc.
# User preferences
week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ..., 6=Saturday
# Notification preferences
notify_weekly_parking = Column(Integer, default=1) # Weekly parking summary (Friday at 12)
notify_daily_parking = Column(Integer, default=1) # 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(Integer, default=1) # Immediate notification on assignment changes
created_at = Column(Text)
updated_at = Column(Text)
# 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")
managed_offices = relationship("OfficeMembership", back_populates="user", cascade="all, delete-orphan")
__table_args__ = (
Index('idx_user_email', 'email'),
Index('idx_user_office', 'office_id'),
)
class Office(Base):
"""Office locations - containers for grouping employees"""
__tablename__ = "offices"
id = Column(Text, primary_key=True)
name = Column(Text, nullable=False)
location = Column(Text)
# Note: parking_spots removed - spots are now managed at manager level
created_at = Column(Text)
updated_at = Column(Text)
# Relationships
users = relationship("User", back_populates="office")
managers = relationship("OfficeMembership", back_populates="office", cascade="all, delete-orphan")
class OfficeMembership(Base):
"""Manager-Office relationship (which managers manage which offices)"""
__tablename__ = "office_memberships"
id = Column(Text, primary_key=True)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
created_at = Column(Text)
# Relationships
user = relationship("User", back_populates="managed_offices")
office = relationship("Office", back_populates="managers")
__table_args__ = (
Index('idx_membership_user', 'user_id'),
Index('idx_membership_office', 'office_id'),
Index('idx_membership_unique', 'user_id', 'office_id', unique=True),
)
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(Text, nullable=False) # YYYY-MM-DD
status = Column(Text, nullable=False) # present, remote, absent
created_at = Column(Text)
updated_at = Column(Text)
# 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 managers"""
__tablename__ = "daily_parking_assignments"
id = Column(Text, primary_key=True)
date = Column(Text, nullable=False) # YYYY-MM-DD
spot_id = Column(Text, nullable=False) # A1, A2, B1, B2, etc. (prefix from manager)
user_id = Column(Text, ForeignKey("users.id", ondelete="SET NULL"))
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) # Manager who owns the spot
created_at = Column(Text)
# Relationships
user = relationship("User", back_populates="assignments", foreign_keys=[user_id])
manager = relationship("User", foreign_keys=[manager_id])
__table_args__ = (
Index('idx_assignment_manager_date', 'manager_id', 'date'),
Index('idx_assignment_user', 'user_id'),
Index('idx_assignment_date_spot', 'date', 'spot_id'),
)
class ManagerClosingDay(Base):
"""Specific date closing days for a manager's offices (holidays, special closures)"""
__tablename__ = "manager_closing_days"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
date = Column(Text, nullable=False) # YYYY-MM-DD
reason = Column(Text)
# Relationships
manager = relationship("User")
__table_args__ = (
Index('idx_closing_manager_date', 'manager_id', 'date', unique=True),
)
class ManagerWeeklyClosingDay(Base):
"""Weekly recurring closing days for a manager's offices (e.g., Saturday and Sunday)"""
__tablename__ = "manager_weekly_closing_days"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday
# Relationships
manager = relationship("User")
__table_args__ = (
Index('idx_weekly_closing_manager_day', 'manager_id', 'weekday', unique=True),
)
class ParkingGuarantee(Base):
"""Users guaranteed a parking spot when present (set by manager)"""
__tablename__ = "parking_guarantees"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
start_date = Column(Text) # Optional: YYYY-MM-DD (null = no start limit)
end_date = Column(Text) # Optional: YYYY-MM-DD (null = no end limit)
created_at = Column(Text)
# Relationships
manager = relationship("User", foreign_keys=[manager_id])
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_guarantee_manager_user', 'manager_id', 'user_id', unique=True),
)
class ParkingExclusion(Base):
"""Users excluded from parking assignment (set by manager)"""
__tablename__ = "parking_exclusions"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
start_date = Column(Text) # Optional: YYYY-MM-DD (null = no start limit)
end_date = Column(Text) # Optional: YYYY-MM-DD (null = no end limit)
created_at = Column(Text)
# Relationships
manager = relationship("User", foreign_keys=[manager_id])
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_exclusion_manager_user', 'manager_id', 'user_id', unique=True),
)
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(Text, nullable=False) # presence_reminder, weekly_parking, daily_parking, parking_change
reference_date = Column(Text) # The date/week this notification refers to (YYYY-MM-DD or YYYY-Www)
sent_at = Column(Text, nullable=False)
__table_args__ = (
Index('idx_notification_user_type_date', 'user_id', 'notification_type', 'reference_date'),
)
class NotificationQueue(Base):
"""Queue for pending notifications (for immediate parking change notifications)"""
__tablename__ = "notification_queue"
id = Column(Text, primary_key=True)
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
notification_type = Column(Text, nullable=False) # parking_change
subject = Column(Text, nullable=False)
body = Column(Text, nullable=False)
created_at = Column(Text, nullable=False)
sent_at = Column(Text) # null = not sent yet
__table_args__ = (
Index('idx_queue_pending', 'sent_at'),
)