Coverage for app/backend/src/couchers/models/discussions.py: 100%
113 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +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
24 id: Mapped[int] = mapped_column(
25 BigInteger, communities_seq, primary_key=True, server_default=communities_seq.next_value(), init=False
26 )
28 title: Mapped[str] = mapped_column(String)
29 content: Mapped[str] = mapped_column(String)
30 thread_id: Mapped[int] = mapped_column(ForeignKey("threads.id"), unique=True)
31 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
32 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
33 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
34 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
36 creator_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
37 owner_cluster_id: Mapped[int] = mapped_column(ForeignKey("clusters.id"), index=True)
39 slug: Mapped[str] = column_property(func.slugify(title))
41 thread: Mapped[Thread] = relationship(init=False, backref="discussion", uselist=False)
43 subscribers: Mapped[list[User]] = relationship(
44 init=False, backref="discussions", secondary="discussion_subscriptions", viewonly=True
45 )
47 creator_user: Mapped[User] = relationship(
48 init=False, backref="created_discussions", foreign_keys="Discussion.creator_user_id"
49 )
50 owner_cluster: Mapped[Cluster] = relationship(init=False, back_populates="owned_discussions", uselist=False)
53class DiscussionSubscription(Base, kw_only=True):
54 """
55 users subscriptions to discussions
56 """
58 __tablename__ = "discussion_subscriptions"
59 __table_args__ = (UniqueConstraint("discussion_id", "user_id"),)
61 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
63 user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
64 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
65 joined: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
66 left: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
68 user: Mapped[User] = relationship(init=False, backref="discussion_subscriptions")
69 discussion: Mapped[Discussion] = relationship(init=False, backref="discussion_subscriptions")
72class Thread(Base, kw_only=True):
73 """
74 Thread
75 """
77 __tablename__ = "threads"
79 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
81 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
82 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
85class Comment(Base, kw_only=True):
86 """
87 Comment
88 """
90 __tablename__ = "comments"
91 __moderation_author_column__ = "author_user_id"
92 __moderation_object_type__ = ModerationObjectType.comment
94 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
96 thread_id: Mapped[int] = mapped_column(ForeignKey("threads.id"), index=True)
97 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
98 author_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
99 content: Mapped[str] = mapped_column(String) # CommonMark without images
100 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
101 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
102 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
104 thread: Mapped[Thread] = relationship(init=False, backref="comments")
107class Reply(Base, kw_only=True):
108 """
109 Reply
110 """
112 __tablename__ = "replies"
113 __moderation_author_column__ = "author_user_id"
114 __moderation_object_type__ = ModerationObjectType.reply
116 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
118 comment_id: Mapped[int] = mapped_column(ForeignKey("comments.id"), index=True)
119 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
120 author_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
121 content: Mapped[str] = mapped_column(String) # CommonMark without images
122 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
123 deleted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
124 last_edited: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None)
126 comment: Mapped[Comment] = relationship(init=False, backref="replies")
129class ContentChangeType(enum.Enum):
130 edit = enum.auto()
131 delete = enum.auto()
134class DiscussionVersion(Base, kw_only=True):
135 """
136 audit log of edits and deletions to discussions
137 """
139 __tablename__ = "discussion_versions"
141 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
143 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
144 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
145 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
146 old_title: Mapped[str | None] = mapped_column(String, default=None)
147 new_title: Mapped[str | None] = mapped_column(String, default=None)
148 old_content: Mapped[str | None] = mapped_column(String, default=None)
149 new_content: Mapped[str | None] = mapped_column(String, default=None)
150 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
152 discussion: Mapped[Discussion] = relationship(init=False, backref="versions")
153 editor_user: Mapped[User] = relationship(
154 init=False, backref="edited_discussions", foreign_keys="DiscussionVersion.editor_user_id"
155 )
158class CommentVersion(Base, kw_only=True):
159 """
160 audit log of edits and deletions to comments
161 """
163 __tablename__ = "comment_versions"
165 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
167 comment_id: Mapped[int] = mapped_column(ForeignKey("comments.id"), index=True)
168 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
169 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
170 old_content: Mapped[str | None] = mapped_column(String, default=None)
171 new_content: Mapped[str | None] = mapped_column(String, default=None)
172 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
174 comment: Mapped[Comment] = relationship(init=False, backref="versions")
175 editor_user: Mapped[User] = relationship(
176 init=False, backref="edited_comments", foreign_keys="CommentVersion.editor_user_id"
177 )
180class ReplyVersion(Base, kw_only=True):
181 """
182 audit log of edits and deletions to replies
183 """
185 __tablename__ = "reply_versions"
187 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
189 reply_id: Mapped[int] = mapped_column(ForeignKey("replies.id"), index=True)
190 editor_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
191 change_type: Mapped[ContentChangeType] = mapped_column(Enum(ContentChangeType))
192 old_content: Mapped[str | None] = mapped_column(String, default=None)
193 new_content: Mapped[str | None] = mapped_column(String, default=None)
194 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
196 reply: Mapped[Reply] = relationship(init=False, backref="versions")
197 editor_user: Mapped[User] = relationship(
198 init=False, backref="edited_replies", foreign_keys="ReplyVersion.editor_user_id"
199 )
202class ClusterDiscussionAssociation(Base, kw_only=True):
203 """
204 discussions related to clusters
205 """
207 __tablename__ = "cluster_discussion_associations"
208 __table_args__ = (UniqueConstraint("discussion_id", "cluster_id"),)
210 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
212 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True)
213 cluster_id: Mapped[int] = mapped_column(ForeignKey("clusters.id"), index=True)
215 discussion: Mapped[Discussion] = relationship(init=False, backref="cluster_discussion_associations")
216 cluster: Mapped[Cluster] = relationship(init=False, backref="cluster_discussion_associations")