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""" 

9 

10import grpc 

11from sqlalchemy.orm import aliased 

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

13 

14from couchers import errors 

15from couchers.db import session_scope 

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

17from couchers.sql import couchers_select as select 

18from couchers.tasks import maybe_send_reference_report_email, send_friend_reference_email, send_host_reference_email 

19from couchers.utils import Timestamp_from_datetime 

20from proto import references_pb2, references_pb2_grpc 

21 

22reftype2sql = { 

23 references_pb2.ReferenceType.REFERENCE_TYPE_FRIEND: ReferenceType.friend, 

24 references_pb2.ReferenceType.REFERENCE_TYPE_SURFED: ReferenceType.surfed, 

25 references_pb2.ReferenceType.REFERENCE_TYPE_HOSTED: ReferenceType.hosted, 

26} 

27 

28reftype2api = { 

29 ReferenceType.friend: references_pb2.ReferenceType.REFERENCE_TYPE_FRIEND, 

30 ReferenceType.surfed: references_pb2.ReferenceType.REFERENCE_TYPE_SURFED, 

31 ReferenceType.hosted: references_pb2.ReferenceType.REFERENCE_TYPE_HOSTED, 

32} 

33 

34 

35def reference_to_pb(reference: Reference, context): 

36 return references_pb2.Reference( 

37 reference_id=reference.id, 

38 from_user_id=reference.from_user_id, 

39 to_user_id=reference.to_user_id, 

40 reference_type=reftype2api[reference.reference_type], 

41 text=reference.text, 

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

43 host_request_id=( 

44 reference.host_request_id if context.user_id in [reference.from_user_id, reference.to_user_id] else None 

45 ), 

46 ) 

47 

48 

49def check_valid_reference(request, context): 

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

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

52 

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

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

55 

56 

57MAX_PAGINATION_LENGTH = 25 

58 

59 

60class References(references_pb2_grpc.ReferencesServicer): 

61 def ListReferences(self, request, context): 

62 with session_scope() as session: 

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

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

65 

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

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

68 

69 to_users = aliased(User) 

70 from_users = aliased(User) 

71 statement = select(Reference) 

72 if request.from_user_id: 

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

74 statement = ( 

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

76 .where( 

77 ~to_users.is_banned 

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

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

80 ) 

81 if request.to_user_id: 

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

83 statement = ( 

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

85 .where( 

86 ~from_users.is_banned 

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

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

89 ) 

90 if len(request.reference_type_filter) > 0: 

91 statement = statement.where( 

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

93 ) 

94 

95 if next_reference_id: 

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

97 

98 # Reference visibility logic: 

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

100 # 1. It is a friend reference 

101 # 2. Both references have been written 

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

103 

104 # we get the matching other references through this subquery 

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

106 Reference.reference_type != ReferenceType.friend 

107 ) 

108 if request.from_user_id: 

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

110 if request.to_user_id: 

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

112 

113 sub = sub.subquery() 

114 statement = ( 

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

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

117 .where( 

118 or_( 

119 Reference.reference_type == ReferenceType.friend, 

120 sub.c.sub_id != None, 

121 HostRequest.end_time_to_write_reference < func.now(), 

122 ) 

123 ) 

124 ) 

125 

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

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

128 

129 return references_pb2.ListReferencesRes( 

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

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

132 ) 

133 

134 def WriteFriendReference(self, request, context): 

135 if context.user_id == request.to_user_id: 

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

137 

138 with session_scope() as session: 

139 check_valid_reference(request, context) 

140 

141 if not session.execute( 

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

143 ).scalar_one_or_none(): 

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

145 

146 if session.execute( 

147 select(Reference) 

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

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

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

151 ).scalar_one_or_none(): 

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

153 

154 reference = Reference( 

155 from_user_id=context.user_id, 

156 to_user_id=request.to_user_id, 

157 reference_type=ReferenceType.friend, 

158 text=request.text.strip(), 

159 private_text=request.private_text.strip(), 

160 rating=request.rating, 

161 was_appropriate=request.was_appropriate, 

162 ) 

163 session.add(reference) 

164 session.commit() 

165 

166 # send the recipient of the reference an email 

167 send_friend_reference_email(reference) 

168 

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

170 maybe_send_reference_report_email(reference) 

171 

172 return reference_to_pb(reference, context) 

173 

174 def WriteHostRequestReference(self, request, context): 

175 with session_scope() as session: 

176 check_valid_reference(request, context) 

177 

178 host_request = session.execute( 

179 select(HostRequest) 

180 .where_users_column_visible(context, HostRequest.surfer_user_id) 

181 .where_users_column_visible(context, HostRequest.host_user_id) 

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

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

184 ).scalar_one_or_none() 

185 

186 if not host_request: 

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

188 

189 if not host_request.can_write_reference: 

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

191 

192 if session.execute( 

193 select(Reference) 

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

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

196 ).scalar_one_or_none(): 

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

198 

199 other_reference = session.execute( 

200 select(Reference) 

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

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

203 ).scalar_one_or_none() 

204 

205 reference = Reference( 

206 from_user_id=context.user_id, 

207 host_request_id=host_request.conversation_id, 

208 text=request.text.strip(), 

209 private_text=request.private_text.strip(), 

210 rating=request.rating, 

211 was_appropriate=request.was_appropriate, 

212 ) 

213 

214 if host_request.surfer_user_id == context.user_id: 

215 # we requested to surf with someone 

216 reference.reference_type = ReferenceType.surfed 

217 reference.to_user_id = host_request.host_user_id 

218 assert context.user_id == host_request.surfer_user_id 

219 else: 

220 # we hosted someone 

221 reference.reference_type = ReferenceType.hosted 

222 reference.to_user_id = host_request.surfer_user_id 

223 assert context.user_id == host_request.host_user_id 

224 

225 session.add(reference) 

226 session.commit() 

227 

228 # send the recipient of the reference an email 

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

230 

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

232 maybe_send_reference_report_email(reference) 

233 

234 return reference_to_pb(reference, context) 

235 

236 def AvailableWriteReferences(self, request, context): 

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

238 if request.to_user_id == context.user_id: 

239 return references_pb2.AvailableWriteReferencesRes() 

240 

241 with session_scope() as session: 

242 if not session.execute( 

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

244 ).scalar_one_or_none(): 

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

246 

247 can_write_friend_reference = ( 

248 session.execute( 

249 select(Reference) 

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

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

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

253 ).scalar_one_or_none() 

254 ) is None 

255 

256 q1 = ( 

257 select(literal(True), HostRequest) 

258 .outerjoin( 

259 Reference, 

260 and_( 

261 Reference.host_request_id == HostRequest.conversation_id, 

262 Reference.from_user_id == context.user_id, 

263 ), 

264 ) 

265 .where(Reference.id == None) 

266 .where(HostRequest.can_write_reference) 

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

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

269 ) 

270 

271 q2 = ( 

272 select(literal(False), HostRequest) 

273 .outerjoin( 

274 Reference, 

275 and_( 

276 Reference.host_request_id == HostRequest.conversation_id, 

277 Reference.from_user_id == context.user_id, 

278 ), 

279 ) 

280 .where(Reference.id == None) 

281 .where(HostRequest.can_write_reference) 

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

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

284 ) 

285 

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

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

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

289 

290 return references_pb2.AvailableWriteReferencesRes( 

291 can_write_friend_reference=can_write_friend_reference, 

292 available_write_references=[ 

293 references_pb2.AvailableWriteReferenceType( 

294 host_request_id=host_request.conversation_id, 

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

296 time_expires=Timestamp_from_datetime(host_request.end_time_to_write_reference), 

297 ) 

298 for surfed, host_request in host_request_references 

299 ], 

300 ) 

301 

302 def ListPendingReferencesToWrite(self, request, context): 

303 with session_scope() as session: 

304 q1 = ( 

305 select(literal(True), HostRequest) 

306 .outerjoin( 

307 Reference, 

308 and_( 

309 Reference.host_request_id == HostRequest.conversation_id, 

310 Reference.from_user_id == context.user_id, 

311 ), 

312 ) 

313 .where_users_column_visible(context, HostRequest.host_user_id) 

314 .where(Reference.id == None) 

315 .where(HostRequest.can_write_reference) 

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

317 ) 

318 

319 q2 = ( 

320 select(literal(False), HostRequest) 

321 .outerjoin( 

322 Reference, 

323 and_( 

324 Reference.host_request_id == HostRequest.conversation_id, 

325 Reference.from_user_id == context.user_id, 

326 ), 

327 ) 

328 .where_users_column_visible(context, HostRequest.surfer_user_id) 

329 .where(Reference.id == None) 

330 .where(HostRequest.can_write_reference) 

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

332 ) 

333 

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

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

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

337 

338 return references_pb2.ListPendingReferencesToWriteRes( 

339 pending_references=[ 

340 references_pb2.AvailableWriteReferenceType( 

341 host_request_id=host_request.conversation_id, 

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

343 time_expires=Timestamp_from_datetime(host_request.end_time_to_write_reference), 

344 ) 

345 for surfed, host_request in host_request_references 

346 ], 

347 )