Coverage for src/couchers/servicers/references.py: 97%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

108 statements  

1""" 

2* Only one friend reference 

3* Multiple of the other types (one for each stay) 

4* Have 2 weeks to write a reference after hosting/surfing 

5* References become visible after min{2 weeks, both reciprocal references written} 

6 

7* TODO: Get bugged about writing reference 1 day after, 1 week after, 2weeks-2days 

8""" 

9import grpc 

10from sqlalchemy.orm import aliased 

11from sqlalchemy.sql import and_, func, literal, or_, union_all 

12 

13from couchers import errors 

14from couchers.db import session_scope 

15from couchers.models import HostRequest, Reference, ReferenceType, User 

16from couchers.sql import couchers_select as select 

17from couchers.tasks import maybe_send_reference_report_email, send_friend_reference_email, send_host_reference_email 

18from couchers.utils import Timestamp_from_datetime 

19from proto import references_pb2, references_pb2_grpc 

20 

21reftype2sql = { 

22 references_pb2.ReferenceType.REFERENCE_TYPE_FRIEND: ReferenceType.friend, 

23 references_pb2.ReferenceType.REFERENCE_TYPE_SURFED: ReferenceType.surfed, 

24 references_pb2.ReferenceType.REFERENCE_TYPE_HOSTED: ReferenceType.hosted, 

25} 

26 

27reftype2api = { 

28 ReferenceType.friend: references_pb2.ReferenceType.REFERENCE_TYPE_FRIEND, 

29 ReferenceType.surfed: references_pb2.ReferenceType.REFERENCE_TYPE_SURFED, 

30 ReferenceType.hosted: references_pb2.ReferenceType.REFERENCE_TYPE_HOSTED, 

31} 

32 

33 

34def reference_to_pb(reference: Reference, context): 

35 return references_pb2.Reference( 

36 reference_id=reference.id, 

37 from_user_id=reference.from_user_id, 

38 to_user_id=reference.to_user_id, 

39 reference_type=reftype2api[reference.reference_type], 

40 text=reference.text, 

41 written_time=Timestamp_from_datetime(reference.time.replace(hour=0, minute=0, second=0, microsecond=0)), 

42 host_request_id=reference.host_request_id 

43 if context.user_id in [reference.from_user_id, reference.to_user_id] 

44 else None, 

45 ) 

46 

47 

48def check_valid_reference(request, context): 

49 if request.rating < 0 or request.rating > 1: 

50 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REFERENCE_INVALID_RATING) 

51 

52 if request.text.strip() == "": 

53 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REFERENCE_NO_TEXT) 

54 

55 

56MAX_PAGINATION_LENGTH = 25 

57 

58 

59class References(references_pb2_grpc.ReferencesServicer): 

60 def ListReferences(self, request, context): 

61 with session_scope() as session: 

62 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

63 next_reference_id = int(request.page_token) if request.page_token else 0 

64 

65 if not request.from_user_id and not request.to_user_id: 

66 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.NEED_TO_SPECIFY_AT_LEAST_ONE_USER) 

67 

68 to_users = aliased(User) 

69 from_users = aliased(User) 

70 statement = select(Reference) 

71 if request.from_user_id: 

72 # join the to_users, because only interested if the recipient is visible 

73 statement = ( 

74 statement.join(to_users, Reference.to_user_id == to_users.id) 

75 .where( 

76 ~to_users.is_banned 

77 ) # instead of where_users_visible; if user is deleted or blocked, reference still visible 

78 .where(Reference.from_user_id == request.from_user_id) 

79 ) 

80 if request.to_user_id: 

81 # join the from_users, because only interested if the writer is visible 

82 statement = ( 

83 statement.join(from_users, Reference.from_user_id == from_users.id) 

84 .where( 

85 ~from_users.is_banned 

86 ) # instead of where_users_visible; if user is deleted or blocked, reference still visible 

87 .where(Reference.to_user_id == request.to_user_id) 

88 ) 

89 if len(request.reference_type_filter) > 0: 

90 statement = statement.where( 

91 Reference.reference_type.in_([reftype2sql[t] for t in request.reference_type_filter]) 

92 ) 

93 

94 if next_reference_id: 

95 statement = statement.where(Reference.id <= next_reference_id) 

96 

97 # Reference visibility logic: 

98 # A reference is visible if any of the following apply: 

99 # 1. It is a friend reference 

100 # 2. Both references have been written 

101 # 3. It has been over 2 weeks since the host request ended 

102 

103 # we get the matching other references through this subquery 

104 sub = select(Reference.id.label("sub_id"), Reference.host_request_id).where( 

105 Reference.reference_type != ReferenceType.friend 

106 ) 

107 if request.from_user_id: 

108 sub = sub.where(Reference.to_user_id == request.from_user_id) 

109 if request.to_user_id: 

110 sub = sub.where(Reference.from_user_id == request.to_user_id) 

111 

112 sub = sub.subquery() 

113 statement = ( 

114 statement.outerjoin(sub, sub.c.host_request_id == Reference.host_request_id) 

115 .outerjoin(HostRequest, HostRequest.conversation_id == Reference.host_request_id) 

116 .where( 

117 or_( 

118 Reference.reference_type == ReferenceType.friend, 

119 sub.c.sub_id != None, 

120 HostRequest.end_time_to_write_reference < func.now(), 

121 ) 

122 ) 

123 ) 

124 

125 statement = statement.order_by(Reference.id.desc()).limit(page_size + 1) 

126 references = session.execute(statement).scalars().all() 

127 

128 return references_pb2.ListReferencesRes( 

129 references=[reference_to_pb(reference, context) for reference in references[:page_size]], 

130 next_page_token=str(references[-1].id) if len(references) > page_size else None, 

131 ) 

132 

133 def WriteFriendReference(self, request, context): 

134 if context.user_id == request.to_user_id: 

135 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.CANT_REFER_SELF) 

136 

137 with session_scope() as session: 

138 check_valid_reference(request, context) 

139 

140 if not session.execute( 

141 select(User).where_users_visible(context).where(User.id == request.to_user_id) 

142 ).scalar_one_or_none(): 

143 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

144 

145 if session.execute( 

146 select(Reference) 

147 .where(Reference.from_user_id == context.user_id) 

148 .where(Reference.to_user_id == request.to_user_id) 

149 .where(Reference.reference_type == ReferenceType.friend) 

150 ).scalar_one_or_none(): 

151 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.REFERENCE_ALREADY_GIVEN) 

152 

153 reference = Reference( 

154 from_user_id=context.user_id, 

155 to_user_id=request.to_user_id, 

156 reference_type=ReferenceType.friend, 

157 text=request.text.strip(), 

158 private_text=request.private_text.strip(), 

159 rating=request.rating, 

160 was_appropriate=request.was_appropriate, 

161 ) 

162 session.add(reference) 

163 session.commit() 

164 

165 # send the recipient of the reference an email 

166 send_friend_reference_email(reference) 

167 

168 # possibly send out an alert to the mod team if the reference was bad 

169 maybe_send_reference_report_email(reference) 

170 

171 return reference_to_pb(reference, context) 

172 

173 def WriteHostRequestReference(self, request, context): 

174 with session_scope() as session: 

175 check_valid_reference(request, context) 

176 

177 host_request = session.execute( 

178 select(HostRequest) 

179 .where_users_column_visible(context, HostRequest.surfer_user_id) 

180 .where_users_column_visible(context, HostRequest.host_user_id) 

181 .where(HostRequest.conversation_id == request.host_request_id) 

182 .where(or_(HostRequest.surfer_user_id == context.user_id, HostRequest.host_user_id == context.user_id)) 

183 ).scalar_one_or_none() 

184 

185 if not host_request: 

186 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND) 

187 

188 if not host_request.can_write_reference: 

189 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_WRITE_REFERENCE_FOR_REQUEST) 

190 

191 if session.execute( 

192 select(Reference) 

193 .where(Reference.host_request_id == host_request.conversation_id) 

194 .where(Reference.from_user_id == context.user_id) 

195 ).scalar_one_or_none(): 

196 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.REFERENCE_ALREADY_GIVEN) 

197 

198 other_reference = session.execute( 

199 select(Reference) 

200 .where(Reference.host_request_id == host_request.conversation_id) 

201 .where(Reference.to_user_id == context.user_id) 

202 ).scalar_one_or_none() 

203 

204 reference = Reference( 

205 from_user_id=context.user_id, 

206 host_request_id=host_request.conversation_id, 

207 text=request.text.strip(), 

208 private_text=request.private_text.strip(), 

209 rating=request.rating, 

210 was_appropriate=request.was_appropriate, 

211 ) 

212 

213 if host_request.surfer_user_id == context.user_id: 

214 # we requested to surf with someone 

215 reference.reference_type = ReferenceType.surfed 

216 reference.to_user_id = host_request.host_user_id 

217 assert context.user_id == host_request.surfer_user_id 

218 else: 

219 # we hosted someone 

220 reference.reference_type = ReferenceType.hosted 

221 reference.to_user_id = host_request.surfer_user_id 

222 assert context.user_id == host_request.host_user_id 

223 

224 session.add(reference) 

225 session.commit() 

226 

227 # send the recipient of the reference an email 

228 send_host_reference_email(reference, both_written=other_reference is not None) 

229 

230 # possibly send out an alert to the mod team if the reference was bad 

231 maybe_send_reference_report_email(reference) 

232 

233 return reference_to_pb(reference, context) 

234 

235 def AvailableWriteReferences(self, request, context): 

236 # can't write anything for ourselves, but let's return empty so this can be used generically on profile page 

237 if request.to_user_id == context.user_id: 

238 return references_pb2.AvailableWriteReferencesRes() 

239 

240 with session_scope() as session: 

241 if not session.execute( 

242 select(User).where_users_visible(context).where(User.id == request.to_user_id) 

243 ).scalar_one_or_none(): 

244 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

245 

246 can_write_friend_reference = ( 

247 session.execute( 

248 select(Reference) 

249 .where(Reference.from_user_id == context.user_id) 

250 .where(Reference.to_user_id == request.to_user_id) 

251 .where(Reference.reference_type == ReferenceType.friend) 

252 ).scalar_one_or_none() 

253 ) is None 

254 

255 q1 = ( 

256 select(literal(True), HostRequest) 

257 .outerjoin( 

258 Reference, 

259 and_( 

260 Reference.host_request_id == HostRequest.conversation_id, 

261 Reference.from_user_id == context.user_id, 

262 ), 

263 ) 

264 .where(Reference.id == None) 

265 .where(HostRequest.can_write_reference) 

266 .where(HostRequest.surfer_user_id == context.user_id) 

267 .where(HostRequest.host_user_id == request.to_user_id) 

268 ) 

269 

270 q2 = ( 

271 select(literal(False), HostRequest) 

272 .outerjoin( 

273 Reference, 

274 and_( 

275 Reference.host_request_id == HostRequest.conversation_id, 

276 Reference.from_user_id == context.user_id, 

277 ), 

278 ) 

279 .where(Reference.id == None) 

280 .where(HostRequest.can_write_reference) 

281 .where(HostRequest.surfer_user_id == request.to_user_id) 

282 .where(HostRequest.host_user_id == context.user_id) 

283 ) 

284 

285 union = union_all(q1, q2).order_by(HostRequest.end_time_to_write_reference.asc()).subquery() 

286 union = select(union.c[0].label("surfed"), aliased(HostRequest, union)) 

287 host_request_references = session.execute(union).all() 

288 

289 return references_pb2.AvailableWriteReferencesRes( 

290 can_write_friend_reference=can_write_friend_reference, 

291 available_write_references=[ 

292 references_pb2.AvailableWriteReferenceType( 

293 host_request_id=host_request.conversation_id, 

294 reference_type=reftype2api[ReferenceType.surfed if surfed else ReferenceType.hosted], 

295 time_expires=Timestamp_from_datetime(host_request.end_time_to_write_reference), 

296 ) 

297 for surfed, host_request in host_request_references 

298 ], 

299 ) 

300 

301 def ListPendingReferencesToWrite(self, request, context): 

302 with session_scope() as session: 

303 q1 = ( 

304 select(literal(True), HostRequest) 

305 .outerjoin( 

306 Reference, 

307 and_( 

308 Reference.host_request_id == HostRequest.conversation_id, 

309 Reference.from_user_id == context.user_id, 

310 ), 

311 ) 

312 .where_users_column_visible(context, HostRequest.host_user_id) 

313 .where(Reference.id == None) 

314 .where(HostRequest.can_write_reference) 

315 .where(HostRequest.surfer_user_id == context.user_id) 

316 ) 

317 

318 q2 = ( 

319 select(literal(False), HostRequest) 

320 .outerjoin( 

321 Reference, 

322 and_( 

323 Reference.host_request_id == HostRequest.conversation_id, 

324 Reference.from_user_id == context.user_id, 

325 ), 

326 ) 

327 .where_users_column_visible(context, HostRequest.surfer_user_id) 

328 .where(Reference.id == None) 

329 .where(HostRequest.can_write_reference) 

330 .where(HostRequest.host_user_id == context.user_id) 

331 ) 

332 

333 union = union_all(q1, q2).order_by(HostRequest.end_time_to_write_reference.asc()).subquery() 

334 union = select(union.c[0].label("surfed"), aliased(HostRequest, union)) 

335 host_request_references = session.execute(union).all() 

336 

337 return references_pb2.ListPendingReferencesToWriteRes( 

338 pending_references=[ 

339 references_pb2.AvailableWriteReferenceType( 

340 host_request_id=host_request.conversation_id, 

341 reference_type=reftype2api[ReferenceType.surfed if surfed else ReferenceType.hosted], 

342 time_expires=Timestamp_from_datetime(host_request.end_time_to_write_reference), 

343 ) 

344 for surfed, host_request in host_request_references 

345 ], 

346 )