Coverage for app/backend/src/couchers/models/public_trips.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-10 18:39 +0000

1import enum 

2from datetime import date, datetime 

3from typing import TYPE_CHECKING 

4 

5from sqlalchemy import ( 

6 BigInteger, 

7 Boolean, 

8 CheckConstraint, 

9 Date, 

10 DateTime, 

11 Enum, 

12 ForeignKey, 

13 Index, 

14 String, 

15 func, 

16 text, 

17) 

18from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship 

19from sqlalchemy.sql import expression 

20 

21from couchers.models.base import Base 

22from couchers.models.moderation import ModerationObjectType 

23from couchers.utils import date_in_timezone 

24 

25if TYPE_CHECKING: 

26 from couchers.models import Node, User 

27 from couchers.models.host_requests import HostRequest 

28 from couchers.models.moderation import ModerationState 

29 

30 

31class PublicTripStatus(enum.Enum): 

32 searching_for_host = enum.auto() 

33 closed = enum.auto() 

34 

35 

36class PublicTrip(Base, kw_only=True): 

37 """ 

38 A public trip posted by a traveler looking for a host in a community. 

39 """ 

40 

41 __tablename__ = "public_trips" 

42 __moderation_author_column__ = "user_id" 

43 __moderation_object_type__ = ModerationObjectType.public_trip 

44 __moderation_has_own_visibility_mechanism__ = False 

45 

46 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

47 

48 # The traveler posting the trip 

49 user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) 

50 

51 # The community/location (city-level node) 

52 node_id: Mapped[int] = mapped_column(ForeignKey("nodes.id"), index=True) 

53 

54 # TODO: proper timezone handling 

55 timezone = "Etc/UTC" 

56 

57 # Trip dates 

58 from_date: Mapped[date] = mapped_column(Date) 

59 to_date: Mapped[date] = mapped_column(Date) 

60 

61 # timezone-aware end time of the trip, can be compared to now() 

62 end_time = column_property(date_in_timezone(to_date, timezone) + text("interval '1 days'")) 

63 

64 # User's message about their trip 

65 description: Mapped[str] = mapped_column(String) 

66 

67 # Current status 

68 status: Mapped[PublicTripStatus] = mapped_column( 

69 Enum(PublicTripStatus), default=PublicTripStatus.searching_for_host 

70 ) 

71 

72 # If true, only users with the same gender as the poster can see this trip 

73 same_gender_only: Mapped[bool] = mapped_column(Boolean, default=False, server_default=expression.false()) 

74 

75 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True) 

76 

77 # Timestamps 

78 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

79 

80 # Relationships 

81 user: Mapped[User] = relationship(init=False, back_populates="public_trips") 

82 node: Mapped[Node] = relationship(init=False, back_populates="public_trips") 

83 host_requests: Mapped[list[HostRequest]] = relationship(init=False, back_populates="public_trip") 

84 moderation_state: Mapped[ModerationState] = relationship(init=False) 

85 

86 __table_args__ = ( 

87 # Ensure from_date is not after to_date 

88 CheckConstraint("from_date <= to_date", name="valid_date_range"), 

89 # Index for querying active trips in a community 

90 # Using partial index since we mostly query for active trips 

91 Index( 

92 "ix_public_trips_node_from_date_active", 

93 node_id, 

94 from_date, 

95 postgresql_where=status == PublicTripStatus.searching_for_host, 

96 ), 

97 )