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

1import enum 

2from datetime import datetime 

3from typing import TYPE_CHECKING 

4 

5from sqlalchemy import BigInteger, DateTime, Enum, ForeignKey, String, UniqueConstraint, func 

6from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship 

7 

8from couchers.models.base import Base, communities_seq 

9from couchers.models.moderation import ModerationObjectType 

10 

11if TYPE_CHECKING: 

12 from couchers.models import Cluster, User 

13 

14 

15class Discussion(Base, kw_only=True): 

16 """ 

17 forum board 

18 """ 

19 

20 __tablename__ = "discussions" 

21 __moderation_author_column__ = "creator_user_id" 

22 __moderation_object_type__ = ModerationObjectType.discussion 

23 __moderation_has_own_visibility_mechanism__ = False 

24 

25 id: Mapped[int] = mapped_column( 

26 BigInteger, communities_seq, primary_key=True, server_default=communities_seq.next_value(), init=False 

27 ) 

28 

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) 

36 

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) 

39 

40 slug: Mapped[str] = column_property(func.slugify(title)) 

41 

42 thread: Mapped[Thread] = relationship(init=False, backref="discussion", uselist=False) 

43 

44 subscribers: Mapped[list[User]] = relationship( 

45 init=False, backref="discussions", secondary="discussion_subscriptions", viewonly=True 

46 ) 

47 

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) 

52 

53 

54class DiscussionSubscription(Base, kw_only=True): 

55 """ 

56 users subscriptions to discussions 

57 """ 

58 

59 __tablename__ = "discussion_subscriptions" 

60 __table_args__ = (UniqueConstraint("discussion_id", "user_id"),) 

61 

62 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

63 

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) 

68 

69 user: Mapped[User] = relationship(init=False, backref="discussion_subscriptions") 

70 discussion: Mapped[Discussion] = relationship(init=False, backref="discussion_subscriptions") 

71 

72 

73class Thread(Base, kw_only=True): 

74 """ 

75 Thread 

76 """ 

77 

78 __tablename__ = "threads" 

79 

80 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

81 

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) 

84 

85 

86class Comment(Base, kw_only=True): 

87 """ 

88 Comment 

89 """ 

90 

91 __tablename__ = "comments" 

92 __moderation_author_column__ = "author_user_id" 

93 __moderation_object_type__ = ModerationObjectType.comment 

94 __moderation_has_own_visibility_mechanism__ = False 

95 

96 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

97 

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) 

105 

106 thread: Mapped[Thread] = relationship(init=False, backref="comments") 

107 

108 

109class Reply(Base, kw_only=True): 

110 """ 

111 Reply 

112 """ 

113 

114 __tablename__ = "replies" 

115 __moderation_author_column__ = "author_user_id" 

116 __moderation_object_type__ = ModerationObjectType.reply 

117 __moderation_has_own_visibility_mechanism__ = False 

118 

119 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

120 

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) 

128 

129 comment: Mapped[Comment] = relationship(init=False, backref="replies") 

130 

131 

132class ContentChangeType(enum.Enum): 

133 edit = enum.auto() 

134 delete = enum.auto() 

135 

136 

137class DiscussionVersion(Base, kw_only=True): 

138 """ 

139 audit log of edits and deletions to discussions 

140 """ 

141 

142 __tablename__ = "discussion_versions" 

143 

144 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

145 

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) 

154 

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 ) 

159 

160 

161class CommentVersion(Base, kw_only=True): 

162 """ 

163 audit log of edits and deletions to comments 

164 """ 

165 

166 __tablename__ = "comment_versions" 

167 

168 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

169 

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) 

176 

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 ) 

181 

182 

183class ReplyVersion(Base, kw_only=True): 

184 """ 

185 audit log of edits and deletions to replies 

186 """ 

187 

188 __tablename__ = "reply_versions" 

189 

190 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

191 

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) 

198 

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 ) 

203 

204 

205class ClusterDiscussionAssociation(Base, kw_only=True): 

206 """ 

207 discussions related to clusters 

208 """ 

209 

210 __tablename__ = "cluster_discussion_associations" 

211 __table_args__ = (UniqueConstraint("discussion_id", "cluster_id"),) 

212 

213 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

214 

215 discussion_id: Mapped[int] = mapped_column(ForeignKey("discussions.id"), index=True) 

216 cluster_id: Mapped[int] = mapped_column(ForeignKey("clusters.id"), index=True) 

217 

218 discussion: Mapped[Discussion] = relationship(init=False, backref="cluster_discussion_associations") 

219 cluster: Mapped[Cluster] = relationship(init=False, backref="cluster_discussion_associations")