Added status to the requirements

This commit is contained in:
gulimabr
2025-12-02 11:47:45 -03:00
parent 459ceaa162
commit 9428c4d2de
12 changed files with 340 additions and 15 deletions

View File

@@ -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"),
)

View File

@@ -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()

View File

@@ -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):

View File

@@ -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",
]

View File

@@ -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

View 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)