Coverage for src/couchers/notifications/settings.py: 93%

58 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-07-14 16:54 +0000

1import logging 

2 

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 

13 

14logger = logging.getLogger(__name__) 

15 

16 

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 

20 

21 Must be done in session scope 

22 

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)] 

36 

37 

38def get_topic_actions_by_delivery_type( 

39 session, user_id: int, delivery_type: NotificationDeliveryType 

40) -> set[NotificationTopicAction]: 

41 """ 

42 Given push/email/digest, returns notifications that this user has enabled for that type. 

43 """ 

44 overrides = dict( 

45 session.execute( 

46 select(NotificationPreference.topic_action, NotificationPreference.deliver) 

47 .where(NotificationPreference.user_id == user_id) 

48 .where(NotificationPreference.delivery_type == delivery_type) 

49 ).all() 

50 ) 

51 return {t for t in NotificationTopicAction if overrides.get(t, delivery_type in t.defaults)} 

52 

53 

54def reset_preference(session, user_id, topic_action, delivery_type): 

55 current_pref = session.execute( 

56 select(NotificationPreference) 

57 .where(NotificationPreference.user_id == user_id) 

58 .where(NotificationPreference.topic_action == topic_action) 

59 .where(NotificationDelivery.delivery_type == delivery_type) 

60 ).scalar_one_or_none() 

61 if current_pref: 

62 session.delete(current_pref) 

63 session.flush() 

64 

65 

66class PreferenceNotUserEditableError(Exception): 

67 pass 

68 

69 

70def set_preference(session, user_id, topic_action: NotificationTopicAction, delivery_type, deliver): 

71 if not topic_action.user_editable: 

72 raise PreferenceNotUserEditableError() 

73 current_pref = session.execute( 

74 select(NotificationPreference) 

75 .where(NotificationPreference.user_id == user_id) 

76 .where(NotificationPreference.topic_action == topic_action) 

77 .where(NotificationPreference.delivery_type == delivery_type) 

78 ).scalar_one_or_none() 

79 if current_pref: 

80 current_pref.deliver = deliver 

81 else: 

82 session.add( 

83 NotificationPreference( 

84 user_id=user_id, 

85 topic_action=topic_action, 

86 delivery_type=delivery_type, 

87 deliver=deliver, 

88 ) 

89 ) 

90 session.flush() 

91 

92 

93settings_layout = [ 

94 ( 

95 "Core Features", 

96 [ 

97 ( 

98 "host_request", 

99 "Host requests", 

100 [ 

101 ("create", "Someone sends you a host request"), 

102 ("accept", "Someone accepts your host request"), 

103 ("confirm", "Someone confirms their host request"), 

104 ("reject", "Someone declines your host request"), 

105 ("cancel", "Someone cancels their host request"), 

106 ("message", "Someone sends a message in a host request"), 

107 ("missed_messages", "You miss messages in a host request"), 

108 ("reminder", "You don't respond to a pending host request for a while"), 

109 ], 

110 ), 

111 ( 

112 "activeness", 

113 "Activity Check-in", 

114 [ 

115 ("probe", "Check in to see if you are still hosting after a long period of inactivity"), 

116 ], 

117 ), 

118 ( 

119 "chat", 

120 "Messaging", 

121 [ 

122 ("message", "Someone sends you a message"), 

123 ("missed_messages", "You miss messages in a chat"), 

124 ], 

125 ), 

126 ( 

127 "reference", 

128 "References", 

129 [ 

130 ("receive_hosted", "You receive a reference from someone who hosted you"), 

131 ("receive_surfed", "You receive a reference from someone you hosted"), 

132 ("receive_friend", "You received a reference from a friend"), 

133 ("reminder_hosted", "Reminder to write a reference to someone you hosted"), 

134 ("reminder_surfed", "Reminder to write a reference to someone you surfed with"), 

135 ], 

136 ), 

137 ], 

138 ), 

139 ( 

140 "Community Features", 

141 [ 

142 ( 

143 "friend_request", 

144 "Friend requests", 

145 [ 

146 ("create", "Someone sends you a friend request"), 

147 ("accept", "Someone accepts your friend request"), 

148 ], 

149 ), 

150 ( 

151 "event", 

152 "Events", 

153 [ 

154 ("create_approved", "An event that is approved by the moderators is created in your community"), 

155 ("create_any", "A user creates any event in your community (not checked by an admin)"), 

156 ("update", "An event you are attending is updated"), 

157 ("comment", "Someone comments on an event you are organizing or attending"), 

158 ("cancel", "An event you are attending is cancelled"), 

159 ("delete", "An event you are attending is deleted"), 

160 ("invite_organizer", "Someone invites you to co-organize an event"), 

161 ("reminder", "Reminder for an upcoming event"), 

162 ], 

163 ), 

164 ( 

165 "discussion", 

166 "Community discussions", 

167 [ 

168 ("create", "Someone creates a discussion in one of your communities"), 

169 ("comment", "Someone comments on a discussion you authored"), 

170 ], 

171 ), 

172 ( 

173 "thread", 

174 "Threads, Comments, & Replies", 

175 [ 

176 ("reply", "Someone replies to your comment"), 

177 ], 

178 ), 

179 ], 

180 ), 

181 ( 

182 "Account Settings", 

183 [ 

184 ( 

185 "onboarding", 

186 "Onboarding", 

187 [ 

188 ("reminder", "Reminder to complete your profile after signing up"), 

189 ], 

190 ), 

191 ( 

192 "badge", 

193 "Updates to Badges on your profile", 

194 [ 

195 ("add", "A badge is added to your account"), 

196 ("remove", "A badge is removed from your account"), 

197 ], 

198 ), 

199 ( 

200 "donation", 

201 "Donations", 

202 [ 

203 ("received", "Your donation is received"), 

204 ], 

205 ), 

206 ], 

207 ), 

208 ( 

209 "Account Security", 

210 [ 

211 ( 

212 "password", 

213 "Password change", 

214 [ 

215 ("change", "Your password is changed"), 

216 ], 

217 ), 

218 ( 

219 "password_reset", 

220 "Password reset", 

221 [ 

222 ("start", "Password reset is initiated"), 

223 ("complete", "Password reset is completed"), 

224 ], 

225 ), 

226 ( 

227 "email_address", 

228 "Email address change", 

229 [ 

230 ("change", "Email change is initiated"), 

231 ("verify", "Your new email is verified"), 

232 ], 

233 ), 

234 ( 

235 "account_deletion", 

236 "Account deletion", 

237 [ 

238 ("start", "You initiate account deletion"), 

239 ("complete", "Your account is deleted"), 

240 ("recovered", "Your account is recovered (undeleted)"), 

241 ], 

242 ), 

243 ( 

244 "api_key", 

245 "API keys", 

246 [ 

247 ("create", "An API key is created for your account"), 

248 ], 

249 ), 

250 ( 

251 "phone_number", 

252 "Phone number change", 

253 [ 

254 ("change", "Your phone number is changed"), 

255 ("verify", "Your phone number is verified"), 

256 ], 

257 ), 

258 ( 

259 "birthdate", 

260 "Birthdate change", 

261 [ 

262 ("change", "Your birthdate is changed"), 

263 ], 

264 ), 

265 ( 

266 "gender", 

267 "Displayed gender change", 

268 [ 

269 ("change", "The gender displayed on your profile is changed"), 

270 ], 

271 ), 

272 ( 

273 "modnote", 

274 "Moderator notes", 

275 [ 

276 ("create", "You receive a moderator note"), 

277 ], 

278 ), 

279 ( 

280 "verification", 

281 "Verification", 

282 [ 

283 ("sv_fail", "Strong Verification fails"), 

284 ("sv_success", "Strong Verification succeeds"), 

285 ], 

286 ), 

287 ], 

288 ), 

289 ( 

290 "Other Notifications", 

291 [ 

292 ( 

293 "general", 

294 "General", 

295 [ 

296 ("new_blog_post", "We published a new blog post"), 

297 ], 

298 ), 

299 ], 

300 ), 

301] 

302 

303 

304def check_settings(): 

305 # check settings contain all actions+topics 

306 actions_by_topic = {} 

307 for t in NotificationTopicAction: 

308 actions_by_topic[t.topic] = actions_by_topic.get(t.topic, []) + [t.action] 

309 

310 actions_by_topic_check = {} 

311 

312 for heading, group in settings_layout: 

313 for topic, name, items in group: 

314 actions = [] 

315 for action, description in items: 

316 actions.append(action) 

317 actions_by_topic_check[topic] = actions 

318 

319 for topic, actions in actions_by_topic.items(): 

320 assert sorted(actions) == sorted(actions_by_topic_check[topic]), ( 

321 f"Expected {actions} == {actions_by_topic_check[topic]} for {topic}" 

322 ) 

323 assert sorted(actions_by_topic.keys()) == sorted(actions_by_topic_check.keys()) 

324 

325 

326check_settings() 

327 

328 

329def get_user_setting_groups(user_id) -> list[notifications_pb2.NotificationGroup]: 

330 with session_scope() as session: 

331 groups = [] 

332 for heading, group in settings_layout: 

333 topics = [] 

334 for topic, name, items in group: 

335 actions = [] 

336 for action, description in items: 

337 topic_action = enum_from_topic_action[topic, action] 

338 delivery_types = get_preference(session, user_id, topic_action) 

339 actions.append( 

340 notifications_pb2.NotificationItem( 

341 action=action, 

342 description=description, 

343 user_editable=topic_action.user_editable, 

344 push=NotificationDeliveryType.push in delivery_types, 

345 email=NotificationDeliveryType.email in delivery_types, 

346 digest=NotificationDeliveryType.digest in delivery_types, 

347 ) 

348 ) 

349 topics.append( 

350 notifications_pb2.NotificationTopic( 

351 topic=topic, 

352 name=name, 

353 items=actions, 

354 ) 

355 ) 

356 groups.append( 

357 notifications_pb2.NotificationGroup( 

358 heading=heading, 

359 topics=topics, 

360 ) 

361 ) 

362 return groups