140 lines
6.2 KiB
Python
140 lines
6.2 KiB
Python
# 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): ... |