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
« 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
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
21from couchers.models.base import Base
22from couchers.models.moderation import ModerationObjectType
23from couchers.utils import date_in_timezone
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
31class PublicTripStatus(enum.Enum):
32 searching_for_host = enum.auto()
33 closed = enum.auto()
36class PublicTrip(Base, kw_only=True):
37 """
38 A public trip posted by a traveler looking for a host in a community.
39 """
41 __tablename__ = "public_trips"
42 __moderation_author_column__ = "user_id"
43 __moderation_object_type__ = ModerationObjectType.public_trip
44 __moderation_has_own_visibility_mechanism__ = False
46 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
48 # The traveler posting the trip
49 user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
51 # The community/location (city-level node)
52 node_id: Mapped[int] = mapped_column(ForeignKey("nodes.id"), index=True)
54 # TODO: proper timezone handling
55 timezone = "Etc/UTC"
57 # Trip dates
58 from_date: Mapped[date] = mapped_column(Date)
59 to_date: Mapped[date] = mapped_column(Date)
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'"))
64 # User's message about their trip
65 description: Mapped[str] = mapped_column(String)
67 # Current status
68 status: Mapped[PublicTripStatus] = mapped_column(
69 Enum(PublicTripStatus), default=PublicTripStatus.searching_for_host
70 )
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())
75 moderation_state_id: Mapped[int] = mapped_column(ForeignKey("moderation_states.id"), index=True)
77 # Timestamps
78 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
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)
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 )