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
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
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}
7* TODO: Get bugged about writing reference 1 day after, 1 week after, 2weeks-2days
8"""
10import grpc
11from sqlalchemy.orm import aliased
12from sqlalchemy.sql import and_, func, literal, or_, union_all
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
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}
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}
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 )
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)
53 if request.text.strip() == "":
54 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REFERENCE_NO_TEXT)
57MAX_PAGINATION_LENGTH = 25
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
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)
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 )
95 if next_reference_id:
96 statement = statement.where(Reference.id <= next_reference_id)
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
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)
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 )
126 statement = statement.order_by(Reference.id.desc()).limit(page_size + 1)
127 references = session.execute(statement).scalars().all()
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 )
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)
138 with session_scope() as session:
139 check_valid_reference(request, context)
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)
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)
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()
166 # send the recipient of the reference an email
167 send_friend_reference_email(reference)
169 # possibly send out an alert to the mod team if the reference was bad
170 maybe_send_reference_report_email(reference)
172 return reference_to_pb(reference, context)
174 def WriteHostRequestReference(self, request, context):
175 with session_scope() as session:
176 check_valid_reference(request, context)
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()
186 if not host_request:
187 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
189 if not host_request.can_write_reference:
190 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_WRITE_REFERENCE_FOR_REQUEST)
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)
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()
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 )
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
225 session.add(reference)
226 session.commit()
228 # send the recipient of the reference an email
229 send_host_reference_email(reference, both_written=other_reference is not None)
231 # possibly send out an alert to the mod team if the reference was bad
232 maybe_send_reference_report_email(reference)
234 return reference_to_pb(reference, context)
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()
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)
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
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 )
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 )
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()
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 )
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 )
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 )
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()
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 )