BE-PR-003: Add Essential SQLAlchemy model
- New app/models/essential.py with Essential model and EssentialType enum (feature, improvement, refactor) - Fields: id, essential_code (unique), proposal_id (FK to proposes), type, title, description, created_by_id (FK to users), created_at, updated_at - Added essentials relationship to Proposal model (cascade delete-orphan) - Added essentials table auto-migration in main.py _migrate_schema() - Registered essential module import in startup()
This commit is contained in:
23
app/main.py
23
app/main.py
@@ -259,6 +259,27 @@ def _migrate_schema():
|
|||||||
if _has_table(db, "server_states") and not _has_column(db, "server_states", "nginx_sites_json"):
|
if _has_table(db, "server_states") and not _has_column(db, "server_states", "nginx_sites_json"):
|
||||||
db.execute(text("ALTER TABLE server_states ADD COLUMN nginx_sites_json TEXT NULL"))
|
db.execute(text("ALTER TABLE server_states ADD COLUMN nginx_sites_json TEXT NULL"))
|
||||||
|
|
||||||
|
# --- essentials table (BE-PR-003) ---
|
||||||
|
if not _has_table(db, "essentials"):
|
||||||
|
db.execute(text("""
|
||||||
|
CREATE TABLE essentials (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
essential_code VARCHAR(64) NOT NULL,
|
||||||
|
proposal_id INTEGER NOT NULL,
|
||||||
|
type ENUM('feature','improvement','refactor') NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
created_by_id INTEGER NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX idx_essentials_code (essential_code),
|
||||||
|
INDEX idx_essentials_proposal_id (proposal_id),
|
||||||
|
CONSTRAINT fk_essentials_proposal_id FOREIGN KEY (proposal_id) REFERENCES proposes(id),
|
||||||
|
CONSTRAINT fk_essentials_created_by_id FOREIGN KEY (created_by_id) REFERENCES users(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
"""))
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
@@ -293,7 +314,7 @@ def _sync_default_user_roles(db):
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def startup():
|
def startup():
|
||||||
from app.core.config import Base, engine, SessionLocal
|
from app.core.config import Base, engine, SessionLocal
|
||||||
from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting, proposal, propose
|
from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting, proposal, propose, essential
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
_migrate_schema()
|
_migrate_schema()
|
||||||
|
|
||||||
|
|||||||
59
app/models/essential.py
Normal file
59
app/models/essential.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Essential model — actionable items under a Proposal.
|
||||||
|
|
||||||
|
Each Essential represents one deliverable scope item (feature, improvement,
|
||||||
|
or refactor). When a Proposal is accepted, every Essential is converted into
|
||||||
|
a corresponding ``story/*`` task under the chosen Milestone.
|
||||||
|
|
||||||
|
See: NEXT_WAVE_DEV_DIRECTION.md §8.5
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.config import Base
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class EssentialType(str, enum.Enum):
|
||||||
|
FEATURE = "feature"
|
||||||
|
IMPROVEMENT = "improvement"
|
||||||
|
REFACTOR = "refactor"
|
||||||
|
|
||||||
|
|
||||||
|
class Essential(Base):
|
||||||
|
__tablename__ = "essentials"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
essential_code = Column(
|
||||||
|
String(64),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
index=True,
|
||||||
|
comment="Unique human-readable code, e.g. PROJ:E00001",
|
||||||
|
)
|
||||||
|
|
||||||
|
proposal_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("proposes.id"), # FK targets the actual DB table name
|
||||||
|
nullable=False,
|
||||||
|
comment="Owning Proposal",
|
||||||
|
)
|
||||||
|
|
||||||
|
type = Column(
|
||||||
|
Enum(EssentialType, values_callable=lambda x: [e.value for e in x]),
|
||||||
|
nullable=False,
|
||||||
|
comment="Essential type: feature | improvement | refactor",
|
||||||
|
)
|
||||||
|
|
||||||
|
title = Column(String(255), nullable=False, comment="Short title")
|
||||||
|
description = Column(Text, nullable=True, comment="Detailed description")
|
||||||
|
|
||||||
|
created_by_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("users.id"),
|
||||||
|
nullable=True,
|
||||||
|
comment="Author of the essential",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum
|
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.core.config import Base
|
from app.core.config import Base
|
||||||
import enum
|
import enum
|
||||||
@@ -69,6 +70,14 @@ class Proposal(Base):
|
|||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# ---- relationships -----------------------------------------------------
|
||||||
|
essentials = relationship(
|
||||||
|
"Essential",
|
||||||
|
foreign_keys="Essential.proposal_id",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="select",
|
||||||
|
)
|
||||||
|
|
||||||
# ---- convenience alias ------------------------------------------------
|
# ---- convenience alias ------------------------------------------------
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def proposal_code(self) -> str | None:
|
def proposal_code(self) -> str | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user