Coverage for src/couchers/models/host_requests.py: 99%
67 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-08 00:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-08 00:20 +0000
1import enum
3from geoalchemy2 import Geometry
4from sqlalchemy import BigInteger, Boolean, Column, Date, DateTime, Enum, Float, ForeignKey, Index, String, func, text
5from sqlalchemy.ext.hybrid import hybrid_property
6from sqlalchemy.orm import column_property, relationship
7from sqlalchemy.sql import expression
9from couchers.models.base import Base
10from couchers.utils import date_in_timezone, now
13class HostRequestStatus(enum.Enum):
14 pending = enum.auto()
15 accepted = enum.auto()
16 rejected = enum.auto()
17 confirmed = enum.auto()
18 cancelled = enum.auto()
21class HostRequestQuality(enum.Enum):
22 high_quality = enum.auto()
23 okay_quality = enum.auto()
24 low_quality = enum.auto()
27class HostRequest(Base):
28 """
29 A request to stay with a host
30 """
32 __tablename__ = "host_requests"
34 conversation_id = Column("id", ForeignKey("conversations.id"), nullable=False, primary_key=True)
35 surfer_user_id = Column(ForeignKey("users.id"), nullable=False, index=True)
36 host_user_id = Column(ForeignKey("users.id"), nullable=False, index=True)
38 hosting_city = Column(String, nullable=False)
39 hosting_location = Column(Geometry("POINT", srid=4326), nullable=False)
40 hosting_radius = Column(Float, nullable=False)
42 # TODO: proper timezone handling
43 timezone = "Etc/UTC"
45 # dates in the timezone above
46 from_date = Column(Date, nullable=False)
47 to_date = Column(Date, nullable=False)
49 # timezone aware start and end times of the request, can be compared to now()
50 start_time = column_property(date_in_timezone(from_date, timezone))
51 end_time = column_property(date_in_timezone(to_date, timezone) + text("interval '1 days'"))
52 start_time_to_write_reference = column_property(date_in_timezone(to_date, timezone))
53 # notice 1 day for midnight at the *end of the day*, then 14 days to write a ref
54 end_time_to_write_reference = column_property(date_in_timezone(to_date, timezone) + text("interval '15 days'"))
56 status = Column(Enum(HostRequestStatus), nullable=False)
57 is_host_archived = Column(Boolean, nullable=False, default=False, server_default=expression.false())
58 is_surfer_archived = Column(Boolean, nullable=False, default=False, server_default=expression.false())
60 host_last_seen_message_id = Column(BigInteger, nullable=False, default=0)
61 surfer_last_seen_message_id = Column(BigInteger, nullable=False, default=0)
63 # number of reference reminders sent out
64 host_sent_reference_reminders = Column(BigInteger, nullable=False, server_default=text("0"))
65 surfer_sent_reference_reminders = Column(BigInteger, nullable=False, server_default=text("0"))
66 host_sent_request_reminders = Column(BigInteger, nullable=False, server_default=text("0"))
67 last_sent_request_reminder_time = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
69 # reason why the host/surfer marked that they didn't meet up
70 # if null then they haven't marked it such
71 host_reason_didnt_meetup = Column(String, nullable=True)
72 surfer_reason_didnt_meetup = Column(String, nullable=True)
74 surfer = relationship("User", backref="host_requests_sent", foreign_keys="HostRequest.surfer_user_id")
75 host = relationship("User", backref="host_requests_received", foreign_keys="HostRequest.host_user_id")
76 conversation = relationship("Conversation")
78 __table_args__ = (
79 # allows fast lookup as to whether they didn't meet up
80 Index(
81 "ix_host_requests_host_didnt_meetup",
82 host_reason_didnt_meetup != None,
83 ),
84 Index(
85 "ix_host_requests_surfer_didnt_meetup",
86 surfer_reason_didnt_meetup != None,
87 ),
88 # Used for figuring out who needs a reminder to respond
89 Index(
90 "ix_host_requests_status_reminder_counts",
91 status,
92 host_sent_request_reminders,
93 last_sent_request_reminder_time,
94 from_date,
95 ),
96 )
98 @hybrid_property
99 def can_write_reference(self):
100 return (
101 ((self.status == HostRequestStatus.confirmed) | (self.status == HostRequestStatus.accepted))
102 & (now() >= self.start_time_to_write_reference)
103 & (now() <= self.end_time_to_write_reference)
104 )
106 @can_write_reference.expression
107 def can_write_reference(cls):
108 return (
109 ((cls.status == HostRequestStatus.confirmed) | (cls.status == HostRequestStatus.accepted))
110 & (func.now() >= cls.start_time_to_write_reference)
111 & (func.now() <= cls.end_time_to_write_reference)
112 )
114 def __repr__(self):
115 return f"HostRequest(id={self.conversation_id}, surfer_user_id={self.surfer_user_id}, host_user_id={self.host_user_id}...)"
118class HostRequestFeedback(Base):
119 """
120 Private feedback from a host about a host request
121 """
123 __tablename__ = "host_request_feedbacks"
125 id = Column(BigInteger, primary_key=True)
126 time = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
127 host_request_id = Column(ForeignKey("host_requests.id"), nullable=False)
129 from_user_id = Column(ForeignKey("users.id"), nullable=False, index=True)
130 to_user_id = Column(ForeignKey("users.id"), nullable=False, index=True)
132 request_quality = Column(Enum(HostRequestQuality), nullable=True)
133 decline_reason = Column(String, nullable=True) # plain text
135 host_request = relationship("HostRequest")
137 __table_args__ = (
138 # Each user can leave at most one friend reference to another user
139 Index(
140 "ix_unique_host_req_feedback",
141 from_user_id,
142 to_user_id,
143 host_request_id,
144 unique=True,
145 ),
146 )