Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Add public trips

Revision ID: 981fb62b20ce
Revises: e9190b051324
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring mismatch (LOW): This says Revises: e9190b051324 but the actual down_revision on line 14 is "0138". Cosmetic only — Alembic uses the variable — but should be corrected for clarity.

Suggested change
Revises: e9190b051324
Revises: 0138

Create Date: 2026-01-22 12:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "981fb62b20ce"
down_revision = "0138"
branch_labels = None
depends_on = None


def upgrade() -> None:
# Create public_trips table
op.create_table(
"public_trips",
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("node_id", sa.BigInteger(), nullable=False),
sa.Column("from_date", sa.Date(), nullable=False),
sa.Column("to_date", sa.Date(), nullable=False),
sa.Column("description", sa.String(), nullable=False),
sa.Column(
"status",
sa.Enum("searching_for_host", "closed", name="publictripstatus"),
nullable=False,
),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.CheckConstraint("from_date <= to_date", name=op.f("ck_public_trips_valid_date_range")),
sa.ForeignKeyConstraint(["node_id"], ["nodes.id"], name=op.f("fk_public_trips_node_id_nodes")),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], name=op.f("fk_public_trips_user_id_users")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_public_trips")),
)
op.create_index(op.f("ix_public_trips_node_id"), "public_trips", ["node_id"], unique=False)
op.create_index(op.f("ix_public_trips_user_id"), "public_trips", ["user_id"], unique=False)
op.create_index(
"ix_public_trips_node_from_date_active",
"public_trips",
["node_id", "from_date"],
unique=False,
postgresql_where=sa.text("status = 'searching_for_host'"),
)

# Add public_trip_id to host_requests
op.add_column("host_requests", sa.Column("public_trip_id", sa.BigInteger(), nullable=True))
op.create_index(op.f("ix_host_requests_public_trip_id"), "host_requests", ["public_trip_id"], unique=False)
op.create_foreign_key(
op.f("fk_host_requests_public_trip_id_public_trips"),
"host_requests",
"public_trips",
["public_trip_id"],
["id"],
)


def downgrade() -> None:
# Remove public_trip_id from host_requests
op.drop_constraint(op.f("fk_host_requests_public_trip_id_public_trips"), "host_requests", type_="foreignkey")
op.drop_index(op.f("ix_host_requests_public_trip_id"), table_name="host_requests")
op.drop_column("host_requests", "public_trip_id")

# Drop public_trips table
op.drop_index("ix_public_trips_node_from_date_active", table_name="public_trips")
op.drop_index(op.f("ix_public_trips_user_id"), table_name="public_trips")
op.drop_index(op.f("ix_public_trips_node_id"), table_name="public_trips")
op.drop_table("public_trips")

# Drop enum
sa.Enum(name="publictripstatus").drop(op.get_bind(), checkfirst=True)
1 change: 1 addition & 0 deletions app/backend/src/couchers/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .moderation import * # noqa: F401,F403
from .notifications import * # noqa: F401,F403
from .postal_verification import * # noqa: F401,F403
from .public_trips import * # noqa: F401,F403
from .rest import * # noqa: F401,F403
from .static import * # noqa: F401,F403
from .uploads import * # noqa: F401,F403
Expand Down
2 changes: 2 additions & 0 deletions app/backend/src/couchers/models/clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

if TYPE_CHECKING:
from couchers.models import Discussion, Event, Thread, Upload, User
from couchers.models.public_trips import PublicTrip


class NodeType(enum.Enum):
Expand Down Expand Up @@ -79,6 +80,7 @@ class Node(Base, kw_only=True):
uselist=False,
viewonly=True,
)
public_trips: Mapped[list[PublicTrip]] = relationship(init=False, back_populates="node")


class Cluster(Base, kw_only=True):
Expand Down
5 changes: 5 additions & 0 deletions app/backend/src/couchers/models/host_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
if TYPE_CHECKING:
from couchers.models.conversations import Conversation
from couchers.models.moderation import ModerationState
from couchers.models.public_trips import PublicTrip
from couchers.models.users import User


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

# Optional link to public trip if this request originated from one
public_trip_id: Mapped[int | None] = mapped_column(ForeignKey("public_trips.id"), index=True, default=None)

surfer: Mapped[User] = relationship(
init=False, backref="host_requests_sent", foreign_keys="HostRequest.surfer_user_id"
)
Expand All @@ -92,6 +96,7 @@ class HostRequest(Base, kw_only=True):
)
conversation: Mapped[Conversation] = relationship(init=False)
moderation_state: Mapped[ModerationState] = relationship(init=False)
public_trip: Mapped[PublicTrip | None] = relationship(init=False, back_populates="host_requests")

__table_args__ = (
# allows fast lookup as to whether they didn't meet up
Expand Down
66 changes: 66 additions & 0 deletions app/backend/src/couchers/models/public_trips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import enum
from datetime import date, datetime
from typing import TYPE_CHECKING

from sqlalchemy import BigInteger, CheckConstraint, Date, DateTime, Enum, ForeignKey, Index, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship

from couchers.models.base import Base

if TYPE_CHECKING:
from couchers.models import Node, User
from couchers.models.host_requests import HostRequest


class PublicTripStatus(enum.Enum):
searching_for_host = enum.auto()
closed = enum.auto()


class PublicTrip(Base, kw_only=True):
"""
A public trip posted by a traveler looking for a host in a community.
"""

__tablename__ = "public_trips"

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

# The traveler posting the trip
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nabramow @aapeliv should we maybe use mapped_column(..., comment='The traveler posting the trip') so that it's available in the database?


# The community/location (city-level node)
node_id: Mapped[int] = mapped_column(ForeignKey("nodes.id"), index=True)

# Trip dates
from_date: Mapped[date] = mapped_column(Date)
to_date: Mapped[date] = mapped_column(Date)

# User's message about their trip
description: Mapped[str] = mapped_column(String)

# Current status
status: Mapped[PublicTripStatus] = mapped_column(
Enum(PublicTripStatus), default=PublicTripStatus.searching_for_host
)

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

# Relationships
user: Mapped[User] = relationship(init=False, back_populates="public_trips")
node: Mapped[Node] = relationship(init=False, back_populates="public_trips")
host_requests: Mapped[list[HostRequest]] = relationship(init=False, back_populates="public_trip")

__table_args__ = (
# Ensure from_date is not after to_date
CheckConstraint("from_date <= to_date", name="valid_date_range"),
# Index for querying active trips in a community
# Using partial index since we mostly query for active trips
Index(
"ix_public_trips_node_from_date_active",
node_id,
from_date,
postgresql_where=status == PublicTripStatus.searching_for_host,
),
)
3 changes: 3 additions & 0 deletions app/backend/src/couchers/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
if TYPE_CHECKING:
from couchers.models import UserBadge
from couchers.models.admin import UserAdminTag
from couchers.models.public_trips import PublicTrip
from couchers.models.rest import InviteCode, ModerationUserList
from couchers.models.uploads import PhotoGallery

Expand Down Expand Up @@ -355,6 +356,8 @@ class User(Base, kw_only=True):
back_populates="user",
)

public_trips: Mapped[list[PublicTrip]] = relationship(init=False, back_populates="user")

__table_args__ = (
# Verified phone numbers should be unique
Index(
Expand Down
Loading