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

1import enum 

2 

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 

8 

9from couchers.models.base import Base 

10from couchers.utils import date_in_timezone, now 

11 

12 

13class HostRequestStatus(enum.Enum): 

14 pending = enum.auto() 

15 accepted = enum.auto() 

16 rejected = enum.auto() 

17 confirmed = enum.auto() 

18 cancelled = enum.auto() 

19 

20 

21class HostRequestQuality(enum.Enum): 

22 high_quality = enum.auto() 

23 okay_quality = enum.auto() 

24 low_quality = enum.auto() 

25 

26 

27class HostRequest(Base): 

28 """ 

29 A request to stay with a host 

30 """ 

31 

32 __tablename__ = "host_requests" 

33 

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) 

37 

38 hosting_city = Column(String, nullable=False) 

39 hosting_location = Column(Geometry("POINT", srid=4326), nullable=False) 

40 hosting_radius = Column(Float, nullable=False) 

41 

42 # TODO: proper timezone handling 

43 timezone = "Etc/UTC" 

44 

45 # dates in the timezone above 

46 from_date = Column(Date, nullable=False) 

47 to_date = Column(Date, nullable=False) 

48 

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'")) 

55 

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()) 

59 

60 host_last_seen_message_id = Column(BigInteger, nullable=False, default=0) 

61 surfer_last_seen_message_id = Column(BigInteger, nullable=False, default=0) 

62 

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()) 

68 

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) 

73 

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") 

77 

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 ) 

97 

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 ) 

105 

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 ) 

113 

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}...)" 

116 

117 

118class HostRequestFeedback(Base): 

119 """ 

120 Private feedback from a host about a host request 

121 """ 

122 

123 __tablename__ = "host_request_feedbacks" 

124 

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) 

128 

129 from_user_id = Column(ForeignKey("users.id"), nullable=False, index=True) 

130 to_user_id = Column(ForeignKey("users.id"), nullable=False, index=True) 

131 

132 request_quality = Column(Enum(HostRequestQuality), nullable=True) 

133 decline_reason = Column(String, nullable=True) # plain text 

134 

135 host_request = relationship("HostRequest") 

136 

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 )