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

1import logging 

2 

3from sqlalchemy import delete, select 

4from sqlalchemy.orm import Session 

5 

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 

15 

16logger = logging.getLogger(__name__) 

17 

18 

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 

24 

25 Must be done in session scope 

26 

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

40 

41 

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

56 

57 

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 ) 

70 

71 

72class PreferenceNotUserEditableError(Exception): 

73 pass 

74 

75 

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

103 

104 

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] 

323 

324 

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