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"""
9import grpc
10from sqlalchemy.orm import aliased
11from sqlalchemy.sql import and_, func, literal, or_, union_all
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
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}
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}
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 )
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)
52 if request.text.strip() == "":
53 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.REFERENCE_NO_TEXT)
56MAX_PAGINATION_LENGTH = 25
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
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)
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 )
94 if next_reference_id:
95 statement = statement.where(Reference.id <= next_reference_id)
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
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)
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 )
125 statement = statement.order_by(Reference.id.desc()).limit(page_size + 1)
126 references = session.execute(statement).scalars().all()
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 )
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)
137 with session_scope() as session:
138 check_valid_reference(request, context)
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)
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)
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()
165 # send the recipient of the reference an email
166 send_friend_reference_email(reference)
168 # possibly send out an alert to the mod team if the reference was bad
169 maybe_send_reference_report_email(reference)
171 return reference_to_pb(reference, context)
173 def WriteHostRequestReference(self, request, context):
174 with session_scope() as session:
175 check_valid_reference(request, context)
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()
185 if not host_request:
186 context.abort(grpc.StatusCode.NOT_FOUND, errors.HOST_REQUEST_NOT_FOUND)
188 if not host_request.can_write_reference:
189 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_WRITE_REFERENCE_FOR_REQUEST)
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)
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()
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 )
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
224 session.add(reference)
225 session.commit()
227 # send the recipient of the reference an email
228 send_host_reference_email(reference, both_written=other_reference is not None)
230 # possibly send out an alert to the mod team if the reference was bad
231 maybe_send_reference_report_email(reference)
233 return reference_to_pb(reference, context)
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()
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)
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
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 )
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 )
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()
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 )
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 )
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 )
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()
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 )