Coverage for app / backend / src / couchers / servicers / discussions.py: 85%

65 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 06:18 +0000

1import logging 

2 

3import grpc 

4from sqlalchemy import select 

5from sqlalchemy.orm import Session 

6 

7from couchers.context import CouchersContext, make_background_user_context 

8from couchers.db import can_moderate_node, session_scope 

9from couchers.jobs.enqueue import queue_job 

10from couchers.models import Cluster, Discussion, Thread, User 

11from couchers.models.notifications import NotificationTopicAction 

12from couchers.notifications.notify import notify 

13from couchers.proto import discussions_pb2, discussions_pb2_grpc, notification_data_pb2 

14from couchers.proto.internal import jobs_pb2 

15from couchers.servicers.api import user_model_to_pb 

16from couchers.servicers.blocking import is_not_visible 

17from couchers.servicers.threads import thread_to_pb 

18from couchers.utils import Timestamp_from_datetime 

19 

20logger = logging.getLogger(__name__) 

21 

22 

23def discussion_to_pb(session: Session, discussion: Discussion, context: CouchersContext) -> discussions_pb2.Discussion: 

24 owner_community_id = None 

25 owner_group_id = None 

26 if discussion.owner_cluster.is_official_cluster: 

27 owner_community_id = discussion.owner_cluster.parent_node_id 

28 else: 

29 owner_group_id = discussion.owner_cluster.id 

30 

31 can_moderate = can_moderate_node(session, context.user_id, discussion.owner_cluster.parent_node_id) 

32 

33 return discussions_pb2.Discussion( 

34 discussion_id=discussion.id, 

35 slug=discussion.slug, 

36 created=Timestamp_from_datetime(discussion.created), 

37 creator_user_id=discussion.creator_user_id, 

38 owner_community_id=owner_community_id, 

39 owner_group_id=owner_group_id, 

40 owner_title=discussion.owner_cluster.name, 

41 title=discussion.title, 

42 content=discussion.content, 

43 thread=thread_to_pb(session, discussion.thread_id), 

44 can_moderate=can_moderate, 

45 ) 

46 

47 

48def generate_create_discussion_notifications(payload: jobs_pb2.GenerateCreateDiscussionNotificationsPayload) -> None: 

49 with session_scope() as session: 

50 discussion = session.execute(select(Discussion).where(Discussion.id == payload.discussion_id)).scalar_one() 

51 

52 cluster = discussion.owner_cluster 

53 

54 if not cluster.is_official_cluster: 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 raise NotImplementedError("Shouldn't have discussions under groups, only communities") 

56 

57 for user in list(cluster.members.where(User.is_visible)): 

58 if is_not_visible(session, user.id, discussion.creator_user_id): 58 ↛ 59line 58 didn't jump to line 59 because the condition on line 58 was never true

59 continue 

60 context = make_background_user_context(user_id=user.id) 

61 notify( 

62 session, 

63 user_id=user.id, 

64 topic_action=NotificationTopicAction.discussion__create, 

65 key=str(payload.discussion_id), 

66 data=notification_data_pb2.DiscussionCreate( 

67 author=user_model_to_pb(discussion.creator_user, session, context), 

68 discussion=discussion_to_pb(session, discussion, context), 

69 ), 

70 ) 

71 

72 

73class Discussions(discussions_pb2_grpc.DiscussionsServicer): 

74 def CreateDiscussion( 

75 self, request: discussions_pb2.CreateDiscussionReq, context: CouchersContext, session: Session 

76 ) -> discussions_pb2.Discussion: 

77 if not request.title: 

78 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_discussion_title") 

79 if not request.content: 

80 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_discussion_content") 

81 if not request.owner_community_id and not request.owner_group_id: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true

82 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "group_or_community_not_found") 

83 

84 if request.WhichOneof("owner") == "owner_group_id": 

85 cluster = session.execute( 

86 select(Cluster).where(~Cluster.is_official_cluster).where(Cluster.id == request.owner_group_id) 

87 ).scalar_one_or_none() 

88 elif request.WhichOneof("owner") == "owner_community_id": 88 ↛ 95line 88 didn't jump to line 95 because the condition on line 88 was always true

89 cluster = session.execute( 

90 select(Cluster) 

91 .where(Cluster.parent_node_id == request.owner_community_id) 

92 .where(Cluster.is_official_cluster) 

93 ).scalar_one_or_none() 

94 

95 if not cluster: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true

96 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "group_or_community_not_found") 

97 

98 if not cluster.discussions_enabled: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cannot_create_discussion") 

100 

101 thread = Thread() 

102 session.add(thread) 

103 session.flush() 

104 

105 discussion = Discussion( 

106 title=request.title, 

107 content=request.content, 

108 creator_user_id=context.user_id, 

109 owner_cluster_id=cluster.id, 

110 thread_id=thread.id, 

111 ) 

112 session.add(discussion) 

113 session.flush() 

114 

115 queue_job( 

116 session, 

117 job=generate_create_discussion_notifications, 

118 payload=jobs_pb2.GenerateCreateDiscussionNotificationsPayload( 

119 discussion_id=discussion.id, 

120 ), 

121 ) 

122 

123 return discussion_to_pb(session, discussion, context) 

124 

125 def GetDiscussion( 

126 self, request: discussions_pb2.GetDiscussionReq, context: CouchersContext, session: Session 

127 ) -> discussions_pb2.Discussion: 

128 discussion = session.execute( 

129 select(Discussion).where(Discussion.id == request.discussion_id) 

130 ).scalar_one_or_none() 

131 if not discussion: 131 ↛ 132line 131 didn't jump to line 132 because the condition on line 131 was never true

132 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "discussion_not_found") 

133 

134 return discussion_to_pb(session, discussion, context)