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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-03 11:21 +0000
1import logging
3import grpc
4from sqlalchemy import select
5from sqlalchemy.orm import Session
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
19logger = logging.getLogger(__name__)
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
30 can_moderate = can_moderate_node(session, context.user_id, discussion.owner_cluster.parent_node_id)
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 )
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()
51 cluster = discussion.owner_cluster
53 if not cluster.is_official_cluster:
54 raise NotImplementedError("Shouldn't have discussions under groups, only communities")
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 )
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")
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()
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")
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")
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()
110 queue_job(
111 session,
112 job=generate_create_discussion_notifications,
113 payload=jobs_pb2.GenerateCreateDiscussionNotificationsPayload(
114 discussion_id=discussion.id,
115 ),
116 )
118 return discussion_to_pb(session, discussion, context)
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")
129 return discussion_to_pb(session, discussion, context)