Coverage for app/backend/src/couchers/helpers/references.py: 100%
13 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-07 07:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-07 07:42 +0000
1from typing import TYPE_CHECKING, Any
3from sqlalchemy import select
4from sqlalchemy.orm import InstrumentedAttribute, aliased
5from sqlalchemy.sql import Select, exists, func, or_
7from couchers.models import HostRequest, Reference, ReferenceType, User
8from couchers.sql import _shadow_clause
10if TYPE_CHECKING:
11 from couchers.context import CouchersContext
14def where_reference_user_visible[T: tuple[Any, ...]](
15 statement: Select[T], context: CouchersContext, user_id_column: InstrumentedAttribute[int]
16) -> Select[T]:
17 """
18 Filters references based on the visibility of the user in the given column (the writer
19 or the subject of the reference).
21 Deliberately weaker than users_visible: references involving deleted or blocked users
22 stay visible so reference history is preserved; only banned or shadowed (to others)
23 users hide their references. Both the reference list (ListReferences) and the reference
24 count (get_num_references) must use this, otherwise the count diverges from the list.
25 """
26 return statement.where(
27 exists(
28 select(1)
29 .select_from(User)
30 .where(User.id == user_id_column)
31 .where(User.banned_at.is_(None))
32 .where(_shadow_clause(context, User))
33 .correlate_except(User)
34 )
35 )
38def where_references_not_hidden_by_reciprocity[T: tuple[Any, ...]](statement: Select[T]) -> Select[T]:
39 """
40 Filters out references that are still hidden by the reciprocal-reference rule.
42 A host/surf reference stays hidden until either the recipient has written their
43 reciprocal reference or the 2-week window to write one has closed; friend
44 references are always visible.
46 Apply this to any query that selects from Reference. Both the reference list
47 (ListReferences) and the reference count (get_num_references) must use it,
48 otherwise the count includes references the list hides, leaking the existence
49 of a still-hidden reference.
50 """
51 other_reference = aliased(Reference)
52 reciprocal_written = exists(
53 select(other_reference.id)
54 .where(other_reference.host_request_id == Reference.host_request_id)
55 .where(other_reference.from_user_id == Reference.to_user_id)
56 .where(other_reference.reference_type != ReferenceType.friend)
57 )
58 window_closed = exists(
59 select(HostRequest.conversation_id)
60 .where(HostRequest.conversation_id == Reference.host_request_id)
61 .where(HostRequest.end_time_to_write_reference < func.now())
62 )
63 return statement.where(
64 or_(
65 Reference.reference_type == ReferenceType.friend,
66 reciprocal_written,
67 window_closed,
68 )
69 )