project.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. from datetime import datetime
  2. from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func
  3. from sqlalchemy.orm import Mapped, mapped_column, relationship
  4. from backend.app.core.database import Base
  5. class Project(Base):
  6. """Project to group related prints (e.g., 'Voron Build' with multiple parts)."""
  7. __tablename__ = "projects"
  8. id: Mapped[int] = mapped_column(primary_key=True)
  9. name: Mapped[str] = mapped_column(String(255))
  10. description: Mapped[str | None] = mapped_column(Text, nullable=True)
  11. color: Mapped[str | None] = mapped_column(String(20), nullable=True) # Hex color for UI
  12. status: Mapped[str] = mapped_column(String(20), default="active") # active, completed, archived
  13. # External link rendered as a clickable icon next to the project name (#1155).
  14. url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
  15. # Filename of the cover photo inside the project's attachments dir; serves as
  16. # the card's hero image when set (#1155). The file lives alongside other
  17. # attachments but is tracked here separately so users can manage one without
  18. # the other.
  19. cover_image_filename: Mapped[str | None] = mapped_column(String(255), nullable=True)
  20. target_count: Mapped[int | None] = mapped_column(
  21. Integer, nullable=True
  22. ) # Optional target number of prints (plates)
  23. target_parts_count: Mapped[int | None] = mapped_column(
  24. Integer, nullable=True
  25. ) # Optional target number of parts/objects
  26. # Phase 2: Rich text notes (HTML from WYSIWYG editor)
  27. notes: Mapped[str | None] = mapped_column(Text, nullable=True)
  28. # Phase 3: File attachments stored as JSON array
  29. # Format: [{"filename": "x.stl", "original_name": "part.stl", "size": 1234, "uploaded_at": "..."}]
  30. attachments: Mapped[list | None] = mapped_column(JSON, nullable=True)
  31. # Phase 4: Tags (comma-separated)
  32. tags: Mapped[str | None] = mapped_column(Text, nullable=True)
  33. # Phase 5: Due dates and priority
  34. due_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
  35. priority: Mapped[str] = mapped_column(String(20), default="normal") # low, normal, high, urgent
  36. # Phase 6: Budget tracking
  37. budget: Mapped[float | None] = mapped_column(Float, nullable=True)
  38. # Phase 8: Templates
  39. is_template: Mapped[bool] = mapped_column(Boolean, default=False)
  40. template_source_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
  41. # Phase 10: Sub-projects (hierarchical)
  42. parent_id: Mapped[int | None] = mapped_column(ForeignKey("projects.id"), nullable=True)
  43. # Timestamps
  44. created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
  45. updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
  46. # Relationships
  47. archives: Mapped[list["PrintArchive"]] = relationship(back_populates="project")
  48. queue_items: Mapped[list["PrintQueueItem"]] = relationship(back_populates="project")
  49. children: Mapped[list["Project"]] = relationship(
  50. "Project",
  51. back_populates="parent",
  52. foreign_keys="Project.parent_id",
  53. )
  54. parent: Mapped["Project | None"] = relationship(
  55. "Project",
  56. back_populates="children",
  57. remote_side="Project.id",
  58. foreign_keys="Project.parent_id",
  59. )
  60. bom_items: Mapped[list["ProjectBOMItem"]] = relationship(back_populates="project", cascade="all, delete-orphan")
  61. from backend.app.models.archive import PrintArchive # noqa: E402
  62. from backend.app.models.print_queue import PrintQueueItem # noqa: E402
  63. from backend.app.models.project_bom import ProjectBOMItem # noqa: E402