Coverage for app/backend/src/couchers/notifications/settings.py: 100%
44 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
1import logging
3from sqlalchemy import delete, select
4from sqlalchemy.orm import Session
6from couchers.db import session_scope
7from couchers.i18n import LocalizationContext
8from couchers.models import (
9 NotificationDeliveryType,
10 NotificationPreference,
11 NotificationTopicAction,
12)
13from couchers.notifications.utils import get_topic_action_description
14from couchers.proto import notifications_pb2
16logger = logging.getLogger(__name__)
19def get_preference(
20 session: Session, user_id: int, topic_action: NotificationTopicAction
21) -> list[NotificationDeliveryType]:
22 """
23 Gets the user's preference from the DB or otherwise falls back to defaults
25 Must be done in session scope
27 Returns list of delivery types
28 """
29 overrides = {
30 res.delivery_type: res.deliver
31 for res in session.execute(
32 select(NotificationPreference)
33 .where(NotificationPreference.user_id == user_id)
34 .where(NotificationPreference.topic_action == topic_action)
35 )
36 .scalars()
37 .all()
38 }
39 return [dt for dt in NotificationDeliveryType if overrides.get(dt, dt in topic_action.defaults)]
42def get_topic_actions_by_delivery_type(
43 session: Session, user_id: int, delivery_type: NotificationDeliveryType
44) -> set[NotificationTopicAction]:
45 """
46 Given push/email/digest, returns notifications that this user has enabled for that type.
47 """
48 overrides: dict[NotificationTopicAction, bool] = {}
49 for topic_action, deliver in session.execute(
50 select(NotificationPreference.topic_action, NotificationPreference.deliver)
51 .where(NotificationPreference.user_id == user_id)
52 .where(NotificationPreference.delivery_type == delivery_type)
53 ).all():
54 overrides[topic_action] = deliver
55 return {t for t in NotificationTopicAction if overrides.get(t, delivery_type in t.defaults)}
58def reset_preference(
59 session: Session,
60 user_id: int,
61 topic_action: NotificationTopicAction,
62 delivery_type: NotificationDeliveryType,
63) -> None:
64 session.execute(
65 delete(NotificationPreference)
66 .where(NotificationPreference.user_id == user_id)
67 .where(NotificationPreference.topic_action == topic_action)
68 .where(NotificationPreference.delivery_type == delivery_type)
69 )
72class PreferenceNotUserEditableError(Exception):
73 pass
76def set_preference(
77 session: Session,
78 user_id: int,
79 topic_action: NotificationTopicAction,
80 delivery_type: NotificationDeliveryType,
81 deliver: bool,
82) -> None:
83 if topic_action.is_critical:
84 raise PreferenceNotUserEditableError()
85 current_pref = session.execute(
86 select(NotificationPreference)
87 .where(NotificationPreference.user_id == user_id)
88 .where(NotificationPreference.topic_action == topic_action)
89 .where(NotificationPreference.delivery_type == delivery_type)
90 ).scalar_one_or_none()
91 if current_pref:
92 current_pref.deliver = deliver
93 else:
94 session.add(
95 NotificationPreference(
96 user_id=user_id,
97 topic_action=topic_action,
98 delivery_type=delivery_type,
99 deliver=deliver,
100 )
101 )
102 session.flush()
105settings_layout = [
106 (
107 "Core Features",
108 [
109 (
110 "host_request",
111 "Host requests",
112 [
113 NotificationTopicAction.host_request__create,
114 NotificationTopicAction.host_request__accept,
115 NotificationTopicAction.host_request__confirm,
116 NotificationTopicAction.host_request__reject,
117 NotificationTopicAction.host_request__cancel,
118 NotificationTopicAction.host_request__message,
119 NotificationTopicAction.host_request__missed_messages,
120 NotificationTopicAction.host_request__reminder,
121 ],
122 ),
123 (
124 "activeness",
125 "Activity Check-in",
126 [
127 NotificationTopicAction.activeness__probe,
128 ],
129 ),
130 (
131 "chat",
132 "Messaging",
133 [
134 NotificationTopicAction.chat__message,
135 NotificationTopicAction.chat__missed_messages,
136 ],
137 ),
138 (
139 "reference",
140 "References",
141 [
142 NotificationTopicAction.reference__receive_hosted,
143 NotificationTopicAction.reference__receive_surfed,
144 NotificationTopicAction.reference__receive_friend,
145 NotificationTopicAction.reference__reminder_hosted,
146 NotificationTopicAction.reference__reminder_surfed,
147 ],
148 ),
149 ],
150 ),
151 (
152 "Community Features",
153 [
154 (
155 "friend_request",
156 "Friend requests",
157 [
158 NotificationTopicAction.friend_request__create,
159 NotificationTopicAction.friend_request__accept,
160 ],
161 ),
162 (
163 "event",
164 "Events",
165 [
166 NotificationTopicAction.event__create_approved,
167 NotificationTopicAction.event__create_any,
168 NotificationTopicAction.event__update,
169 NotificationTopicAction.event__comment,
170 NotificationTopicAction.event__cancel,
171 NotificationTopicAction.event__delete,
172 NotificationTopicAction.event__invite_organizer,
173 NotificationTopicAction.event__reminder,
174 ],
175 ),
176 (
177 "discussion",
178 "Community discussions",
179 [
180 NotificationTopicAction.discussion__create,
181 NotificationTopicAction.discussion__comment,
182 ],
183 ),
184 (
185 "thread",
186 "Threads, Comments, & Replies",
187 [
188 NotificationTopicAction.thread__reply,
189 ],
190 ),
191 ],
192 ),
193 (
194 "Account Settings",
195 [
196 (
197 "onboarding",
198 "Onboarding",
199 [
200 NotificationTopicAction.onboarding__reminder,
201 ],
202 ),
203 (
204 "badge",
205 "Updates to Badges on your profile",
206 [
207 NotificationTopicAction.badge__add,
208 NotificationTopicAction.badge__remove,
209 ],
210 ),
211 (
212 "donation",
213 "Donations",
214 [
215 NotificationTopicAction.donation__received,
216 ],
217 ),
218 ],
219 ),
220 (
221 "Account Security",
222 [
223 (
224 "password",
225 "Password change",
226 [
227 NotificationTopicAction.password__change,
228 ],
229 ),
230 (
231 "password_reset",
232 "Password reset",
233 [
234 NotificationTopicAction.password_reset__start,
235 NotificationTopicAction.password_reset__complete,
236 ],
237 ),
238 (
239 "email_address",
240 "Email address change",
241 [
242 NotificationTopicAction.email_address__change,
243 NotificationTopicAction.email_address__verify,
244 ],
245 ),
246 (
247 "account_deletion",
248 "Account deletion",
249 [
250 NotificationTopicAction.account_deletion__start,
251 NotificationTopicAction.account_deletion__complete,
252 NotificationTopicAction.account_deletion__recovered,
253 ],
254 ),
255 (
256 "api_key",
257 "API keys",
258 [
259 NotificationTopicAction.api_key__create,
260 ],
261 ),
262 (
263 "phone_number",
264 "Phone number change",
265 [
266 NotificationTopicAction.phone_number__change,
267 NotificationTopicAction.phone_number__verify,
268 ],
269 ),
270 (
271 "birthdate",
272 "Birthdate change",
273 [
274 NotificationTopicAction.birthdate__change,
275 ],
276 ),
277 (
278 "gender",
279 "Displayed gender change",
280 [
281 NotificationTopicAction.gender__change,
282 ],
283 ),
284 (
285 "modnote",
286 "Moderator notes",
287 [
288 NotificationTopicAction.modnote__create,
289 ],
290 ),
291 (
292 "verification",
293 "Verification",
294 [
295 NotificationTopicAction.verification__sv_fail,
296 NotificationTopicAction.verification__sv_success,
297 ],
298 ),
299 (
300 "postal_verification",
301 "Postal Verification",
302 [
303 NotificationTopicAction.postal_verification__postcard_sent,
304 NotificationTopicAction.postal_verification__success,
305 NotificationTopicAction.postal_verification__failed,
306 ],
307 ),
308 ],
309 ),
310 (
311 "Other Notifications",
312 [
313 (
314 "general",
315 "General",
316 [
317 NotificationTopicAction.general__new_blog_post,
318 ],
319 ),
320 ],
321 ),
322]
325def get_user_setting_groups(
326 user_id: int, loc_context: LocalizationContext
327) -> list[notifications_pb2.NotificationGroup]:
328 with session_scope() as session:
329 groups = []
330 for heading, group in settings_layout:
331 topics = []
332 for topic, name, items in group:
333 actions = []
334 for topic_action in items:
335 delivery_types = get_preference(session, user_id, topic_action)
336 description = get_topic_action_description(topic_action, locales=loc_context.locale_list)
337 actions.append(
338 notifications_pb2.NotificationItem(
339 action=topic_action.action,
340 description=description,
341 user_editable=not topic_action.is_critical,
342 push=NotificationDeliveryType.push in delivery_types,
343 email=NotificationDeliveryType.email in delivery_types,
344 digest=NotificationDeliveryType.digest in delivery_types,
345 )
346 )
347 topics.append(
348 notifications_pb2.NotificationTopic(
349 topic=topic,
350 name=name,
351 items=actions,
352 )
353 )
354 groups.append(
355 notifications_pb2.NotificationGroup(
356 heading=heading,
357 topics=topics,
358 )
359 )
360 return groups