Coverage for src/couchers/notifications/settings.py: 93%
55 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
1import logging
3from couchers.db import session_scope
4from couchers.models import (
5 NotificationDelivery,
6 NotificationDeliveryType,
7 NotificationPreference,
8 NotificationTopicAction,
9)
10from couchers.notifications.utils import enum_from_topic_action
11from couchers.sql import couchers_select as select
12from proto import notifications_pb2
14logger = logging.getLogger(__name__)
17def get_preference(session, user_id: int, topic_action: NotificationTopicAction) -> list[NotificationDeliveryType]:
18 """
19 Gets the user's preference from the DB or otherwise falls back to defaults
21 Must be done in session scope
23 Returns list of delivery types
24 """
25 overrides = {
26 res.delivery_type: res.deliver
27 for res in session.execute(
28 select(NotificationPreference)
29 .where(NotificationPreference.user_id == user_id)
30 .where(NotificationPreference.topic_action == topic_action)
31 )
32 .scalars()
33 .all()
34 }
35 return [dt for dt in NotificationDeliveryType if overrides.get(dt, dt in topic_action.defaults)]
38def reset_preference(session, user_id, topic_action, delivery_type):
39 current_pref = session.execute(
40 select(NotificationPreference)
41 .where(NotificationPreference.user_id == user_id)
42 .where(NotificationPreference.topic_action == topic_action)
43 .where(NotificationDelivery.delivery_type == delivery_type)
44 ).scalar_one_or_none()
45 if current_pref:
46 session.delete(current_pref)
47 session.flush()
50class PreferenceNotUserEditableError(Exception):
51 pass
54def set_preference(session, user_id, topic_action: NotificationTopicAction, delivery_type, deliver):
55 if not topic_action.user_editable:
56 raise PreferenceNotUserEditableError()
57 current_pref = session.execute(
58 select(NotificationPreference)
59 .where(NotificationPreference.user_id == user_id)
60 .where(NotificationPreference.topic_action == topic_action)
61 .where(NotificationPreference.delivery_type == delivery_type)
62 ).scalar_one_or_none()
63 if current_pref:
64 current_pref.deliver = deliver
65 else:
66 session.add(
67 NotificationPreference(
68 user_id=user_id,
69 topic_action=topic_action,
70 delivery_type=delivery_type,
71 deliver=deliver,
72 )
73 )
74 session.flush()
77settings_layout = [
78 (
79 "Core Features",
80 [
81 (
82 "host_request",
83 "Host requests",
84 [
85 ("create", "Someone sends you a host request"),
86 ("accept", "Someone accepts your host request"),
87 ("confirm", "Someone confirms their host request"),
88 ("reject", "Someone declines your host request"),
89 ("cancel", "Someone cancels their host request"),
90 ("message", "Someone sends a message in a host request"),
91 ("missed_messages", "You miss messages in a host request"),
92 ],
93 ),
94 (
95 "chat",
96 "Messaging",
97 [
98 ("message", "Someone sends you a message"),
99 ("missed_messages", "You miss messages in a chat"),
100 ],
101 ),
102 (
103 "reference",
104 "References",
105 [
106 ("receive_hosted", "You receive a reference from someone who hosted you"),
107 ("receive_surfed", "You receive a reference from someone you hosted"),
108 ("receive_friend", "You received a reference from a friend"),
109 ("reminder_hosted", "Reminder to write a reference to someone you hosted"),
110 ("reminder_surfed", "Reminder to write a reference to someone you surfed with"),
111 ],
112 ),
113 ],
114 ),
115 (
116 "Community Features",
117 [
118 (
119 "friend_request",
120 "Friend requests",
121 [
122 ("create", "Someone sends you a friend request"),
123 ("accept", "Someone accepts your friend request"),
124 ],
125 ),
126 (
127 "event",
128 "Events",
129 [
130 ("create_approved", "An event that is approved by the moderators is created in your community"),
131 ("create_any", "A user creates any event in your community (not checked by an admin)"),
132 ("update", "An event you are attending is updated"),
133 ("cancel", "An event you are attending is cancelled"),
134 ("delete", "An event you are attending is deleted"),
135 ("invite_organizer", "Someone invites you to co-organize an event"),
136 ],
137 ),
138 ],
139 ),
140 (
141 "Account Settings",
142 [
143 (
144 "onboarding",
145 "Onboarding",
146 [
147 ("reminder", "Reminder to complete your profile after signing up"),
148 ],
149 ),
150 (
151 "badge",
152 "Updates to Badges on your profile",
153 [
154 ("add", "A badge is added to your account"),
155 ("remove", "A badge is removed from your account"),
156 ],
157 ),
158 (
159 "donation",
160 "Donations",
161 [
162 ("received", "Your donation is received"),
163 ],
164 ),
165 ],
166 ),
167 (
168 "Account Security",
169 [
170 (
171 "password",
172 "Password change",
173 [
174 ("change", "Your password is changed"),
175 ],
176 ),
177 (
178 "password_reset",
179 "Password reset",
180 [
181 ("start", "Password reset is initiated"),
182 ("complete", "Password reset is completed"),
183 ],
184 ),
185 (
186 "email_address",
187 "Email address change",
188 [
189 ("change", "Email change is initiated"),
190 ("verify", "Your new email is verified"),
191 ],
192 ),
193 (
194 "account_deletion",
195 "Account deletion",
196 [
197 ("start", "You initiate account deletion"),
198 ("complete", "Your account is deleted"),
199 ("recovered", "Your account is recovered (undeleted)"),
200 ],
201 ),
202 (
203 "api_key",
204 "API keys",
205 [
206 ("create", "An API key is created for your account"),
207 ],
208 ),
209 (
210 "phone_number",
211 "Phone number change",
212 [
213 ("change", "Your phone number is changed"),
214 ("verify", "Your phone number is verified"),
215 ],
216 ),
217 (
218 "birthdate",
219 "Birthdate change",
220 [
221 ("change", "Your birthdate is changed"),
222 ],
223 ),
224 (
225 "gender",
226 "Displayed gender change",
227 [
228 ("change", "The gender displayed on your profile is changed"),
229 ],
230 ),
231 (
232 "modnote",
233 "Moderator notes",
234 [
235 ("create", "You receive a moderator note"),
236 ],
237 ),
238 (
239 "verification",
240 "Verification",
241 [
242 ("sv_fail", "Strong Verification fails"),
243 ("sv_success", "Strong Verification succeeds"),
244 ],
245 ),
246 ],
247 ),
248]
251def check_settings():
252 # check settings contain all actions+topics
253 actions_by_topic = {}
254 for t in NotificationTopicAction:
255 actions_by_topic[t.topic] = actions_by_topic.get(t.topic, []) + [t.action]
257 actions_by_topic_check = {}
259 for heading, group in settings_layout:
260 for topic, name, items in group:
261 actions = []
262 for action, description in items:
263 actions.append(action)
264 actions_by_topic_check[topic] = actions
266 for topic, actions in actions_by_topic.items():
267 assert sorted(actions) == sorted(
268 actions_by_topic_check[topic]
269 ), f"Expected {actions} == {actions_by_topic_check[topic]} for {topic}"
270 assert sorted(actions_by_topic.keys()) == sorted(actions_by_topic_check.keys())
273check_settings()
276def get_user_setting_groups(user_id) -> list[notifications_pb2.NotificationGroup]:
277 with session_scope() as session:
278 groups = []
279 for heading, group in settings_layout:
280 topics = []
281 for topic, name, items in group:
282 actions = []
283 for action, description in items:
284 topic_action = enum_from_topic_action[topic, action]
285 delivery_types = get_preference(session, user_id, topic_action)
286 actions.append(
287 notifications_pb2.NotificationItem(
288 action=action,
289 description=description,
290 user_editable=topic_action.user_editable,
291 push=NotificationDeliveryType.push in delivery_types,
292 email=NotificationDeliveryType.email in delivery_types,
293 digest=NotificationDeliveryType.digest in delivery_types,
294 )
295 )
296 topics.append(
297 notifications_pb2.NotificationTopic(
298 topic=topic,
299 name=name,
300 items=actions,
301 )
302 )
303 groups.append(
304 notifications_pb2.NotificationGroup(
305 heading=heading,
306 topics=topics,
307 )
308 )
309 return groups