Coverage for app/backend/src/couchers/models/discussions.py: 100%
116 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
1import enum
2from datetime import datetime
3from typing import TYPE_CHECKING
5from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, String, UniqueConstraint, func
6from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship
8from couchers.models.base import Base, communities_seq
9from couchers.models.moderation import ModerationObjectType
11if TYPE_CHECKING:
12 from couchers.models import Cluster, User
15class Discussion(Base, kw_only=True):
16 """
17 forum board
18 """
20 __tablename__ = "discussions"
21 __moderation_author_column__ = "creator_user_id"
22 __moderation_object_type__ = ModerationObjectType.discussion
23 __moderation_has_own_visibility_mechanism__ = False
25 id: Mapped[int] = mapped_column(
26 BigInteger, communities_seq, primary_key=True, server_default=communities_seq.next_value(), init=False
27 )
29 title: Mapped[str] = mapped_column(String)
30 content: Mapped[str] = mapped_column(String)
31 thread_id: Mapped[int] = mapped_column(ForeignKey("threads.id"), unique=True)
32 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
33 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
34 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
35 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
37 creator_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
38 owner_cluster_id: Mapped[int] = mapped_column(ForeignKey("clusters.id"), index=True)
40 slug: Mapped[str] = column_property(func.slugify(title))
42 thread: Mapped[Thread] = relationship(init=False, backref="discussion", uselist=False)
44 subscribers: Mapped[list[User]] = relationship(
45 init=False, backref="discussions", secondary="discussion_subscriptions", viewonly=True
46 )
48 creator_user: Mapped[User] = relationship(
49 init=False, backref="created_discussions", foreign_keys="Discussion.creator_user_id"
50 )
51 owner_cluster: Mapped[Cluster] = relationship(init=False, back_populates="owned_discussions", uselist=False)
54class DiscussionSubscription(Base, kw_only=True):
55 """
56 users subscriptions to discussions
57 """
59 __tablename__ = "discussion_subscriptions"
60 __table_args__ = (UniqueConstraint("discussion_id", "user_id"),)
62 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
64 user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
65 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
66 joined: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
67 left: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
69 user: Mapped[User] = relationship(init=False, backref="discussion_subscriptions")
70 discussion: Mapped[Discussion] = relationship(init=False, backref="discussion_subscriptions")
73class Thread(Base, kw_only=True):
74 """
75 Thread
76 """
78 __tablename__ = "threads"
80 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
82 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
83 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
86class Comment(Base, kw_only=True):
87 """
88 Comment
89 """
91 __tablename__ = "comments"
92 __moderation_author_column__ = "author_user_id"
93 __moderation_object_type__ = ModerationObjectType.comment
94 __moderation_has_own_visibility_mechanism__ = False
96 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
98 thread_id: Mapped[int] = mapped_column(ForeignKey("threads.id"), index=True)
99 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
100 author_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
101 content: Mapped[str] = mapped_column(String) # CommonMark without images
102 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
103 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
104 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
106 thread: Mapped[Thread] = relationship(init=False, backref="comments")
109class Reply(Base, kw_only=True):
110 """
111 Reply
112 """
114 __tablename__ = "replies"
115 __moderation_author_column__ = "author_user_id"
116 __moderation_object_type__ = ModerationObjectType.reply
117 __moderation_has_own_visibility_mechanism__ = False
119 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
121 comment_id: Mapped[int] = mapped_column(ForeignKey("comments.id"), index=True)
122 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
123 author_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
124 content: Mapped[str] = mapped_column(String) # CommonMark without images
125 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
126 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
127 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
129 comment: Mapped[Comment] = relationship(init=False, backref="replies")
132class ContentChangeType(enum.Enum):
133 edit = enum.auto()
134 delete = enum.auto()
137class DiscussionVersion(Base, kw_only=True):
138 """
139 audit log of edits and deletions to discussions
140 """
142 __tablename__ = "discussion_versions"
144 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
146 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
147 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
148 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
149 old_title: Mapped[str | None] = mapped_column(String, default=None)
150 new_title: Mapped[str | None] = mapped_column(String, default=None)
151 old_content: Mapped[str | None] = mapped_column(String, default=None)
152 new_content: Mapped[str | None] = mapped_column(String, default=None)
153 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
155 discussion: Mapped[Discussion] = relationship(init=False, backref="versions")
156 editor_user: Mapped[User] = relationship(
157 init=False, backref="edited_discussions", foreign_keys="DiscussionVersion.editor_user_id"
158 )
161class CommentVersion(Base, kw_only=True):
162 """
163 audit log of edits and deletions to comments
164 """
166 __tablename__ = "comment_versions"
168 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
170 comment_id: Mapped[int] = mapped_column(ForeignKey("comments.id"), index=True)
171 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
172 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
173 old_content: Mapped[str | None] = mapped_column(String, default=None)
174 new_content: Mapped[str | None] = mapped_column(String, default=None)
175 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
177 comment: Mapped[Comment] = relationship(init=False, backref="versions")
178 editor_user: Mapped[User] = relationship(
179 init=False, backref="edited_comments", foreign_keys="CommentVersion.editor_user_id"
180 )
183class ReplyVersion(Base, kw_only=True):
184 """
185 audit log of edits and deletions to replies
186 """
188 __tablename__ = "reply_versions"
190 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
192 reply_id: Mapped[int] = mapped_column(ForeignKey("replies.id"), index=True)
193 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
194 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
195 old_content: Mapped[str | None] = mapped_column(String, default=None)
196 new_content: Mapped[str | None] = mapped_column(String, default=None)
197 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
199 reply: Mapped[Reply] = relationship(init=False, backref="versions")
200 editor_user: Mapped[User] = relationship(
201 init=False, backref="edited_replies", foreign_keys="ReplyVersion.editor_user_id"
202 )
205class ClusterDiscussionAssociation(Base, kw_only=True):
206 """
207 discussions related to clusters
208 """
210 __tablename__ = "cluster_discussion_associations"
211 __table_args__ = (UniqueConstraint("discussion_id", "cluster_id"),)
213 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
215 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
216 cluster_id: Mapped[int] = mapped_column(ForeignKey("clusters.id"), index=True)
218 discussion: Mapped[Discussion] = relationship(init=False, backref="cluster_discussion_associations")
219 cluster: Mapped[Cluster] = relationship(init=False, backref="cluster_discussion_associations")