diff --git a/backend/app/routers/tests.py b/backend/app/routers/tests.py index dce26df..21d5b4a 100644 --- a/backend/app/routers/tests.py +++ b/backend/app/routers/tests.py @@ -90,6 +90,9 @@ def list_tests( pending_validation_side: Optional[str] = Query( None, description="Filter in_review tests pending validation on 'red' or 'blue' side" ), + not_in_any_campaign: bool = Query( + False, description="Only return tests not linked to any campaign" + ), offset: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), @@ -103,6 +106,7 @@ def list_tests( platform=platform, created_by=created_by, pending_validation_side=pending_validation_side, + not_in_any_campaign=not_in_any_campaign, offset=offset, limit=limit, ) diff --git a/backend/app/services/test_crud_service.py b/backend/app/services/test_crud_service.py index 5c41f84..6864f71 100644 --- a/backend/app/services/test_crud_service.py +++ b/backend/app/services/test_crud_service.py @@ -19,6 +19,7 @@ from app.models.enums import TestState from app.models.technique import Technique from app.models.test import Test from app.models.test_template import TestTemplate +from app.models.campaign import CampaignTest from app.models.audit import AuditLog from app.utils import escape_like @@ -31,6 +32,7 @@ def list_tests( platform: str | None = None, created_by: uuid.UUID | None = None, pending_validation_side: str | None = None, + not_in_any_campaign: bool = False, offset: int = 0, limit: int = 50, ) -> list[Test]: @@ -55,6 +57,9 @@ def list_tests( Test.state == TestState.in_review, Test.blue_validation_status.in_(["pending", None]), ) + if not_in_any_campaign: + linked = db.query(CampaignTest.test_id).distinct().subquery() + query = query.filter(~Test.id.in_(linked)) return query.order_by(Test.created_at.desc()).offset(offset).limit(limit).all() diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index 069ad6d..0780e9d 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -62,6 +62,7 @@ export interface TestListFilters { platform?: string; created_by?: string; pending_validation_side?: "red" | "blue"; + not_in_any_campaign?: boolean; offset?: number; limit?: number; } @@ -76,6 +77,7 @@ export async function getTests(filters?: TestListFilters): Promise { if (filters?.platform) params.append("platform", filters.platform); if (filters?.created_by) params.append("created_by", filters.created_by); if (filters?.pending_validation_side) params.append("pending_validation_side", filters.pending_validation_side); + if (filters?.not_in_any_campaign) params.append("not_in_any_campaign", "true"); if (filters?.offset !== undefined) params.append("offset", String(filters.offset)); if (filters?.limit !== undefined) params.append("limit", String(filters.limit)); diff --git a/frontend/src/components/AddTestToCampaignModal.tsx b/frontend/src/components/AddTestToCampaignModal.tsx index c0a7391..bc65309 100644 --- a/frontend/src/components/AddTestToCampaignModal.tsx +++ b/frontend/src/components/AddTestToCampaignModal.tsx @@ -106,7 +106,7 @@ export default function AddTestToCampaignModal({ const { data: allTests, isLoading: testsLoading } = useQuery({ queryKey: ["tests", "for-campaign-picker"], - queryFn: () => getTests({ limit: 200 }), + queryFn: () => getTests({ state: "draft", not_in_any_campaign: true, limit: 200 }), enabled: open && tab === "existing", }); @@ -506,7 +506,7 @@ export default function AddTestToCampaignModal({

{existingSearch ? "No tests match your search." - : "No available tests to add."} + : "No draft tests available. All existing tests are already in a campaign."}

) : (