models.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """
  2. SQL Alchemy models declaration.
  3. https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table
  4. Dataclass style for powerful autocompletion support.
  5. https://alembic.sqlalchemy.org/en/latest/tutorial.html
  6. Note, it is used by alembic migrations logic, see `alembic/env.py`
  7. Alembic shortcuts:
  8. # create migration
  9. alembic revision --autogenerate -m "migration_name"
  10. # apply all migrations
  11. alembic upgrade head
  12. """
  13. from typing import Optional
  14. import uuid
  15. from datetime import datetime
  16. from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table
  17. from sqlalchemy.dialects.postgresql import UUID
  18. from sqlalchemy.ext.hybrid import hybrid_property
  19. from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
  20. from sqlalchemy.sql import func
  21. class Base(DeclarativeBase):
  22. pass
  23. def uid_column() -> Mapped[str]:
  24. """Returns a postgreSQL UUID column for SQL ORM"""
  25. return mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda _: str(uuid.uuid4()))
  26. class User(Base):
  27. __tablename__ = "user_model"
  28. id: Mapped[str] = uid_column()
  29. email: Mapped[str] = mapped_column(String(254), nullable=False, unique=True, index=True)
  30. hashed_password: Mapped[str] = mapped_column(String(128), nullable=False)
  31. class Project(Base):
  32. __tablename__ = "projects"
  33. id: Mapped[str] = uid_column()
  34. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  35. updated_at: Mapped[datetime] = mapped_column(
  36. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  37. )
  38. name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
  39. is_public: Mapped[bool] = mapped_column(Boolean())
  40. volunteers: Mapped[list["Volunteer"]] = relationship(
  41. back_populates="project", cascade="delete, delete-orphan"
  42. )
  43. slots: Mapped[list["Slot"]] = relationship(
  44. back_populates="project", cascade="delete, delete-orphan"
  45. )
  46. sms: Mapped[list["Sms"]] = relationship(
  47. back_populates="project", cascade="delete, delete-orphan"
  48. )
  49. templates: Mapped[list["SlotTemplate"]] = relationship(
  50. back_populates="project", cascade="delete, delete-orphan"
  51. )
  52. tags: Mapped[list["SlotTag"]] = relationship(
  53. back_populates="project", cascade="delete, delete-orphan"
  54. )
  55. association_table_volunteer_slot = Table(
  56. "association_volunteer_slot",
  57. Base.metadata,
  58. Column(
  59. "volunteer_id",
  60. ForeignKey("volunteers.id", ondelete="CASCADE"),
  61. primary_key=True,
  62. ),
  63. Column("slot_id", ForeignKey("slots.id", ondelete="CASCADE"), primary_key=True),
  64. )
  65. class Volunteer(Base):
  66. __tablename__ = "volunteers"
  67. id: Mapped[str] = uid_column()
  68. project_id: Mapped[str] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
  69. project: Mapped["Project"] = relationship(back_populates="volunteers")
  70. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  71. updated_at: Mapped[datetime] = mapped_column(
  72. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  73. )
  74. name: Mapped[str] = mapped_column(String(128))
  75. surname: Mapped[str] = mapped_column(String(128))
  76. email: Mapped[str] = mapped_column(String(128))
  77. phone_number: Mapped[str] = mapped_column(String(128))
  78. automatic_sms: Mapped[bool] = mapped_column(Boolean(), default=False)
  79. slots: Mapped[list["Slot"]] = relationship(
  80. secondary=association_table_volunteer_slot, back_populates="volunteers"
  81. )
  82. comment: Mapped[str] = mapped_column(String(), default="")
  83. sms: Mapped[list["Sms"]] = relationship(back_populates="volunteer", cascade="all, delete")
  84. @hybrid_property
  85. def slots_id(self) -> list[str]:
  86. return [s.id for s in self.slots]
  87. class Slot(Base):
  88. __tablename__ = "slots"
  89. id: Mapped[str] = uid_column()
  90. project_id: Mapped[str] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
  91. project: Mapped["Project"] = relationship(back_populates="slots")
  92. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  93. updated_at: Mapped[datetime] = mapped_column(
  94. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  95. )
  96. title: Mapped[str] = mapped_column(String(128), nullable=False)
  97. starting_time: Mapped[datetime] = mapped_column(DateTime(timezone=True))
  98. ending_time: Mapped[datetime] = mapped_column(DateTime(timezone=True))
  99. required_volunteers: Mapped[int] = mapped_column(Integer, default=0)
  100. volunteers: Mapped[list[Volunteer]] = relationship(
  101. secondary=association_table_volunteer_slot, back_populates="slots"
  102. )
  103. template_id: Mapped[Optional[str]] = mapped_column(
  104. ForeignKey("slot_templates.id", ondelete="SET NULL"), nullable=True
  105. )
  106. template: Mapped["SlotTemplate"] = relationship(back_populates="slots")
  107. @hybrid_property
  108. def volunteers_id(self) -> list[str]:
  109. return [v.id for v in self.volunteers]
  110. association_table_template_tags = Table(
  111. "association_description_tag",
  112. Base.metadata,
  113. Column(
  114. "description_id",
  115. ForeignKey("slot_templates.id", ondelete="CASCADE"),
  116. primary_key=True,
  117. ),
  118. Column("tag_id", ForeignKey("slot_tags.id", ondelete="CASCADE"), primary_key=True),
  119. )
  120. class SlotTag(Base):
  121. __tablename__ = "slot_tags"
  122. id: Mapped[str] = uid_column()
  123. project_id: Mapped[str] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
  124. project: Mapped["Project"] = relationship(back_populates="tags")
  125. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  126. updated_at: Mapped[datetime] = mapped_column(
  127. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  128. )
  129. title: Mapped[str] = mapped_column(String(), default="")
  130. templates: Mapped[list["SlotTemplate"]] = relationship(
  131. secondary=association_table_template_tags, back_populates="tags"
  132. )
  133. @hybrid_property
  134. def templates_id(self) -> list[str]:
  135. return [s.id for s in self.templates]
  136. class SlotTemplate(Base):
  137. __tablename__ = "slot_templates"
  138. id: Mapped[str] = uid_column()
  139. project_id: Mapped[str] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
  140. project: Mapped["Project"] = relationship(back_populates="templates")
  141. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  142. updated_at: Mapped[datetime] = mapped_column(
  143. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  144. )
  145. title: Mapped[str] = mapped_column(String(), default="")
  146. description: Mapped[str] = mapped_column(String(), default="")
  147. place: Mapped[str] = mapped_column(String(), default="")
  148. responsible_contact: Mapped[str] = mapped_column(String(), default="")
  149. slots: Mapped[list[Slot]] = relationship(back_populates="template")
  150. tags: Mapped[list[SlotTag]] = relationship(
  151. secondary=association_table_template_tags, back_populates="templates"
  152. )
  153. comment: Mapped[str] = mapped_column(String(), default="")
  154. @hybrid_property
  155. def slots_id(self) -> list[str]:
  156. return [s.id for s in self.slots]
  157. @hybrid_property
  158. def tags_id(self) -> list[str]:
  159. return [s.id for s in self.tags]
  160. class Sms(Base):
  161. __tablename__ = "sms"
  162. id: Mapped[str] = mapped_column(
  163. UUID(as_uuid=False), primary_key=True, default=lambda _: str(uuid.uuid4())
  164. )
  165. project_id: Mapped[str] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
  166. project: Mapped["Project"] = relationship(back_populates="sms")
  167. volunteer_id: Mapped[str] = mapped_column(
  168. ForeignKey("volunteers.id", ondelete="CASCADE", onupdate="CASCADE"),
  169. nullable=True,
  170. )
  171. volunteer: Mapped["Volunteer"] = relationship(back_populates="sms", cascade="all, delete")
  172. created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
  173. updated_at: Mapped[datetime] = mapped_column(
  174. DateTime(timezone=True), default=datetime.now, onupdate=func.now()
  175. )
  176. content: Mapped[str] = mapped_column(String(), nullable=False)
  177. phone_number: Mapped[str] = mapped_column(String(24))
  178. sending_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.now)
  179. send_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
  180. class ServerStatus(Base):
  181. __tablename__ = "server_status"
  182. # Use a fixed ID to ensure we only ever have one row
  183. id = Column(Integer, primary_key=True, default=1)
  184. updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=func.now())
  185. host = Column(String, nullable=False)
  186. user_agent = Column(String)