Added history track
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user