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")
|
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):
|
class Project(Base):
|
||||||
"""Projects - containers for requirements that can have multiple members."""
|
"""Projects - containers for requirements that can have multiple members."""
|
||||||
__tablename__ = "projects"
|
__tablename__ = "projects"
|
||||||
@@ -172,6 +185,12 @@ class Requirement(Base):
|
|||||||
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
|
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)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
tag_id: Mapped[int] = mapped_column(Integer, ForeignKey("tags.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(
|
last_editor_id: Mapped[Optional[int]] = mapped_column(
|
||||||
Integer,
|
Integer,
|
||||||
ForeignKey("users.id"),
|
ForeignKey("users.id"),
|
||||||
@@ -210,6 +229,7 @@ class Requirement(Base):
|
|||||||
foreign_keys=[last_editor_id]
|
foreign_keys=[last_editor_id]
|
||||||
)
|
)
|
||||||
tag: Mapped["Tag"] = relationship("Tag", back_populates="requirements")
|
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")
|
priority: Mapped[Optional["Priority"]] = relationship("Priority", back_populates="requirements")
|
||||||
groups: Mapped[List["Group"]] = relationship(
|
groups: Mapped[List["Group"]] = relationship(
|
||||||
"Group",
|
"Group",
|
||||||
@@ -239,6 +259,7 @@ class Requirement(Base):
|
|||||||
Index("idx_req_tag", "tag_id"),
|
Index("idx_req_tag", "tag_id"),
|
||||||
Index("idx_req_priority", "priority_id"),
|
Index("idx_req_priority", "priority_id"),
|
||||||
Index("idx_req_user", "user_id"),
|
Index("idx_req_user", "user_id"),
|
||||||
|
Index("idx_req_status", "status_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from src.models import (
|
|||||||
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
|
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
|
||||||
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult,
|
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult,
|
||||||
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
|
RoleResponse, ProjectMemberResponse, UserRoleUpdateRequest, ROLE_DISPLAY_NAMES,
|
||||||
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest
|
CommentResponse, CommentReplyResponse, CommentCreateRequest, ReplyCreateRequest,
|
||||||
|
RequirementStatusResponse
|
||||||
)
|
)
|
||||||
from src.controller import AuthController
|
from src.controller import AuthController
|
||||||
from src.config import get_openid, get_settings
|
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 (
|
from src.repositories import (
|
||||||
RoleRepository, GroupRepository, TagRepository, RequirementRepository,
|
RoleRepository, GroupRepository, TagRepository, RequirementRepository,
|
||||||
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
|
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
|
||||||
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository
|
RelationshipTypeRepository, RequirementLinkRepository, CommentRepository, ReplyRepository,
|
||||||
|
RequirementStatusRepository
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -54,6 +56,13 @@ async def lifespan(app: FastAPI):
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info("Default roles ensured")
|
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
|
# Ensure default validation statuses exist
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
await session.execute(
|
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]
|
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
|
# Projects Endpoints
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -803,6 +829,11 @@ def _build_requirement_response(req) -> RequirementResponse:
|
|||||||
if req.last_editor:
|
if req.last_editor:
|
||||||
last_editor_username = _get_display_name(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(
|
return RequirementResponse(
|
||||||
id=req.id,
|
id=req.id,
|
||||||
project_id=req.project_id,
|
project_id=req.project_id,
|
||||||
@@ -814,6 +845,7 @@ def _build_requirement_response(req) -> RequirementResponse:
|
|||||||
tag=TagResponse.model_validate(req.tag),
|
tag=TagResponse.model_validate(req.tag),
|
||||||
priority=req.priority if req.priority else None,
|
priority=req.priority if req.priority else None,
|
||||||
groups=[GroupResponse.model_validate(g) for g in req.groups],
|
groups=[GroupResponse.model_validate(g) for g in req.groups],
|
||||||
|
status=status_response,
|
||||||
validation_status=validation_status,
|
validation_status=validation_status,
|
||||||
validated_by=validated_by,
|
validated_by=validated_by,
|
||||||
validated_at=validated_at,
|
validated_at=validated_at,
|
||||||
@@ -966,6 +998,7 @@ async def create_requirement(
|
|||||||
req_desc=req_data.req_desc,
|
req_desc=req_data.req_desc,
|
||||||
priority_id=req_data.priority_id,
|
priority_id=req_data.priority_id,
|
||||||
group_ids=req_data.group_ids,
|
group_ids=req_data.group_ids,
|
||||||
|
status_id=req_data.status_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -1017,6 +1050,7 @@ async def update_requirement(
|
|||||||
tag_id=req_data.tag_id,
|
tag_id=req_data.tag_id,
|
||||||
priority_id=req_data.priority_id,
|
priority_id=req_data.priority_id,
|
||||||
group_ids=req_data.group_ids,
|
group_ids=req_data.group_ids,
|
||||||
|
status_id=req_data.status_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -151,6 +151,18 @@ class PriorityResponse(BaseModel):
|
|||||||
from_attributes = True
|
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
|
# Validation schemas
|
||||||
class ValidationStatusResponse(BaseModel):
|
class ValidationStatusResponse(BaseModel):
|
||||||
"""Response schema for a validation status."""
|
"""Response schema for a validation status."""
|
||||||
@@ -207,6 +219,9 @@ class RequirementResponse(BaseModel):
|
|||||||
tag: TagResponse
|
tag: TagResponse
|
||||||
priority: Optional[PriorityResponse] = None
|
priority: Optional[PriorityResponse] = None
|
||||||
groups: List[GroupResponse] = []
|
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
|
validation_status: Optional[str] = None # Computed from latest validation
|
||||||
validated_by: Optional[str] = None # Username of the validator
|
validated_by: Optional[str] = None # Username of the validator
|
||||||
validated_at: Optional[datetime] = None # When the latest validation was made
|
validated_at: Optional[datetime] = None # When the latest validation was made
|
||||||
@@ -231,6 +246,7 @@ class RequirementCreateRequest(BaseModel):
|
|||||||
req_desc: Optional[str] = None
|
req_desc: Optional[str] = None
|
||||||
priority_id: Optional[int] = None
|
priority_id: Optional[int] = None
|
||||||
group_ids: Optional[List[int]] = None
|
group_ids: Optional[List[int]] = None
|
||||||
|
status_id: Optional[int] = None # Defaults to Draft (1) if not provided
|
||||||
|
|
||||||
|
|
||||||
class RequirementUpdateRequest(BaseModel):
|
class RequirementUpdateRequest(BaseModel):
|
||||||
@@ -240,6 +256,7 @@ class RequirementUpdateRequest(BaseModel):
|
|||||||
tag_id: Optional[int] = None
|
tag_id: Optional[int] = None
|
||||||
priority_id: Optional[int] = None
|
priority_id: Optional[int] = None
|
||||||
group_ids: Optional[List[int]] = None
|
group_ids: Optional[List[int]] = None
|
||||||
|
status_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class RequirementHistoryResponse(BaseModel):
|
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.relationship_type_repository import RelationshipTypeRepository
|
||||||
from src.repositories.requirement_link_repository import RequirementLinkRepository
|
from src.repositories.requirement_link_repository import RequirementLinkRepository
|
||||||
from src.repositories.comment_repository import CommentRepository, ReplyRepository
|
from src.repositories.comment_repository import CommentRepository, ReplyRepository
|
||||||
|
from src.repositories.requirement_status_repository import RequirementStatusRepository
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"UserRepository",
|
"UserRepository",
|
||||||
@@ -27,4 +28,5 @@ __all__ = [
|
|||||||
"RequirementLinkRepository",
|
"RequirementLinkRepository",
|
||||||
"CommentRepository",
|
"CommentRepository",
|
||||||
"ReplyRepository",
|
"ReplyRepository",
|
||||||
|
"RequirementStatusRepository",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import List, Optional, Dict, Any
|
|||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, text
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -19,7 +19,7 @@ class RequirementRepository:
|
|||||||
|
|
||||||
async def get_all(self) -> List[Requirement]:
|
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:
|
Returns:
|
||||||
List of all requirements with eager-loaded relationships
|
List of all requirements with eager-loaded relationships
|
||||||
@@ -30,6 +30,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -55,6 +56,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -81,6 +83,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -107,6 +110,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -134,6 +138,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -171,6 +176,7 @@ class RequirementRepository:
|
|||||||
selectinload(Requirement.tag),
|
selectinload(Requirement.tag),
|
||||||
selectinload(Requirement.priority),
|
selectinload(Requirement.priority),
|
||||||
selectinload(Requirement.groups),
|
selectinload(Requirement.groups),
|
||||||
|
selectinload(Requirement.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.status),
|
selectinload(Requirement.validations).selectinload(Validation.status),
|
||||||
selectinload(Requirement.validations).selectinload(Validation.user),
|
selectinload(Requirement.validations).selectinload(Validation.user),
|
||||||
selectinload(Requirement.user),
|
selectinload(Requirement.user),
|
||||||
@@ -198,6 +204,7 @@ class RequirementRepository:
|
|||||||
req_desc: Optional[str] = None,
|
req_desc: Optional[str] = None,
|
||||||
priority_id: Optional[int] = None,
|
priority_id: Optional[int] = None,
|
||||||
group_ids: Optional[List[int]] = None,
|
group_ids: Optional[List[int]] = None,
|
||||||
|
status_id: Optional[int] = None,
|
||||||
) -> Requirement:
|
) -> Requirement:
|
||||||
"""
|
"""
|
||||||
Create a new requirement.
|
Create a new requirement.
|
||||||
@@ -210,6 +217,7 @@ class RequirementRepository:
|
|||||||
req_desc: The requirement description (optional)
|
req_desc: The requirement description (optional)
|
||||||
priority_id: The priority ID (optional)
|
priority_id: The priority ID (optional)
|
||||||
group_ids: List of group IDs to associate (optional)
|
group_ids: List of group IDs to associate (optional)
|
||||||
|
status_id: The requirement status ID (optional, defaults to 1=Draft)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The created Requirement
|
The created Requirement
|
||||||
@@ -221,6 +229,7 @@ class RequirementRepository:
|
|||||||
req_name=req_name,
|
req_name=req_name,
|
||||||
req_desc=req_desc,
|
req_desc=req_desc,
|
||||||
priority_id=priority_id,
|
priority_id=priority_id,
|
||||||
|
status_id=status_id if status_id else 1, # Default to Draft
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add groups if provided
|
# Add groups if provided
|
||||||
@@ -247,6 +256,7 @@ class RequirementRepository:
|
|||||||
tag_id: Optional[int] = None,
|
tag_id: Optional[int] = None,
|
||||||
priority_id: Optional[int] = None,
|
priority_id: Optional[int] = None,
|
||||||
group_ids: Optional[List[int]] = None,
|
group_ids: Optional[List[int]] = None,
|
||||||
|
status_id: Optional[int] = None,
|
||||||
) -> Optional[Requirement]:
|
) -> Optional[Requirement]:
|
||||||
"""
|
"""
|
||||||
Update an existing requirement.
|
Update an existing requirement.
|
||||||
@@ -259,6 +269,7 @@ class RequirementRepository:
|
|||||||
tag_id: New tag ID (optional)
|
tag_id: New tag ID (optional)
|
||||||
priority_id: New priority ID (optional)
|
priority_id: New priority ID (optional)
|
||||||
group_ids: New list of group IDs (optional)
|
group_ids: New list of group IDs (optional)
|
||||||
|
status_id: New status ID (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The updated Requirement, or None if not found
|
The updated Requirement, or None if not found
|
||||||
@@ -276,6 +287,8 @@ class RequirementRepository:
|
|||||||
requirement.tag_id = tag_id
|
requirement.tag_id = tag_id
|
||||||
if priority_id is not None:
|
if priority_id is not None:
|
||||||
requirement.priority_id = priority_id
|
requirement.priority_id = priority_id
|
||||||
|
if status_id is not None:
|
||||||
|
requirement.status_id = status_id
|
||||||
|
|
||||||
# Set the last editor
|
# Set the last editor
|
||||||
requirement.last_editor_id = editor_id
|
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)
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import { useParams, Link } from 'react-router-dom'
|
import { useParams, Link } from 'react-router-dom'
|
||||||
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService } from '@/services'
|
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService, requirementStatusService } from '@/services'
|
||||||
import type { Requirement } from '@/services/requirementService'
|
import type { Requirement } from '@/services/requirementService'
|
||||||
import type { RelationshipType, RequirementLink, RequirementSearchResult } from '@/services/relationshipService'
|
import type { RelationshipType, RequirementLink, RequirementSearchResult } from '@/services/relationshipService'
|
||||||
import type { Tag } from '@/services/tagService'
|
import type { Tag } from '@/services/tagService'
|
||||||
import type { Priority } from '@/services/priorityService'
|
import type { Priority } from '@/services/priorityService'
|
||||||
import type { Group } from '@/services/groupService'
|
import type { Group } from '@/services/groupService'
|
||||||
import type { ValidationStatus, ValidationHistory, Comment, RequirementHistory } from '@/types'
|
import type { ValidationStatus, ValidationHistory, Comment, RequirementHistory, RequirementStatus } from '@/types'
|
||||||
|
|
||||||
// Tab types
|
// Tab types
|
||||||
type TabType = 'description' | 'relationships' | 'acceptance-criteria' | 'shared-comments' | 'validate' | 'history'
|
type TabType = 'description' | 'relationships' | 'acceptance-criteria' | 'shared-comments' | 'validate' | 'history'
|
||||||
@@ -64,9 +64,11 @@ export default function RequirementDetailPage() {
|
|||||||
const [editTagId, setEditTagId] = useState<number | ''>('')
|
const [editTagId, setEditTagId] = useState<number | ''>('')
|
||||||
const [editPriorityId, setEditPriorityId] = useState<number | ''>('')
|
const [editPriorityId, setEditPriorityId] = useState<number | ''>('')
|
||||||
const [editGroupIds, setEditGroupIds] = useState<number[]>([])
|
const [editGroupIds, setEditGroupIds] = useState<number[]>([])
|
||||||
|
const [editStatusId, setEditStatusId] = useState<number | ''>(1)
|
||||||
const [availableTags, setAvailableTags] = useState<Tag[]>([])
|
const [availableTags, setAvailableTags] = useState<Tag[]>([])
|
||||||
const [availablePriorities, setAvailablePriorities] = useState<Priority[]>([])
|
const [availablePriorities, setAvailablePriorities] = useState<Priority[]>([])
|
||||||
const [availableGroups, setAvailableGroups] = useState<Group[]>([])
|
const [availableGroups, setAvailableGroups] = useState<Group[]>([])
|
||||||
|
const [availableStatuses, setAvailableStatuses] = useState<RequirementStatus[]>([])
|
||||||
const [editOptionsLoading, setEditOptionsLoading] = useState(false)
|
const [editOptionsLoading, setEditOptionsLoading] = useState(false)
|
||||||
|
|
||||||
// Requirement history state
|
// Requirement history state
|
||||||
@@ -429,21 +431,24 @@ export default function RequirementDetailPage() {
|
|||||||
setEditTagId(requirement.tag.id)
|
setEditTagId(requirement.tag.id)
|
||||||
setEditPriorityId(requirement.priority?.id || '')
|
setEditPriorityId(requirement.priority?.id || '')
|
||||||
setEditGroupIds(requirement.groups.map(g => g.id))
|
setEditGroupIds(requirement.groups.map(g => g.id))
|
||||||
|
setEditStatusId(requirement.status?.id || 1)
|
||||||
setEditError(null)
|
setEditError(null)
|
||||||
setShowEditModal(true)
|
setShowEditModal(true)
|
||||||
|
|
||||||
// Fetch options if not already loaded
|
// Fetch options if not already loaded
|
||||||
if (availableTags.length === 0 || availablePriorities.length === 0 || availableGroups.length === 0) {
|
if (availableTags.length === 0 || availablePriorities.length === 0 || availableGroups.length === 0 || availableStatuses.length === 0) {
|
||||||
try {
|
try {
|
||||||
setEditOptionsLoading(true)
|
setEditOptionsLoading(true)
|
||||||
const [tags, priorities, groups] = await Promise.all([
|
const [tags, priorities, groups, statuses] = await Promise.all([
|
||||||
tagService.getTags(),
|
tagService.getTags(),
|
||||||
priorityService.getPriorities(),
|
priorityService.getPriorities(),
|
||||||
groupService.getGroups()
|
groupService.getGroups(),
|
||||||
|
requirementStatusService.getStatuses()
|
||||||
])
|
])
|
||||||
setAvailableTags(tags)
|
setAvailableTags(tags)
|
||||||
setAvailablePriorities(priorities)
|
setAvailablePriorities(priorities)
|
||||||
setAvailableGroups(groups)
|
setAvailableGroups(groups)
|
||||||
|
setAvailableStatuses(statuses)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch edit options:', err)
|
console.error('Failed to fetch edit options:', err)
|
||||||
setEditError('Failed to load options. Please try again.')
|
setEditError('Failed to load options. Please try again.')
|
||||||
@@ -475,7 +480,8 @@ export default function RequirementDetailPage() {
|
|||||||
req_desc: editReqDesc.trim() || undefined,
|
req_desc: editReqDesc.trim() || undefined,
|
||||||
tag_id: editTagId as number,
|
tag_id: editTagId as number,
|
||||||
priority_id: editPriorityId ? editPriorityId as number : undefined,
|
priority_id: editPriorityId ? editPriorityId as number : undefined,
|
||||||
group_ids: editGroupIds
|
group_ids: editGroupIds,
|
||||||
|
status_id: editStatusId ? editStatusId as number : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Refetch to ensure consistency (trigger updates version)
|
// Refetch to ensure consistency (trigger updates version)
|
||||||
@@ -515,6 +521,9 @@ export default function RequirementDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if requirement is in draft status
|
||||||
|
const isDraftStatus = requirement?.status?.status_code === 'DRAFT'
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
@@ -561,9 +570,33 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
|
||||||
|
|
||||||
|
{/* Draft indicator banner */}
|
||||||
|
{isDraftStatus && (
|
||||||
|
<div className="mb-4 p-3 bg-amber-50 border border-amber-300 border-dashed rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-amber-600 text-lg">📝</span>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-amber-800">Draft Requirement</p>
|
||||||
|
<p className="text-sm text-amber-700">This requirement is still in draft status and is not finalized. It may be subject to changes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Version:</span> {requirement.version}
|
<span className="font-semibold">Version:</span> {requirement.version}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
|
<span className="font-semibold">Status:</span>{' '}
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
isDraftStatus
|
||||||
|
? 'bg-amber-100 text-amber-800 border border-amber-300'
|
||||||
|
: 'bg-blue-100 text-blue-800'
|
||||||
|
}`}>
|
||||||
|
{requirement.status?.status_name || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Validation Status:</span>{' '}
|
<span className="font-semibold">Validation Status:</span>{' '}
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
||||||
@@ -1220,6 +1253,14 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="max-w-5xl mx-auto px-8 py-8">
|
<div className="max-w-5xl mx-auto px-8 py-8">
|
||||||
{/* Requirement Header */}
|
{/* Requirement Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
|
{/* Draft Status Indicator */}
|
||||||
|
{isDraftStatus && (
|
||||||
|
<div className="mb-4 inline-flex items-center gap-2 px-4 py-2 bg-amber-100 border-2 border-dashed border-amber-400 rounded-lg">
|
||||||
|
<span className="text-amber-600 text-lg">📝</span>
|
||||||
|
<span className="text-amber-800 font-medium">Draft - Not Finalized</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tag */}
|
{/* Tag */}
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">{tagCode}</h2>
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">{tagCode}</h2>
|
||||||
|
|
||||||
@@ -1491,6 +1532,28 @@ export default function RequirementDetailPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={editStatusId}
|
||||||
|
onChange={(e) => setEditStatusId(e.target.value ? Number(e.target.value) : '')}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
|
disabled={editLoading}
|
||||||
|
>
|
||||||
|
{availableStatuses.map((status) => (
|
||||||
|
<option key={status.id} value={status.id}>
|
||||||
|
{status.status_name} {status.description ? `- ${status.description}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
Draft requirements are not finalized and marked with a visual indicator.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ const getValidationStatusStyle = (status: string): { bgColor: string; textColor:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if requirement is in draft status
|
||||||
|
const isDraftStatus = (statusCode: string | undefined): boolean => {
|
||||||
|
return statusCode === 'DRAFT'
|
||||||
|
}
|
||||||
|
|
||||||
export default function RequirementsPage() {
|
export default function RequirementsPage() {
|
||||||
const { user, logout, isAuditor } = useAuth()
|
const { user, logout, isAuditor } = useAuth()
|
||||||
const { currentProject, isLoading: projectLoading } = useProject()
|
const { currentProject, isLoading: projectLoading } = useProject()
|
||||||
@@ -426,17 +431,32 @@ export default function RequirementsPage() {
|
|||||||
const validationStatus = req.validation_status || 'Not Validated'
|
const validationStatus = req.validation_status || 'Not Validated'
|
||||||
const validationStyle = getValidationStatusStyle(validationStatus)
|
const validationStyle = getValidationStatusStyle(validationStatus)
|
||||||
const isStale = req.validation_version !== null && req.validation_version !== req.version
|
const isStale = req.validation_version !== null && req.validation_version !== req.version
|
||||||
|
const isDraft = isDraftStatus(req.status?.status_code)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={req.id}
|
key={req.id}
|
||||||
className="flex items-center rounded overflow-hidden border border-gray-300 bg-white"
|
className={`flex items-center rounded overflow-hidden border bg-white ${
|
||||||
|
isDraft
|
||||||
|
? 'border-amber-400 border-dashed border-2'
|
||||||
|
: 'border-gray-300'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{/* Tag and name section */}
|
{/* Tag and name section */}
|
||||||
<div className="px-4 py-4 min-w-[280px]">
|
<div className="px-4 py-4 min-w-[280px]">
|
||||||
<span className="font-bold text-gray-800">
|
<div className="flex items-center gap-2">
|
||||||
{tagLabel} - {req.req_name}
|
{isDraft && (
|
||||||
</span>
|
<span
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 border border-amber-300"
|
||||||
|
title="This requirement is still in draft and not finalized"
|
||||||
|
>
|
||||||
|
📝 Draft
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-bold text-gray-800">
|
||||||
|
{tagLabel} - {req.req_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Group chips */}
|
{/* Group chips */}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export { priorityService } from './priorityService'
|
|||||||
export type { Priority } from './priorityService'
|
export type { Priority } from './priorityService'
|
||||||
export { requirementService } from './requirementService'
|
export { requirementService } from './requirementService'
|
||||||
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
|
export type { Requirement, RequirementCreateRequest, RequirementUpdateRequest } from './requirementService'
|
||||||
|
export { requirementStatusService } from './requirementStatusService'
|
||||||
export { projectService } from './projectService'
|
export { projectService } from './projectService'
|
||||||
export type { Project, ProjectCreateRequest, ProjectUpdateRequest } from './projectService'
|
export type { Project, ProjectCreateRequest, ProjectUpdateRequest } from './projectService'
|
||||||
export { validationService } from './validationService'
|
export { validationService } from './validationService'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Group } from './groupService'
|
import { Group } from './groupService'
|
||||||
import { Tag } from './tagService'
|
import { Tag } from './tagService'
|
||||||
import { Priority } from './priorityService'
|
import { Priority } from './priorityService'
|
||||||
import type { RequirementHistory } from '@/types'
|
import type { RequirementHistory, RequirementStatus } from '@/types'
|
||||||
|
|
||||||
const API_BASE_URL = '/api'
|
const API_BASE_URL = '/api'
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ export interface Requirement {
|
|||||||
tag: Tag
|
tag: Tag
|
||||||
priority: Priority | null
|
priority: Priority | null
|
||||||
groups: Group[]
|
groups: Group[]
|
||||||
|
// Requirement lifecycle status (Draft, Regular)
|
||||||
|
status: RequirementStatus | null
|
||||||
|
// Validation status (Approved, Denied, etc.)
|
||||||
validation_status: string | null
|
validation_status: string | null
|
||||||
validated_by: string | null
|
validated_by: string | null
|
||||||
validated_at: string | null
|
validated_at: string | null
|
||||||
@@ -31,6 +34,7 @@ export interface RequirementCreateRequest {
|
|||||||
req_desc?: string
|
req_desc?: string
|
||||||
priority_id?: number
|
priority_id?: number
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
status_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequirementUpdateRequest {
|
export interface RequirementUpdateRequest {
|
||||||
@@ -39,6 +43,7 @@ export interface RequirementUpdateRequest {
|
|||||||
tag_id?: number
|
tag_id?: number
|
||||||
priority_id?: number
|
priority_id?: number
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
status_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequirementService {
|
class RequirementService {
|
||||||
|
|||||||
39
frontend/src/services/requirementStatusService.ts
Normal file
39
frontend/src/services/requirementStatusService.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { RequirementStatus } from '@/types'
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api'
|
||||||
|
|
||||||
|
class RequirementStatusService {
|
||||||
|
/**
|
||||||
|
* Get all requirement lifecycle statuses.
|
||||||
|
*/
|
||||||
|
async getStatuses(): Promise<RequirementStatus[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/requirement-statuses`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses: RequirementStatus[] = await response.json()
|
||||||
|
return statuses
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch requirement statuses:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a requirement is in draft status.
|
||||||
|
*/
|
||||||
|
isDraft(status: RequirementStatus | null): boolean {
|
||||||
|
return status?.status_code === 'DRAFT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requirementStatusService = new RequirementStatusService()
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
export * from './auth'
|
export * from './auth'
|
||||||
|
|
||||||
|
// Requirement Status types (lifecycle status)
|
||||||
|
export interface RequirementStatus {
|
||||||
|
id: number
|
||||||
|
status_code: string
|
||||||
|
status_name: string
|
||||||
|
description: string | null
|
||||||
|
}
|
||||||
|
|
||||||
// Validation types
|
// Validation types
|
||||||
export interface ValidationStatus {
|
export interface ValidationStatus {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user