Coverage for src/couchers/models/uploads.py: 100%
51 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-25 10:58 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-25 10:58 +0000
1from datetime import datetime
2from typing import TYPE_CHECKING, Any
4from sqlalchemy import BigInteger, DateTime, Float, ForeignKey, String, UniqueConstraint, func
5from sqlalchemy.ext.hybrid import hybrid_property
6from sqlalchemy.orm import Mapped, mapped_column, relationship
8from couchers import urls
9from couchers.models.base import Base
11if TYPE_CHECKING:
12 from couchers.models.users import User
15class InitiatedUpload(Base):
16 """
17 Started downloads, not necessarily complete yet.
18 """
20 __tablename__ = "initiated_uploads"
22 key: Mapped[str] = mapped_column(String, primary_key=True)
24 # timezones should always be UTC
25 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
26 expiry: Mapped[datetime] = mapped_column(DateTime(timezone=True))
28 initiator_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
30 initiator_user: Mapped["User"] = relationship("User")
32 @hybrid_property
33 def is_valid(self) -> Any:
34 return (self.created <= func.now()) & (self.expiry >= func.now())
37class Upload(Base):
38 """
39 Completed uploads.
40 """
42 __tablename__ = "uploads"
44 key: Mapped[str] = mapped_column(String, primary_key=True)
46 filename: Mapped[str] = mapped_column(String)
47 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
48 creator_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
50 # photo credit, etc
51 credit: Mapped[str | None] = mapped_column(String, nullable=True)
53 creator_user: Mapped["User"] = relationship("User", backref="uploads", foreign_keys="Upload.creator_user_id")
55 def _url(self, size: str) -> str:
56 return urls.media_url(filename=self.filename, size=size)
58 @property
59 def thumbnail_url(self) -> str:
60 return self._url("thumbnail")
62 @property
63 def full_url(self) -> str:
64 return self._url("full")
67class PhotoGallery(Base):
68 """
69 Photo galleries for users or other entities.
70 """
72 __tablename__ = "photo_galleries"
74 id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
76 # For now, galleries are owned by users, but this could be extended
77 # in the future for communities, events, etc.
78 owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
80 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
82 owner_user: Mapped["User"] = relationship("User", foreign_keys=[owner_user_id], back_populates="galleries")
83 photos: Mapped[list["PhotoGalleryItem"]] = relationship(
84 "PhotoGalleryItem",
85 back_populates="gallery",
86 order_by="PhotoGalleryItem.position",
87 )
90class PhotoGalleryItem(Base):
91 """
92 Individual photos within a gallery with ordering and captions.
93 """
95 __tablename__ = "photo_gallery_items"
97 id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
99 gallery_id: Mapped[int] = mapped_column(ForeignKey("photo_galleries.id"), index=True)
100 upload_key: Mapped[str] = mapped_column(ForeignKey("uploads.key"))
102 # Float position for ordering - allows inserting between items without shifting
103 position: Mapped[float] = mapped_column(Float)
105 caption: Mapped[str | None] = mapped_column(String)
107 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
109 gallery: Mapped["PhotoGallery"] = relationship("PhotoGallery", back_populates="photos")
110 upload: Mapped["Upload"] = relationship("Upload")
112 __table_args__ = (
113 # Ensure each upload is only in a gallery once
114 UniqueConstraint("gallery_id", "upload_key", name="uix_gallery_upload"),
115 )