Added status to the requirements
This commit is contained in:
@@ -104,6 +104,19 @@ class Priority(Base):
|
||||
requirements: Mapped[List["Requirement"]] = relationship("Requirement", back_populates="priority")
|
||||
|
||||
|
||||
class RequirementStatus(Base):
|
||||
"""Requirement lifecycle statuses (Draft, Regular, etc.)."""
|
||||
__tablename__ = "requirement_statuses"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
status_code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True)
|
||||
status_name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
requirements: Mapped[List["Requirement"]] = relationship("Requirement", back_populates="status")
|
||||
|
||||
|
||||
class Project(Base):
|
||||
"""Projects - containers for requirements that can have multiple members."""
|
||||
__tablename__ = "projects"
|
||||
@@ -172,6 +185,12 @@ class Requirement(Base):
|
||||
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("tags.id"), nullable=False)
|
||||
status_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("requirement_statuses.id"),
|
||||
nullable=False,
|
||||
default=1 # Default to 'Draft' status
|
||||
)
|
||||
last_editor_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("users.id"),
|
||||
@@ -210,6 +229,7 @@ class Requirement(Base):
|
||||
foreign_keys=[last_editor_id]
|
||||
)
|
||||
tag: Mapped["Tag"] = relationship("Tag", back_populates="requirements")
|
||||
status: Mapped["RequirementStatus"] = relationship("RequirementStatus", back_populates="requirements")
|
||||
priority: Mapped[Optional["Priority"]] = relationship("Priority", back_populates="requirements")
|
||||
groups: Mapped[List["Group"]] = relationship(
|
||||
"Group",
|
||||
@@ -239,6 +259,7 @@ class Requirement(Base):
|
||||
Index("idx_req_tag", "tag_id"),
|
||||
Index("idx_req_priority", "priority_id"),
|
||||
Index("idx_req_user", "user_id"),
|
||||
Index("idx_req_status", "status_id"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ from src.models import (
|
||||
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
|
||||
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult,
|
||||
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
|
||||
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest
|
||||
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest,
|
||||
RequirementStatusResponse
|
||||
)
|
||||
from src.controller import AuthController
|
||||
from src.config import get_openid, get_settings
|
||||
@@ -22,7 +23,8 @@ from src.database import init_db, close_db, get_db
|
||||
from src.repositories import (
|
||||
RoleRepository, GroupRepository, TagRepository, RequirementRepository,
|
||||
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
|
||||
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository
|
||||
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository,
|
||||
RequirementStatusRepository
|
||||
)
|
||||
import logging
|
||||
|
||||
@@ -54,6 +56,13 @@ async def lifespan(app: FastAPI):
|
||||
await session.commit()
|
||||
logger.info("Default roles ensured")
|
||||
|
||||
# Ensure default requirement statuses exist
|
||||
async with AsyncSessionLocal() as session:
|
||||
req_status_repo = RequirementStatusRepository(session)
|
||||
await req_status_repo.ensure_default_statuses_exist()
|
||||
await session.commit()
|
||||
logger.info("Default requirement statuses ensured")
|
||||
|
||||
# Ensure default validation statuses exist
|
||||
async with AsyncSessionLocal() as session:
|
||||
await session.execute(
|
||||
@@ -277,6 +286,23 @@ async def get_priorities(db: AsyncSession = Depends(get_db)):
|
||||
return [PriorityResponse.model_validate(p) for p in priorities]
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Requirement Statuses Endpoints
|
||||
# ===========================================
|
||||
|
||||
@app.get("/api/requirement-statuses", response_model=List[RequirementStatusResponse])
|
||||
async def get_requirement_statuses(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Get all requirement lifecycle statuses (Draft, Regular, etc.).
|
||||
|
||||
Returns:
|
||||
List of all requirement statuses.
|
||||
"""
|
||||
status_repo = RequirementStatusRepository(db)
|
||||
statuses = await status_repo.get_all()
|
||||
return [RequirementStatusResponse.model_validate(s) for s in statuses]
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Projects Endpoints
|
||||
# ===========================================
|
||||
@@ -803,6 +829,11 @@ def _build_requirement_response(req) -> RequirementResponse:
|
||||
if req.last_editor:
|
||||
last_editor_username = _get_display_name(req.last_editor)
|
||||
|
||||
# Get requirement lifecycle status
|
||||
status_response = None
|
||||
if req.status:
|
||||
status_response = RequirementStatusResponse.model_validate(req.status)
|
||||
|
||||
return RequirementResponse(
|
||||
id=req.id,
|
||||
project_id=req.project_id,
|
||||
@@ -814,6 +845,7 @@ def _build_requirement_response(req) -> RequirementResponse:
|
||||
tag=TagResponse.model_validate(req.tag),
|
||||
priority=req.priority if req.priority else None,
|
||||
groups=[GroupResponse.model_validate(g) for g in req.groups],
|
||||
status=status_response,
|
||||
validation_status=validation_status,
|
||||
validated_by=validated_by,
|
||||
validated_at=validated_at,
|
||||
@@ -966,6 +998,7 @@ async def create_requirement(
|
||||
req_desc=req_data.req_desc,
|
||||
priority_id=req_data.priority_id,
|
||||
group_ids=req_data.group_ids,
|
||||
status_id=req_data.status_id,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
@@ -1017,6 +1050,7 @@ async def update_requirement(
|
||||
tag_id=req_data.tag_id,
|
||||
priority_id=req_data.priority_id,
|
||||
group_ids=req_data.group_ids,
|
||||
status_id=req_data.status_id,
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -151,6 +151,18 @@ class PriorityResponse(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Requirement Status schemas
|
||||
class RequirementStatusResponse(BaseModel):
|
||||
"""Response schema for a requirement lifecycle status."""
|
||||
id: int
|
||||
status_code: str
|
||||
status_name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Validation schemas
|
||||
class ValidationStatusResponse(BaseModel):
|
||||
"""Response schema for a validation status."""
|
||||
@@ -207,6 +219,9 @@ class RequirementResponse(BaseModel):
|
||||
tag: TagResponse
|
||||
priority: Optional[PriorityResponse] = None
|
||||
groups: List[GroupResponse] = []
|
||||
# Requirement lifecycle status (Draft, Regular)
|
||||
status: Optional[RequirementStatusResponse] = None
|
||||
# Validation status (Approved, Denied, etc.)
|
||||
validation_status: Optional[str] = None # Computed from latest validation
|
||||
validated_by: Optional[str] = None # Username of the validator
|
||||
validated_at: Optional[datetime] = None # When the latest validation was made
|
||||
@@ -231,6 +246,7 @@ class RequirementCreateRequest(BaseModel):
|
||||
req_desc: Optional[str] = None
|
||||
priority_id: Optional[int] = None
|
||||
group_ids: Optional[List[int]] = None
|
||||
status_id: Optional[int] = None # Defaults to Draft (1) if not provided
|
||||
|
||||
|
||||
class RequirementUpdateRequest(BaseModel):
|
||||
@@ -240,6 +256,7 @@ class RequirementUpdateRequest(BaseModel):
|
||||
tag_id: Optional[int] = None
|
||||
priority_id: Optional[int] = None
|
||||
group_ids: Optional[List[int]] = None
|
||||
status_id: Optional[int] = None
|
||||
|
||||
|
||||
class RequirementHistoryResponse(BaseModel):
|
||||
|
||||
@@ -12,6 +12,7 @@ from src.repositories.validation_repository import ValidationRepository
|
||||
from src.repositories.relationship_type_repository import RelationshipTypeRepository
|
||||
from src.repositories.requirement_link_repository import RequirementLinkRepository
|
||||
from src.repositories.comment_repository import CommentRepository, ReplyRepository
|
||||
from src.repositories.requirement_status_repository import RequirementStatusRepository
|
||||
|
||||
__all__ = [
|
||||
"UserRepository",
|
||||
@@ -27,4 +28,5 @@ __all__ = [
|
||||
"RequirementLinkRepository",
|
||||
"CommentRepository",
|
||||
"ReplyRepository",
|
||||
"RequirementStatusRepository",
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.db_models import Requirement, Group, Tag, Priority, Validation, RequirementHistory
|
||||
from src.db_models import Requirement, Group, Tag, Priority, Validation, RequirementHistory, RequirementStatus
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -19,7 +19,7 @@ class RequirementRepository:
|
||||
|
||||
async def get_all(self) -> List[Requirement]:
|
||||
"""
|
||||
Get all requirements with their related data (tag, priority, groups, validations).
|
||||
Get all requirements with their related data (tag, priority, groups, validations, status).
|
||||
|
||||
Returns:
|
||||
List of all requirements with eager-loaded relationships
|
||||
@@ -30,6 +30,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -55,6 +56,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -81,6 +83,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -107,6 +110,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -134,6 +138,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -171,6 +176,7 @@ class RequirementRepository:
|
||||
selectinload(Requirement.tag),
|
||||
selectinload(Requirement.priority),
|
||||
selectinload(Requirement.groups),
|
||||
selectinload(Requirement.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||
selectinload(Requirement.user),
|
||||
@@ -198,6 +204,7 @@ class RequirementRepository:
|
||||
req_desc: Optional[str] = None,
|
||||
priority_id: Optional[int] = None,
|
||||
group_ids: Optional[List[int]] = None,
|
||||
status_id: Optional[int] = None,
|
||||
) -> Requirement:
|
||||
"""
|
||||
Create a new requirement.
|
||||
@@ -210,6 +217,7 @@ class RequirementRepository:
|
||||
req_desc: The requirement description (optional)
|
||||
priority_id: The priority ID (optional)
|
||||
group_ids: List of group IDs to associate (optional)
|
||||
status_id: The requirement status ID (optional, defaults to 1=Draft)
|
||||
|
||||
Returns:
|
||||
The created Requirement
|
||||
@@ -221,6 +229,7 @@ class RequirementRepository:
|
||||
req_name=req_name,
|
||||
req_desc=req_desc,
|
||||
priority_id=priority_id,
|
||||
status_id=status_id if status_id else 1, # Default to Draft
|
||||
)
|
||||
|
||||
# Add groups if provided
|
||||
@@ -247,6 +256,7 @@ class RequirementRepository:
|
||||
tag_id: Optional[int] = None,
|
||||
priority_id: Optional[int] = None,
|
||||
group_ids: Optional[List[int]] = None,
|
||||
status_id: Optional[int] = None,
|
||||
) -> Optional[Requirement]:
|
||||
"""
|
||||
Update an existing requirement.
|
||||
@@ -259,6 +269,7 @@ class RequirementRepository:
|
||||
tag_id: New tag ID (optional)
|
||||
priority_id: New priority ID (optional)
|
||||
group_ids: New list of group IDs (optional)
|
||||
status_id: New status ID (optional)
|
||||
|
||||
Returns:
|
||||
The updated Requirement, or None if not found
|
||||
@@ -276,6 +287,8 @@ class RequirementRepository:
|
||||
requirement.tag_id = tag_id
|
||||
if priority_id is not None:
|
||||
requirement.priority_id = priority_id
|
||||
if status_id is not None:
|
||||
requirement.status_id = status_id
|
||||
|
||||
# Set the last editor
|
||||
requirement.last_editor_id = editor_id
|
||||
|
||||
102
backend/src/repositories/requirement_status_repository.py
Normal file
102
backend/src/repositories/requirement_status_repository.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Repository layer for RequirementStatus database operations.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.db_models import RequirementStatus
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequirementStatusRepository:
|
||||
"""Repository for RequirementStatus-related database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get_all(self) -> List[RequirementStatus]:
|
||||
"""
|
||||
Get all requirement statuses.
|
||||
|
||||
Returns:
|
||||
List of all requirement statuses
|
||||
"""
|
||||
result = await self.session.execute(
|
||||
select(RequirementStatus).order_by(RequirementStatus.id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_by_id(self, status_id: int) -> Optional[RequirementStatus]:
|
||||
"""
|
||||
Get a requirement status by ID.
|
||||
|
||||
Args:
|
||||
status_id: The status ID
|
||||
|
||||
Returns:
|
||||
RequirementStatus if found, None otherwise
|
||||
"""
|
||||
result = await self.session.execute(
|
||||
select(RequirementStatus).where(RequirementStatus.id == status_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_code(self, status_code: str) -> Optional[RequirementStatus]:
|
||||
"""
|
||||
Get a requirement status by code.
|
||||
|
||||
Args:
|
||||
status_code: The status code (e.g., 'DRAFT', 'REGULAR')
|
||||
|
||||
Returns:
|
||||
RequirementStatus if found, None otherwise
|
||||
"""
|
||||
result = await self.session.execute(
|
||||
select(RequirementStatus).where(RequirementStatus.status_code == status_code)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(
|
||||
self,
|
||||
status_code: str,
|
||||
status_name: str,
|
||||
description: Optional[str] = None
|
||||
) -> RequirementStatus:
|
||||
"""
|
||||
Create a new requirement status.
|
||||
|
||||
Args:
|
||||
status_code: The unique status code
|
||||
status_name: The display name
|
||||
description: Optional description
|
||||
|
||||
Returns:
|
||||
The created RequirementStatus
|
||||
"""
|
||||
status = RequirementStatus(
|
||||
status_code=status_code,
|
||||
status_name=status_name,
|
||||
description=description
|
||||
)
|
||||
self.session.add(status)
|
||||
await self.session.flush()
|
||||
await self.session.refresh(status)
|
||||
return status
|
||||
|
||||
async def ensure_default_statuses_exist(self) -> None:
|
||||
"""
|
||||
Ensure default requirement statuses exist in the database.
|
||||
Called during application startup.
|
||||
"""
|
||||
default_statuses = [
|
||||
("DRAFT", "Draft", "Initial version, not ready for review"),
|
||||
("REGULAR", "Regular", "Active requirement"),
|
||||
]
|
||||
|
||||
for status_code, status_name, description in default_statuses:
|
||||
existing = await self.get_by_code(status_code)
|
||||
if existing is None:
|
||||
logger.info(f"Creating default requirement status: {status_code}")
|
||||
await self.create(status_code, status_name, description)
|
||||
Reference in New Issue
Block a user