from datetime import datetime from geoalchemy2 import Geometry from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, Numeric, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base class Spot(Base): __tablename__ = "spots" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String(200), nullable=False, index=True) city: Mapped[str] = mapped_column(String(100), nullable=False, index=True) location: Mapped[str] = mapped_column( Geometry("POINT", srid=4326), nullable=False ) description: Mapped[str | None] = mapped_column(Text) transport: Mapped[str | None] = mapped_column(Text) best_time: Mapped[str | None] = mapped_column(String(200)) difficulty: Mapped[str | None] = mapped_column(Text) is_free: Mapped[bool] = mapped_column(Boolean, default=True) price_min: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True) price_max: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True) audit_status: Mapped[str] = mapped_column(String(20), default="pending") reject_reason: Mapped[str | None] = mapped_column(String(500)) avg_rating: Mapped[float | None] = mapped_column(Float, default=None) rating_count: Mapped[int] = mapped_column(Integer, default=0) favorite_count: Mapped[int] = mapped_column(Integer, default=0) creator_id: Mapped[int] = mapped_column( Integer, ForeignKey("users.id"), nullable=False ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) creator = relationship("User", lazy="selectin") images = relationship( "SpotImage", back_populates="spot", lazy="selectin", order_by="SpotImage.sort_order" ) tags = relationship("Tag", secondary="spot_tags", lazy="selectin") @property def longitude(self) -> float | None: if self.location is None: return None # WKBElement → use ST_X via a query; for serialisation we parse the WKT from geoalchemy2.shape import to_shape point = to_shape(self.location) return point.x @property def latitude(self) -> float | None: if self.location is None: return None from geoalchemy2.shape import to_shape point = to_shape(self.location) return point.y def __repr__(self) -> str: return f"" class SpotImage(Base): __tablename__ = "spot_images" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) spot_id: Mapped[int] = mapped_column( Integer, ForeignKey("spots.id"), nullable=False ) image_url: Mapped[str] = mapped_column(String(500), nullable=False) is_cover: Mapped[bool] = mapped_column(Boolean, default=False) audit_status: Mapped[str] = mapped_column(String(20), default="pending") sort_order: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) spot = relationship("Spot", back_populates="images") def __repr__(self) -> str: return f""