hku-class/backend/app/db/models.py
2026-04-12 21:56:08 +08:00

418 lines
18 KiB
Python

import json
from datetime import datetime
from sqlalchemy import String, Text, Integer, DateTime, Boolean, ForeignKey, 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)
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()
)
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"
)
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")