Added history track

This commit is contained in:
gulimabr
2025-12-02 11:21:03 -03:00
parent 0e30db3c18
commit 459ceaa162
6 changed files with 593 additions and 13 deletions

View File

@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.models import (
TokenResponse, UserInfo, GroupResponse,
TagResponse, RequirementResponse, PriorityResponse,
RequirementCreateRequest, RequirementUpdateRequest,
RequirementCreateRequest, RequirementUpdateRequest, RequirementHistoryResponse,
ProjectResponse, ProjectCreateRequest, ProjectUpdateRequest, ProjectMemberRequest,
ValidationStatusResponse, ValidationHistoryResponse, ValidationCreateRequest,
RelationshipTypeResponse, RelationshipTypeCreateRequest, RelationshipTypeUpdateRequest,
@@ -793,6 +793,16 @@ def _build_requirement_response(req) -> RequirementResponse:
validated_at = latest_validation.created_at
validation_version = latest_validation.req_version_snapshot
# Get author (creator) display name
author_username = None
if req.user:
author_username = _get_display_name(req.user)
# Get last editor display name
last_editor_username = None
if req.last_editor:
last_editor_username = _get_display_name(req.last_editor)
return RequirementResponse(
id=req.id,
project_id=req.project_id,
@@ -808,6 +818,8 @@ def _build_requirement_response(req) -> RequirementResponse:
validated_by=validated_by,
validated_at=validated_at,
validation_version=validation_version,
author_username=author_username,
last_editor_username=last_editor_username,
)
@@ -1047,6 +1059,43 @@ async def delete_requirement(
await db.commit()
@app.get("/api/requirements/{requirement_id}/history", response_model=List[RequirementHistoryResponse])
async def get_requirement_history(
requirement_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get the version history for a requirement.
Returns all previous versions ordered by version (newest first).
Note: Group changes are not tracked in history.
Args:
requirement_id: The requirement to get history for
Returns:
List of historical versions with tag, priority, and editor info.
"""
user = await _get_current_user_db(request, db)
# Check if requirement exists
req_repo = RequirementRepository(db)
requirement = await req_repo.get_by_id(requirement_id)
if not requirement:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found"
)
# Verify user is a member of the requirement's project
await _verify_project_membership(requirement.project_id, user.id, db)
# Get history
history = await req_repo.get_history(requirement_id)
return [RequirementHistoryResponse(**h) for h in history]
# ===========================================
# Validation Endpoints
# ===========================================
@@ -1073,7 +1122,7 @@ async def create_validation(
):
"""
Create a new validation for a requirement.
Only auditors (role_id=2) and admins (role_id=1) can validate requirements.
Only auditors (role_id=2) can validate requirements.
Args:
requirement_id: The requirement to validate
@@ -1084,8 +1133,8 @@ async def create_validation(
"""
user = await _get_current_user_db(request, db)
# Only auditors (role_id=2) and admins (role_id=1) can validate
_require_role(user, [1, 2], "validate requirements")
# Only auditors (role_id=2) can validate
_require_role(user, [2], "validate requirements")
# Check if requirement exists and user has access
req_repo = RequirementRepository(db)

View File

@@ -211,6 +211,8 @@ class RequirementResponse(BaseModel):
validated_by: Optional[str] = None # Username of the validator
validated_at: Optional[datetime] = None # When the latest validation was made
validation_version: Optional[int] = None # Version at which requirement was validated
author_username: Optional[str] = None # Display name of who created the requirement
last_editor_username: Optional[str] = None # Display name of who last edited the requirement
class Config:
from_attributes = True
@@ -240,6 +242,22 @@ class RequirementUpdateRequest(BaseModel):
group_ids: Optional[List[int]] = None
class RequirementHistoryResponse(BaseModel):
"""Response schema for requirement history (previous versions)."""
history_id: int
version: Optional[int] = None
req_name: Optional[str] = None
req_desc: Optional[str] = None
tag_code: Optional[str] = None
priority_name: Optional[str] = None
edited_by_username: Optional[str] = None
valid_from: Optional[datetime] = None
valid_to: Optional[datetime] = None
class Config:
from_attributes = True
# Relationship Type schemas
class RelationshipTypeResponse(BaseModel):
"""Response schema for a relationship type."""

View File

@@ -1,11 +1,11 @@
"""
Repository layer for Requirement database operations.
"""
from typing import List, Optional
from sqlalchemy import select
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
from src.db_models import Requirement, Group, Tag, Priority, Validation, RequirementHistory
import logging
logger = logging.getLogger(__name__)
@@ -32,6 +32,8 @@ class RequirementRepository:
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user),
selectinload(Requirement.last_editor),
)
.order_by(Requirement.created_at.desc())
)
@@ -55,6 +57,8 @@ class RequirementRepository:
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user),
selectinload(Requirement.last_editor),
)
.where(Requirement.project_id == project_id)
.order_by(Requirement.created_at.desc())
@@ -79,6 +83,8 @@ class RequirementRepository:
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user),
selectinload(Requirement.last_editor),
)
.where(Requirement.user_id == user_id)
.order_by(Requirement.created_at.desc())
@@ -130,6 +136,8 @@ class RequirementRepository:
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user),
selectinload(Requirement.last_editor),
)
.join(Requirement.groups)
.where(Group.id == group_id)
@@ -165,6 +173,8 @@ class RequirementRepository:
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
selectinload(Requirement.validations).selectinload(Validation.user),
selectinload(Requirement.user),
selectinload(Requirement.last_editor),
)
.where(Requirement.tag_id == tag_id)
)
@@ -349,3 +359,53 @@ class RequirementRepository:
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def get_history(self, requirement_id: int) -> List[Dict[str, Any]]:
"""
Get the version history for a requirement.
This data is populated by a database trigger on UPDATE.
Args:
requirement_id: The requirement ID to get history for
Returns:
List of historical versions with resolved tag/priority/editor names
"""
# Use raw SQL with LEFT JOINs to resolve foreign keys to display names
query = text("""
SELECT
rh.history_id,
rh.version,
rh.req_name,
rh.req_desc,
t.tag_code,
p.priority_name,
u.full_name as edited_by_full_name,
u.username as edited_by_username,
rh.valid_from,
rh.valid_to
FROM requirements_history rh
LEFT JOIN tags t ON rh.tag_id = t.id
LEFT JOIN priorities p ON rh.priority_id = p.id
LEFT JOIN users u ON rh.edited_by = u.id
WHERE rh.original_req_id = :requirement_id
ORDER BY rh.version DESC
""")
result = await self.session.execute(query, {"requirement_id": requirement_id})
rows = result.fetchall()
return [
{
"history_id": row.history_id,
"version": row.version,
"req_name": row.req_name,
"req_desc": row.req_desc,
"tag_code": row.tag_code,
"priority_name": row.priority_name,
"edited_by_username": row.edited_by_full_name or row.edited_by_username,
"valid_from": row.valid_from,
"valid_to": row.valid_to,
}
for row in rows
]