# app/models.py import enum import secrets # For generating invite codes from datetime import datetime, timedelta, timezone # For invite expiry from sqlalchemy import ( Column, Integer, String, DateTime, ForeignKey, Boolean, Enum as SAEnum, # Renamed to avoid clash with Python's enum UniqueConstraint, Index, # Added for invite code index DDL, event, delete, # Added for potential cascade delete if needed (though FK handles it) func, # Added for func.count() text as sa_text # For raw SQL in index definition if needed ) from sqlalchemy.orm import relationship # Removed func import as it's imported above # from sqlalchemy.sql import func # For server_default=func.now() from .database import Base # Import Base from database setup # --- Enums --- class UserRoleEnum(enum.Enum): owner = "owner" member = "member" # --- User Model --- class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) password_hash = Column(String, nullable=False) # Column name used in CRUD name = Column(String, index=True, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) # --- Relationships --- # Groups created by this user created_groups = relationship("Group", back_populates="creator") # Links to Group.creator # Association object for group membership (many-to-many) group_associations = relationship("UserGroup", back_populates="user", cascade="all, delete-orphan") # Links to UserGroup.user # Invites created by this user (one-to-many) created_invites = relationship("Invite", back_populates="creator") # Links to Invite.creator # Optional relationships for items/lists (Add later) # added_items = relationship("Item", foreign_keys="[Item.added_by_id]", back_populates="added_by_user") # completed_items = relationship("Item", foreign_keys="[Item.completed_by_id]", back_populates="completed_by_user") # expense_shares = relationship("ExpenseShare", back_populates="user") # created_lists = relationship("List", foreign_keys="[List.created_by_id]", back_populates="creator") # --- Group Model --- class Group(Base): __tablename__ = "groups" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True, nullable=False) created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False) # FK to User table created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) # --- Relationships --- # The user who created this group (many-to-one) creator = relationship("User", back_populates="created_groups") # Links to User.created_groups # Association object for group membership (one-to-many) member_associations = relationship("UserGroup", back_populates="group", cascade="all, delete-orphan") # Links to UserGroup.group # Invites belonging to this group (one-to-many) invites = relationship("Invite", back_populates="group", cascade="all, delete-orphan") # Links to Invite.group # Lists belonging to this group (Add later) # lists = relationship("List", back_populates="group") # --- UserGroup Association Model (Many-to-Many link) --- class UserGroup(Base): __tablename__ = "user_groups" # Ensure a user cannot be in the same group twice __table_args__ = (UniqueConstraint('user_id', 'group_id', name='uq_user_group'),) id = Column(Integer, primary_key=True, index=True) # Surrogate primary key user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) # FK to User group_id = Column(Integer, ForeignKey("groups.id", ondelete="CASCADE"), nullable=False) # FK to Group role = Column(SAEnum(UserRoleEnum, name="userroleenum", create_type=True), nullable=False, default=UserRoleEnum.member) # Use Enum, ensure type is created joined_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) # --- Relationships --- # Link back to User (many-to-one from the perspective of this table row) user = relationship("User", back_populates="group_associations") # Links to User.group_associations # Link back to Group (many-to-one from the perspective of this table row) group = relationship("Group", back_populates="member_associations") # Links to Group.member_associations # --- Invite Model --- class Invite(Base): __tablename__ = "invites" # Ensure unique codes *within active invites* using a partial index (PostgreSQL specific) # If not using PostgreSQL or need simpler logic, a simple unique=True on 'code' works, # but doesn't allow reusing old codes once deactivated. __table_args__ = ( Index( 'ix_invites_active_code', 'code', unique=True, postgresql_where=sa_text('is_active = true') # Partial index condition ), ) id = Column(Integer, primary_key=True, index=True) # Generate a secure random code by default code = Column(String, unique=False, index=True, nullable=False, default=lambda: secrets.token_urlsafe(16)) # Index helps lookup, uniqueness handled by partial index group_id = Column(Integer, ForeignKey("groups.id", ondelete="CASCADE"), nullable=False) # FK to Group created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False) # FK to User created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) # Set default expiry (e.g., 7 days from creation) expires_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) + timedelta(days=7)) is_active = Column(Boolean, default=True, nullable=False) # To mark as used/invalid # --- Relationships --- # Link back to the Group this invite is for (many-to-one) group = relationship("Group", back_populates="invites") # Links to Group.invites # Link back to the User who created the invite (many-to-one) creator = relationship("User", back_populates="created_invites") # Links to User.created_invites # --- Models for Lists, Items, Expenses (Add later) --- # class List(Base): ... # class Item(Base): ... # class Expense(Base): ... # class ExpenseShare(Base): ...