Added relationship between requirements

This commit is contained in:
gulimabr
2025-12-01 12:11:20 -03:00
parent f7bb62ea99
commit 74454c7b6b
11 changed files with 1306 additions and 25 deletions

View File

@@ -10,14 +10,17 @@ from src.models import (
TagResponse, RequirementResponse, PriorityResponse,
RequirementCreateRequest, RequirementUpdateRequest,
ProjectResponse, ProjectCreateRequest, ProjectUpdateRequest, ProjectMemberRequest,
ValidationStatusResponse, ValidationHistoryResponse, ValidationCreateRequest
ValidationStatusResponse, ValidationHistoryResponse, ValidationCreateRequest,
RelationshipTypeResponse, RelationshipTypeCreateRequest,
RequirementLinkResponse, RequirementLinkCreateRequest, RequirementSearchResult
)
from src.controller import AuthController
from src.config import get_openid, get_settings
from src.database import init_db, close_db, get_db
from src.repositories import (
RoleRepository, GroupRepository, TagRepository, RequirementRepository,
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository
PriorityRepository, ProjectRepository, ValidationStatusRepository, ValidationRepository,
RelationshipTypeRepository, RequirementLinkRepository
)
import logging
@@ -926,3 +929,297 @@ async def get_validation_history(
)
for v in validations
]
# ===========================================
# Relationship Types Endpoints
# ===========================================
@app.get("/api/projects/{project_id}/relationship-types", response_model=List[RelationshipTypeResponse])
async def get_project_relationship_types(
project_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all relationship types for a project.
User must be a member of the project.
Args:
project_id: The project ID
Returns:
List of relationship types for the project.
"""
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
rel_type_repo = RelationshipTypeRepository(db)
rel_types = await rel_type_repo.get_by_project_id(project_id)
return [RelationshipTypeResponse.model_validate(rt) for rt in rel_types]
@app.post("/api/projects/{project_id}/relationship-types", response_model=RelationshipTypeResponse, status_code=status.HTTP_201_CREATED)
async def create_relationship_type(
project_id: int,
request: Request,
type_data: RelationshipTypeCreateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Create a new relationship type for a project.
Only admins (role_id=1) can create relationship types.
Args:
project_id: The project ID
type_data: The relationship type data
Returns:
The created relationship type.
"""
user = await _get_current_user_db(request, db)
# Only admins can create relationship types
_require_role(user, [1], "create relationship types")
await _verify_project_membership(project_id, user.id, db)
rel_type_repo = RelationshipTypeRepository(db)
rel_type = await rel_type_repo.create(
project_id=project_id,
type_name=type_data.type_name,
type_description=type_data.type_description,
inverse_type_name=type_data.inverse_type_name
)
await db.commit()
return RelationshipTypeResponse.model_validate(rel_type)
# ===========================================
# Requirement Search Endpoint (for autocomplete)
# ===========================================
@app.get("/api/projects/{project_id}/requirements/search", response_model=List[RequirementSearchResult])
async def search_requirements(
project_id: int,
request: Request,
q: str = "",
exclude_id: Optional[int] = None,
db: AsyncSession = Depends(get_db)
):
"""
Search requirements by name or tag code for autocomplete.
User must be a member of the project.
Args:
project_id: The project ID
q: Search query (searches tag_code and req_name)
exclude_id: Optional requirement ID to exclude from results (to prevent self-linking)
Returns:
List of matching requirements (limited to 20).
"""
user = await _get_current_user_db(request, db)
await _verify_project_membership(project_id, user.id, db)
req_repo = RequirementRepository(db)
requirements = await req_repo.search_by_name_or_tag(
project_id=project_id,
query=q,
exclude_id=exclude_id,
limit=20
)
return [
RequirementSearchResult(
id=req.id,
req_name=req.req_name,
tag_code=req.tag.tag_code
)
for req in requirements
]
# ===========================================
# Requirement Links Endpoints
# ===========================================
@app.get("/api/requirements/{requirement_id}/links", response_model=List[RequirementLinkResponse])
async def get_requirement_links(
requirement_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Get all links for a requirement (both outgoing and incoming).
User must be a member of the requirement's project.
Args:
requirement_id: The requirement ID
Returns:
List of links with direction 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"
)
await _verify_project_membership(requirement.project_id, user.id, db)
link_repo = RequirementLinkRepository(db)
links = await link_repo.get_by_requirement_id(requirement_id)
return [RequirementLinkResponse(**link) for link in links]
@app.post("/api/requirements/{requirement_id}/links", response_model=RequirementLinkResponse, status_code=status.HTTP_201_CREATED)
async def create_requirement_link(
requirement_id: int,
request: Request,
link_data: RequirementLinkCreateRequest,
db: AsyncSession = Depends(get_db)
):
"""
Create a new link from this requirement to another.
User must be a member of the project.
Auditors (role_id=2) cannot create links.
Args:
requirement_id: The source requirement ID
link_data: The target requirement and relationship type
Returns:
The created link.
"""
user = await _get_current_user_db(request, db)
# Auditors cannot create links
_require_role(user, [1, 3], "create requirement links")
req_repo = RequirementRepository(db)
# Check if source requirement exists
source_req = await req_repo.get_by_id(requirement_id)
if not source_req:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found"
)
await _verify_project_membership(source_req.project_id, user.id, db)
# Check if target requirement exists
target_req = await req_repo.get_by_id(link_data.target_requirement_id)
if not target_req:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Target requirement with id {link_data.target_requirement_id} not found"
)
# Prevent self-linking
if requirement_id == link_data.target_requirement_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot link a requirement to itself"
)
# Verify both requirements are in the same project
if source_req.project_id != target_req.project_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot link requirements from different projects"
)
# Verify relationship type exists and belongs to the project
rel_type_repo = RelationshipTypeRepository(db)
rel_type = await rel_type_repo.get_by_id(link_data.relationship_type_id)
if not rel_type:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Relationship type with id {link_data.relationship_type_id} not found"
)
if rel_type.project_id != source_req.project_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Relationship type does not belong to this project"
)
# Check if link already exists
link_repo = RequirementLinkRepository(db)
if await link_repo.link_exists(requirement_id, link_data.target_requirement_id, link_data.relationship_type_id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="This link already exists"
)
# Create the link
link = await link_repo.create(
source_req_id=requirement_id,
target_req_id=link_data.target_requirement_id,
relationship_type_id=link_data.relationship_type_id,
created_by=user.id
)
await db.commit()
return RequirementLinkResponse(
id=link.id,
direction="outgoing",
type_name=rel_type.type_name,
type_id=link.relationship_type_id,
inverse_type_name=rel_type.inverse_type_name,
linked_requirement={
"id": target_req.id,
"req_name": target_req.req_name,
"tag_code": target_req.tag.tag_code
},
created_by_username=user.sub,
created_by_id=user.id,
created_at=link.created_at
)
@app.delete("/api/requirement-links/{link_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_requirement_link(
link_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
Delete a requirement link.
Only the creator or an admin can delete a link.
Args:
link_id: The link ID to delete
"""
user = await _get_current_user_db(request, db)
link_repo = RequirementLinkRepository(db)
link = await link_repo.get_by_id(link_id)
if not link:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Link with id {link_id} not found"
)
# Verify user is a member of the project
await _verify_project_membership(link.source_requirement.project_id, user.id, db)
# Only creator or admin can delete
if link.created_by != user.id and user.role_id != 1:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only the link creator or an admin can delete this link"
)
await link_repo.delete(link_id)
await db.commit()