import json from datetime import datetime from sqlalchemy import String, Text, Integer, DateTime, Boolean, ForeignKey, Float, Date, func, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base class Class_(Base): __tablename__ = "classes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(100), nullable=False) cohort_year: Mapped[int] = mapped_column(Integer, nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) invite_code: Mapped[str | None] = mapped_column(String(20), unique=True, nullable=True) enabled_modules: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array of module keys created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) # All available modules ALL_MODULES = ["announcements", "directory", "timeline", "assignments", "votes", "schedule", "resources", "fund"] def get_enabled_modules(self) -> list[str]: if not self.enabled_modules: return list(self.ALL_MODULES) try: return json.loads(self.enabled_modules) except (json.JSONDecodeError, TypeError): return list(self.ALL_MODULES) def set_enabled_modules(self, modules: list[str]): self.enabled_modules = json.dumps(modules, ensure_ascii=False) if modules else None members: Mapped[list["User"]] = relationship("User", back_populates="class_") timelines: Mapped[list["Timeline"]] = relationship( "Timeline", back_populates="class_", cascade="all, delete-orphan" ) schedules: Mapped[list["Schedule"]] = relationship( "Schedule", back_populates="class_", cascade="all, delete-orphan" ) announcements: Mapped[list["Announcement"]] = relationship( "Announcement", back_populates="class_", cascade="all, delete-orphan" ) resources: Mapped[list["Resource"]] = relationship( "Resource", back_populates="class_", cascade="all, delete-orphan" ) roster: Mapped[list["StudentRoster"]] = relationship( "StudentRoster", back_populates="class_", cascade="all, delete-orphan" ) assignments: Mapped[list["Assignment"]] = relationship( "Assignment", back_populates="class_", cascade="all, delete-orphan" ) votes: Mapped[list["Vote"]] = relationship( "Vote", back_populates="class_", cascade="all, delete-orphan" ) fund_records: Mapped[list["FundRecord"]] = relationship( "FundRecord", back_populates="class_", cascade="all, delete-orphan" ) class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) password_hash: Mapped[str] = mapped_column(Text, nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) student_id: Mapped[str | None] = mapped_column(String(50), nullable=True, unique=True) # role: super_admin | class_admin | student role: Mapped[str] = mapped_column(String(20), default="student", nullable=False) # status: pending | approved | rejected | disabled status: Mapped[str] = mapped_column(String(20), default="pending", nullable=False) class_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("classes.id"), nullable=True ) class_: Mapped["Class_ | None"] = relationship("Class_", back_populates="members") # Profile industry: Mapped[str | None] = mapped_column(String(100), nullable=True) company: Mapped[str | None] = mapped_column(String(100), nullable=True) position: Mapped[str | None] = mapped_column(String(100), nullable=True) committee_role: Mapped[str | None] = mapped_column(String(50), nullable=True) skills_tags: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array wechat_id: Mapped[str | None] = mapped_column(String(100), nullable=True) phone: Mapped[str | None] = mapped_column(String(20), nullable=True) avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True) bio: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) timeline_posts: Mapped[list["Timeline"]] = relationship( "Timeline", back_populates="author" ) created_assignments: Mapped[list["Assignment"]] = relationship( "Assignment", back_populates="creator" ) assignment_submissions: Mapped[list["AssignmentSubmission"]] = relationship( "AssignmentSubmission", back_populates="student" ) created_votes: Mapped[list["Vote"]] = relationship( "Vote", back_populates="creator" ) def get_skills_list(self) -> list[str]: if not self.skills_tags: return [] try: return json.loads(self.skills_tags) except (json.JSONDecodeError, TypeError): return [] def set_skills_list(self, tags: list[str]): self.skills_tags = json.dumps(tags, ensure_ascii=False) if tags else None class Timeline(Base): __tablename__ = "timelines" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) author_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) content: Mapped[str | None] = mapped_column(Text, nullable=True) image_urls: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="timelines") author: Mapped["User"] = relationship("User", back_populates="timeline_posts") likes: Mapped[list["TimelineLike"]] = relationship( "TimelineLike", back_populates="post", cascade="all, delete-orphan" ) comments: Mapped[list["TimelineComment"]] = relationship( "TimelineComment", back_populates="post", cascade="all, delete-orphan" ) def get_image_urls_list(self) -> list[str]: if not self.image_urls: return [] try: return json.loads(self.image_urls) except (json.JSONDecodeError, TypeError): return [] def set_image_urls_list(self, urls: list[str]): self.image_urls = json.dumps(urls) if urls else None class Schedule(Base): __tablename__ = "schedules" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) # type: course | deadline | activity type: Mapped[str] = mapped_column(String(20), nullable=False) title: Mapped[str] = mapped_column(String(200), nullable=False) start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False) end_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) location: Mapped[str | None] = mapped_column(String(200), nullable=True) description: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="schedules") class Announcement(Base): __tablename__ = "announcements" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) author_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) content: Mapped[str | None] = mapped_column(Text, nullable=True) is_pinned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="announcements") author: Mapped["User"] = relationship("User") class Resource(Base): __tablename__ = "resources" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) uploader_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) file_url: Mapped[str] = mapped_column(Text, nullable=False) file_type: Mapped[str] = mapped_column(String(50), nullable=False) file_size: Mapped[int] = mapped_column(Integer, nullable=False) category: Mapped[str] = mapped_column(String(50), nullable=False) download_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) class_: Mapped["Class_"] = relationship("Class_", back_populates="resources") uploader: Mapped["User"] = relationship("User") class Notification(Base): __tablename__ = "notifications" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False, index=True ) type: Mapped[str] = mapped_column(String(50), nullable=False) title: Mapped[str] = mapped_column(String(200), nullable=False) content: Mapped[str | None] = mapped_column(Text, nullable=True) related_id: Mapped[int | None] = mapped_column(Integer, nullable=True) is_read: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) user: Mapped["User"] = relationship("User") class StudentRoster(Base): __tablename__ = "student_rosters" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) student_id: Mapped[str] = mapped_column(String(50), nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) status: Mapped[str] = mapped_column( String(20), default="unregistered", nullable=False ) user_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("users.id"), nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) class_: Mapped["Class_"] = relationship("Class_", back_populates="roster") user: Mapped["User | None"] = relationship("User") class TimelineLike(Base): __tablename__ = "timeline_likes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) post_id: Mapped[int] = mapped_column( Integer, ForeignKey("timelines.id"), nullable=False, index=True ) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) post: Mapped["Timeline"] = relationship("Timeline", back_populates="likes") user: Mapped["User"] = relationship("User") __table_args__ = ( UniqueConstraint("post_id", "user_id", name="uq_timeline_like"), ) class TimelineComment(Base): __tablename__ = "timeline_comments" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) post_id: Mapped[int] = mapped_column( Integer, ForeignKey("timelines.id"), nullable=False, index=True ) author_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) content: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) post: Mapped["Timeline"] = relationship("Timeline", back_populates="comments") author: Mapped["User"] = relationship("User") class Vote(Base): __tablename__ = "votes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) creator_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) vote_type: Mapped[str] = mapped_column(String(20), default="single", nullable=False) # single | multiple is_anonymous: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) max_choices: Mapped[int] = mapped_column(Integer, default=1, nullable=False) deadline: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) status: Mapped[str] = mapped_column(String(20), default="open", nullable=False) # open | closed created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="votes") creator: Mapped["User"] = relationship("User", back_populates="created_votes") options: Mapped[list["VoteOption"]] = relationship( "VoteOption", back_populates="vote", cascade="all, delete-orphan" ) class VoteOption(Base): __tablename__ = "vote_options" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) vote_id: Mapped[int] = mapped_column( Integer, ForeignKey("votes.id"), nullable=False, index=True ) content: Mapped[str] = mapped_column(String(500), nullable=False) sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False) vote: Mapped["Vote"] = relationship("Vote", back_populates="options") responses: Mapped[list["VoteResponse"]] = relationship( "VoteResponse", back_populates="option", cascade="all, delete-orphan" ) class VoteResponse(Base): __tablename__ = "vote_responses" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) vote_id: Mapped[int] = mapped_column( Integer, ForeignKey("votes.id"), nullable=False, index=True ) option_id: Mapped[int] = mapped_column( Integer, ForeignKey("vote_options.id"), nullable=False, index=True ) voter_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) option: Mapped["VoteOption"] = relationship("VoteOption", back_populates="responses") voter: Mapped["User"] = relationship("User") class Assignment(Base): __tablename__ = "assignments" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) creator_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) deadline: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) attachment_urls: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON array status: Mapped[str] = mapped_column(String(20), default="open", nullable=False) # open | closed created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="assignments") creator: Mapped["User"] = relationship("User", back_populates="created_assignments") submissions: Mapped[list["AssignmentSubmission"]] = relationship( "AssignmentSubmission", back_populates="assignment", cascade="all, delete-orphan" ) def get_attachment_urls_list(self) -> list[str]: if not self.attachment_urls: return [] try: return json.loads(self.attachment_urls) except (json.JSONDecodeError, TypeError): return [] def set_attachment_urls_list(self, urls: list[str]): self.attachment_urls = json.dumps(urls) if urls else None class AssignmentSubmission(Base): __tablename__ = "assignment_submissions" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) assignment_id: Mapped[int] = mapped_column( Integer, ForeignKey("assignments.id"), nullable=False, index=True ) student_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) notes: Mapped[str | None] = mapped_column(Text, nullable=True) file_url: Mapped[str | None] = mapped_column(Text, nullable=True) file_name: Mapped[str | None] = mapped_column(String(255), nullable=True) file_type: Mapped[str | None] = mapped_column(String(50), nullable=True) file_size: Mapped[int | None] = mapped_column(Integer, nullable=True) grade: Mapped[str | None] = mapped_column(String(50), nullable=True) feedback: Mapped[str | None] = mapped_column(Text, nullable=True) graded_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) assignment: Mapped["Assignment"] = relationship("Assignment", back_populates="submissions") student: Mapped["User"] = relationship("User", back_populates="assignment_submissions") class FundRecord(Base): __tablename__ = "fund_records" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) class_id: Mapped[int] = mapped_column( Integer, ForeignKey("classes.id"), nullable=False, index=True ) # type: income | expense type: Mapped[str] = mapped_column(String(20), nullable=False) amount: Mapped[float] = mapped_column(Float, nullable=False) category: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[str | None] = mapped_column(Text, nullable=True) record_date: Mapped[datetime] = mapped_column(Date, nullable=False) recorder_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime, server_default=func.now(), onupdate=func.now() ) class_: Mapped["Class_"] = relationship("Class_", back_populates="fund_records") recorder: Mapped["User"] = relationship("User")