Coverage for src / couchers / servicers / discussions.py: 87%

60 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-03 11:21 +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.notifications.notify import notify 

12from couchers.proto import discussions_pb2, discussions_pb2_grpc, notification_data_pb2 

13from couchers.proto.internal import jobs_pb2 

14from couchers.servicers.api import user_model_to_pb 

15from couchers.servicers.blocking import is_not_visible 

16from couchers.servicers.threads import thread_to_pb 

17from couchers.utils import Timestamp_from_datetime 

18 

19logger = logging.getLogger(__name__) 

20 

21 

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

23 owner_community_id = None 

24 owner_group_id = None 

25 if discussion.owner_cluster.is_official_cluster: 

26 owner_community_id = discussion.owner_cluster.parent_node_id 

27 else: 

28 owner_group_id = discussion.owner_cluster.id 

29 

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

31 

32 return discussions_pb2.Discussion( 

33 discussion_id=discussion.id, 

34 slug=discussion.slug, 

35 created=Timestamp_from_datetime(discussion.created), 

36 creator_user_id=discussion.creator_user_id, 

37 owner_community_id=owner_community_id, 

38 owner_group_id=owner_group_id, 

39 owner_title=discussion.owner_cluster.name, 

40 title=discussion.title, 

41 content=discussion.content, 

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

43 can_moderate=can_moderate, 

44 ) 

45 

46 

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

48 with session_scope() as session: 

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

50 

51 cluster = discussion.owner_cluster 

52 

53 if not cluster.is_official_cluster: 

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

55 

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

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

58 continue 

59 context = make_background_user_context(user_id=user.id) 

60 notify( 

61 session, 

62 user_id=user.id, 

63 topic_action="discussion:create", 

64 key=str(payload.discussion_id), 

65 data=notification_data_pb2.DiscussionCreate( 

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

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

68 ), 

69 ) 

70 

71 

72class Discussions(discussions_pb2_grpc.DiscussionsServicer): 

73 def CreateDiscussion( 

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

75 ) -> discussions_pb2.Discussion: 

76 if not request.title: 

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

78 if not request.content: 

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

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

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

82 

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

84 cluster = session.execute( 

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

86 ).scalar_one_or_none() 

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

88 cluster = session.execute( 

89 select(Cluster) 

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

91 .where(Cluster.is_official_cluster) 

92 ).scalar_one_or_none() 

93 

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

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

96 

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

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

99 

100 discussion = Discussion( 

101 title=request.title, 

102 content=request.content, 

103 creator_user_id=context.user_id, 

104 owner_cluster=cluster, 

105 thread=Thread(), 

106 ) 

107 session.add(discussion) 

108 session.flush() 

109 

110 queue_job( 

111 session, 

112 job=generate_create_discussion_notifications, 

113 payload=jobs_pb2.GenerateCreateDiscussionNotificationsPayload( 

114 discussion_id=discussion.id, 

115 ), 

116 ) 

117 

118 return discussion_to_pb(session, discussion, context) 

119 

120 def GetDiscussion( 

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

122 ) -> discussions_pb2.Discussion: 

123 discussion = session.execute( 

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

125 ).scalar_one_or_none() 

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

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

128 

129 return discussion_to_pb(session, discussion, context)