89 lines
3.4 KiB
Python
89 lines
3.4 KiB
Python
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"<Spot {self.id} {self.title}>"
|
|
|
|
|
|
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"<SpotImage {self.id} spot={self.spot_id}>"
|