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

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 

24 id: Mapped[int] = mapped_column( 

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

26 ) 

27 

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) 

35 

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) 

38 

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

40 

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

42 

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

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

45 ) 

46 

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) 

51 

52 

53class DiscussionSubscription(Base, kw_only=True): 

54 """ 

55 users subscriptions to discussions 

56 """ 

57 

58 __tablename__ = "discussion_subscriptions" 

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

60 

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

62 

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) 

67 

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

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

70 

71 

72class Thread(Base, kw_only=True): 

73 """ 

74 Thread 

75 """ 

76 

77 __tablename__ = "threads" 

78 

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

80 

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) 

83 

84 

85class Comment(Base, kw_only=True): 

86 """ 

87 Comment 

88 """ 

89 

90 __tablename__ = "comments" 

91 __moderation_author_column__ = "author_user_id" 

92 __moderation_object_type__ = ModerationObjectType.comment 

93 

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

95 

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) 

103 

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

105 

106 

107class Reply(Base, kw_only=True): 

108 """ 

109 Reply 

110 """ 

111 

112 __tablename__ = "replies" 

113 __moderation_author_column__ = "author_user_id" 

114 __moderation_object_type__ = ModerationObjectType.reply 

115 

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

117 

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) 

125 

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

127 

128 

129class ContentChangeType(enum.Enum): 

130 edit = enum.auto() 

131 delete = enum.auto() 

132 

133 

134class DiscussionVersion(Base, kw_only=True): 

135 """ 

136 audit log of edits and deletions to discussions 

137 """ 

138 

139 __tablename__ = "discussion_versions" 

140 

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

142 

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) 

151 

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 ) 

156 

157 

158class CommentVersion(Base, kw_only=True): 

159 """ 

160 audit log of edits and deletions to comments 

161 """ 

162 

163 __tablename__ = "comment_versions" 

164 

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

166 

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) 

173 

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 ) 

178 

179 

180class ReplyVersion(Base, kw_only=True): 

181 """ 

182 audit log of edits and deletions to replies 

183 """ 

184 

185 __tablename__ = "reply_versions" 

186 

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

188 

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) 

195 

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 ) 

200 

201 

202class ClusterDiscussionAssociation(Base, kw_only=True): 

203 """ 

204 discussions related to clusters 

205 """ 

206 

207 __tablename__ = "cluster_discussion_associations" 

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

209 

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

211 

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

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

214 

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

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