Coverage for src/couchers/models/host_requests.py: 97%
72 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-25 10:58 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-25 10:58 +0000
1import enum
2from datetime import date, datetime
3from typing import TYPE_CHECKING, Any
5from geoalchemy2 import Geometry
6from sqlalchemy import BigInteger, Boolean, Date, DateTime, Enum, Float, ForeignKey, Index, String, func, text
7from sqlalchemy.ext.hybrid import hybrid_property
8from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship
9from sqlalchemy.sql import expression
11from couchers.models.base import Base, Geom
12from couchers.utils import date_in_timezone, now
14if TYPE_CHECKING:
15 from couchers.models.conversations import Conversation
16 from couchers.models.moderation import ModerationState
17 from couchers.models.users import User
20class HostRequestStatus(enum.Enum):
21 pending = enum.auto()
22 accepted = enum.auto()
23 rejected = enum.auto()
24 confirmed = enum.auto()
25 cancelled = enum.auto()
28class HostRequestQuality(enum.Enum):
29 high_quality = enum.auto()
30 okay_quality = enum.auto()
31 low_quality = enum.auto()
34class HostRequest(Base):
35 """
36 A request to stay with a host
37 """
39 __tablename__ = "host_requests"
40 __moderation_author_column__ = "surfer_user_id"
42 conversation_id: Mapped[int] = mapped_column("id", ForeignKey("conversations.id"), primary_key=True)
43 surfer_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
44 host_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
46 # Unified Moderation System
47 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
49 hosting_city: Mapped[str] = mapped_column(String)
50 hosting_location: Mapped[Geom] = mapped_column(Geometry("POINT", srid=4326))
51 hosting_radius: Mapped[float] = mapped_column(Float)
53 # TODO: proper timezone handling
54 timezone = "Etc/UTC"
56 # dates in the timezone above
57 from_date: Mapped[date] = mapped_column(Date)
58 to_date: Mapped[date] = mapped_column(Date)
60 # timezone-aware start and end times of the request, can be compared to now()
61 start_time = column_property(date_in_timezone(from_date, timezone)) # type: ignore[arg-type]
62 end_time = column_property(date_in_timezone(to_date, timezone) + text("interval '1 days'")) # type: ignore[arg-type]
63 start_time_to_write_reference = column_property(date_in_timezone(to_date, timezone)) # type: ignore[arg-type]
64 # notice 1 day for midnight at the *end of the day*, then 14 days to write a ref
65 end_time_to_write_reference = column_property(date_in_timezone(to_date, timezone) + text("interval '15 days'")) # type: ignore[arg-type]
67 status: Mapped[HostRequestStatus] = mapped_column(Enum(HostRequestStatus))
68 is_host_archived: Mapped[bool] = mapped_column(Boolean, default=False, server_default=expression.false())
69 is_surfer_archived: Mapped[bool] = mapped_column(Boolean, default=False, server_default=expression.false())
71 host_last_seen_message_id: Mapped[int] = mapped_column(BigInteger, default=0)
72 surfer_last_seen_message_id: Mapped[int] = mapped_column(BigInteger, default=0)
74 # number of reference reminders sent out
75 host_sent_reference_reminders: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
76 surfer_sent_reference_reminders: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
77 host_sent_request_reminders: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
78 last_sent_request_reminder_time: Mapped[datetime] = mapped_column(
79 DateTime(timezone=True), server_default=func.now()
80 )
82 # reason why the host/surfer marked that they didn't meet up
83 # if null then they haven't marked it such
84 host_reason_didnt_meetup: Mapped[str | None] = mapped_column(String, nullable=True)
85 surfer_reason_didnt_meetup: Mapped[str | None] = mapped_column(String, nullable=True)
87 surfer: Mapped["User"] = relationship(
88 "User", backref="host_requests_sent", foreign_keys="HostRequest.surfer_user_id"
89 )
90 host: Mapped["User"] = relationship(
91 "User", backref="host_requests_received", foreign_keys="HostRequest.host_user_id"
92 )
93 conversation: Mapped["Conversation"] = relationship("Conversation")
94 moderation_state: Mapped["ModerationState"] = relationship("ModerationState")
96 __table_args__ = (
97 # allows fast lookup as to whether they didn't meet up
98 Index(
99 "ix_host_requests_host_didnt_meetup",
100 host_reason_didnt_meetup != None,
101 ),
102 Index(
103 "ix_host_requests_surfer_didnt_meetup",
104 surfer_reason_didnt_meetup != None,
105 ),
106 # Used for figuring out who needs a reminder to respond
107 Index(
108 "ix_host_requests_status_reminder_counts",
109 status,
110 host_sent_request_reminders,
111 last_sent_request_reminder_time,
112 from_date,
113 ),
114 )
116 @hybrid_property
117 def can_write_reference(self) -> Any:
118 return (
119 ((self.status == HostRequestStatus.confirmed) | (self.status == HostRequestStatus.accepted))
120 & (now() >= self.start_time_to_write_reference)
121 & (now() <= self.end_time_to_write_reference)
122 )
124 @can_write_reference.expression
125 def can_write_reference_expr(cls) -> Any: # noqa: ARG003,D102
126 return (
127 ((cls.status == HostRequestStatus.confirmed) | (cls.status == HostRequestStatus.accepted))
128 & (func.now() >= cls.start_time_to_write_reference)
129 & (func.now() <= cls.end_time_to_write_reference)
130 )
132 def __repr__(self) -> str:
133 return f"HostRequest(id={self.conversation_id}, surfer_user_id={self.surfer_user_id}, host_user_id={self.host_user_id}...)"
136class HostRequestFeedback(Base):
137 """
138 Private feedback from a host about a host request
139 """
141 __tablename__ = "host_request_feedbacks"
143 id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
144 time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
145 host_request_id: Mapped[int] = mapped_column(ForeignKey("host_requests.id"))
147 from_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
148 to_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
150 request_quality: Mapped[HostRequestQuality | None] = mapped_column(Enum(HostRequestQuality), nullable=True)
151 decline_reason: Mapped[str | None] = mapped_column(String, nullable=True) # plain text
153 host_request: Mapped["HostRequest"] = relationship("HostRequest")
155 __table_args__ = (
156 # Each user can leave at most one friend reference to another user
157 Index(
158 "ix_unique_host_req_feedback",
159 from_user_id,
160 to_user_id,
161 host_request_id,
162 unique=True,
163 ),
164 )