495 lines
21 KiB
Python
495 lines
21 KiB
Python
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
|
|
|
|
memberships: Mapped[list["ClassMembership"]] = relationship(
|
|
"ClassMembership", back_populates="class_", cascade="all, delete-orphan"
|
|
)
|
|
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"
|
|
)
|
|
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 | None] = mapped_column(Text, nullable=True)
|
|
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 | teacher | student
|
|
role: Mapped[str] = mapped_column(String(20), default="student", nullable=False)
|
|
# status: inactive | approved | disabled
|
|
status: Mapped[str] = mapped_column(String(20), default="inactive", nullable=False)
|
|
|
|
# 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)
|
|
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()
|
|
)
|
|
|
|
memberships: Mapped[list["ClassMembership"]] = relationship(
|
|
"ClassMembership", back_populates="user", cascade="all, delete-orphan"
|
|
)
|
|
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
|
|
|
|
def get_membership(self, class_id: int | None) -> "ClassMembership | None":
|
|
if class_id is None:
|
|
return None
|
|
return next((m for m in self.memberships if m.class_id == class_id), None)
|
|
|
|
def get_default_membership(self) -> "ClassMembership | None":
|
|
active_membership = getattr(self, "_active_membership", None)
|
|
if active_membership is not None:
|
|
return active_membership
|
|
if len(self.memberships) == 1:
|
|
return self.memberships[0]
|
|
return self.memberships[0] if self.memberships else None
|
|
|
|
def set_active_membership(self, class_id: int | None = None):
|
|
setattr(self, "_active_membership", self.get_membership(class_id))
|
|
|
|
|
|
class ClassMembership(Base):
|
|
__tablename__ = "class_memberships"
|
|
|
|
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
|
|
)
|
|
class_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("classes.id"), nullable=False, index=True
|
|
)
|
|
membership_role: Mapped[str] = mapped_column(
|
|
String(20), default="student", nullable=False
|
|
)
|
|
committee_role: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
class_permissions: 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()
|
|
)
|
|
|
|
user: Mapped["User"] = relationship("User", back_populates="memberships")
|
|
class_: Mapped["Class_"] = relationship("Class_", back_populates="memberships")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "class_id", name="uq_class_membership_user_class"),
|
|
)
|
|
|
|
def get_class_permissions(self) -> list[str]:
|
|
if not self.class_permissions:
|
|
return []
|
|
try:
|
|
return json.loads(self.class_permissions)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return []
|
|
|
|
def set_class_permissions(self, permissions: list[str]):
|
|
self.class_permissions = (
|
|
json.dumps(sorted(set(permissions)), ensure_ascii=False)
|
|
if permissions
|
|
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 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")
|