Skip to content

Commit 3346e88

Browse files
authored
Merge pull request #7723 from Couchers-org/na/backend/public-trips-table
Backend: Add public_trips models and migration
2 parents 2e10745 + c1d34cb commit 3346e88

File tree

7 files changed

+394
-0
lines changed

7 files changed

+394
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Add public trips
2+
3+
Revision ID: 0139
4+
Revises: 0138
5+
Create Date: 2026-01-22 12:00:00.000000
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "0139"
14+
down_revision = "0138"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# Create public_trips table
21+
op.create_table(
22+
"public_trips",
23+
sa.Column("id", sa.BigInteger(), nullable=False),
24+
sa.Column("user_id", sa.BigInteger(), nullable=False),
25+
sa.Column("node_id", sa.BigInteger(), nullable=False),
26+
sa.Column("from_date", sa.Date(), nullable=False),
27+
sa.Column("to_date", sa.Date(), nullable=False),
28+
sa.Column("description", sa.String(), nullable=False),
29+
sa.Column(
30+
"status",
31+
sa.Enum("searching_for_host", "closed", name="publictripstatus"),
32+
nullable=False,
33+
),
34+
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
35+
sa.CheckConstraint("from_date <= to_date", name=op.f("ck_public_trips_valid_date_range")),
36+
sa.ForeignKeyConstraint(["node_id"], ["nodes.id"], name=op.f("fk_public_trips_node_id_nodes")),
37+
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name=op.f("fk_public_trips_user_id_users")),
38+
sa.PrimaryKeyConstraint("id", name=op.f("pk_public_trips")),
39+
)
40+
op.create_index(op.f("ix_public_trips_node_id"), "public_trips", ["node_id"], unique=False)
41+
op.create_index(op.f("ix_public_trips_user_id"), "public_trips", ["user_id"], unique=False)
42+
op.create_index(
43+
"ix_public_trips_node_from_date_active",
44+
"public_trips",
45+
["node_id", "from_date"],
46+
unique=False,
47+
postgresql_where=sa.text("status = 'searching_for_host'"),
48+
)
49+
50+
# Add public_trip_id to host_requests
51+
op.add_column("host_requests", sa.Column("public_trip_id", sa.BigInteger(), nullable=True))
52+
op.create_index(op.f("ix_host_requests_public_trip_id"), "host_requests", ["public_trip_id"], unique=False)
53+
op.create_foreign_key(
54+
op.f("fk_host_requests_public_trip_id_public_trips"),
55+
"host_requests",
56+
"public_trips",
57+
["public_trip_id"],
58+
["id"],
59+
)
60+
61+
62+
def downgrade() -> None:
63+
# Remove public_trip_id from host_requests
64+
op.drop_constraint(op.f("fk_host_requests_public_trip_id_public_trips"), "host_requests", type_="foreignkey")
65+
op.drop_index(op.f("ix_host_requests_public_trip_id"), table_name="host_requests")
66+
op.drop_column("host_requests", "public_trip_id")
67+
68+
# Drop public_trips table
69+
op.drop_index("ix_public_trips_node_from_date_active", table_name="public_trips")
70+
op.drop_index(op.f("ix_public_trips_user_id"), table_name="public_trips")
71+
op.drop_index(op.f("ix_public_trips_node_id"), table_name="public_trips")
72+
op.drop_table("public_trips")
73+
74+
# Drop enum
75+
sa.Enum(name="publictripstatus").drop(op.get_bind(), checkfirst=True)

app/backend/src/couchers/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .moderation import * # noqa: F401,F403
1515
from .notifications import * # noqa: F401,F403
1616
from .postal_verification import * # noqa: F401,F403
17+
from .public_trips import * # noqa: F401,F403
1718
from .rest import * # noqa: F401,F403
1819
from .static import * # noqa: F401,F403
1920
from .uploads import * # noqa: F401,F403

app/backend/src/couchers/models/clusters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
if TYPE_CHECKING:
3333
from couchers.models import Discussion, Event, Thread, Upload, User
34+
from couchers.models.public_trips import PublicTrip
3435

3536

3637
class NodeType(enum.Enum):
@@ -79,6 +80,7 @@ class Node(Base, kw_only=True):
7980
uselist=False,
8081
viewonly=True,
8182
)
83+
public_trips: Mapped[list[PublicTrip]] = relationship(init=False, back_populates="node")
8284

8385

8486
class Cluster(Base, kw_only=True):

app/backend/src/couchers/models/host_requests.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
if TYPE_CHECKING:
1515
from couchers.models.conversations import Conversation
1616
from couchers.models.moderation import ModerationState
17+
from couchers.models.public_trips import PublicTrip
1718
from couchers.models.users import User
1819

1920

@@ -84,6 +85,9 @@ class HostRequest(Base, kw_only=True):
8485
host_reason_didnt_meetup: Mapped[str | None] = mapped_column(String, default=None)
8586
surfer_reason_didnt_meetup: Mapped[str | None] = mapped_column(String, default=None)
8687

88+
# Optional link to public trip if this request originated from one
89+
public_trip_id: Mapped[int | None] = mapped_column(ForeignKey("public_trips.id"), index=True, default=None)
90+
8791
surfer: Mapped[User] = relationship(
8892
init=False, backref="host_requests_sent", foreign_keys="HostRequest.surfer_user_id"
8993
)
@@ -92,6 +96,7 @@ class HostRequest(Base, kw_only=True):
9296
)
9397
conversation: Mapped[Conversation] = relationship(init=False)
9498
moderation_state: Mapped[ModerationState] = relationship(init=False)
99+
public_trip: Mapped[PublicTrip | None] = relationship(init=False, back_populates="host_requests")
95100

96101
__table_args__ = (
97102
# allows fast lookup as to whether they didn't meet up
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import enum
2+
from datetime import date, datetime
3+
from typing import TYPE_CHECKING
4+
5+
from sqlalchemy import BigInteger, CheckConstraint, Date, DateTime, Enum, ForeignKey, Index, String, func
6+
from sqlalchemy.orm import Mapped, mapped_column, relationship
7+
8+
from couchers.models.base import Base
9+
10+
if TYPE_CHECKING:
11+
from couchers.models import Node, User
12+
from couchers.models.host_requests import HostRequest
13+
14+
15+
class PublicTripStatus(enum.Enum):
16+
searching_for_host = enum.auto()
17+
closed = enum.auto()
18+
19+
20+
class PublicTrip(Base, kw_only=True):
21+
"""
22+
A public trip posted by a traveler looking for a host in a community.
23+
"""
24+
25+
__tablename__ = "public_trips"
26+
27+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False)
28+
29+
# The traveler posting the trip
30+
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
31+
32+
# The community/location (city-level node)
33+
node_id: Mapped[int] = mapped_column(ForeignKey("nodes.id"), index=True)
34+
35+
# Trip dates
36+
from_date: Mapped[date] = mapped_column(Date)
37+
to_date: Mapped[date] = mapped_column(Date)
38+
39+
# User's message about their trip
40+
description: Mapped[str] = mapped_column(String)
41+
42+
# Current status
43+
status: Mapped[PublicTripStatus] = mapped_column(
44+
Enum(PublicTripStatus), default=PublicTripStatus.searching_for_host
45+
)
46+
47+
# Timestamps
48+
created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False)
49+
50+
# Relationships
51+
user: Mapped[User] = relationship(init=False, back_populates="public_trips")
52+
node: Mapped[Node] = relationship(init=False, back_populates="public_trips")
53+
host_requests: Mapped[list[HostRequest]] = relationship(init=False, back_populates="public_trip")
54+
55+
__table_args__ = (
56+
# Ensure from_date is not after to_date
57+
CheckConstraint("from_date <= to_date", name="valid_date_range"),
58+
# Index for querying active trips in a community
59+
# Using partial index since we mostly query for active trips
60+
Index(
61+
"ix_public_trips_node_from_date_active",
62+
node_id,
63+
from_date,
64+
postgresql_where=status == PublicTripStatus.searching_for_host,
65+
),
66+
)

app/backend/src/couchers/models/users.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
if TYPE_CHECKING:
4747
from couchers.models import UserBadge
4848
from couchers.models.admin import UserAdminTag
49+
from couchers.models.public_trips import PublicTrip
4950
from couchers.models.rest import InviteCode, ModerationUserList
5051
from couchers.models.uploads import PhotoGallery
5152

@@ -355,6 +356,8 @@ class User(Base, kw_only=True):
355356
back_populates="user",
356357
)
357358

359+
public_trips: Mapped[list[PublicTrip]] = relationship(init=False, back_populates="user")
360+
358361
__table_args__ = (
359362
# Verified phone numbers should be unique
360363
Index(

0 commit comments

Comments
 (0)