fix(campaigns): filter existing-test picker to draft + not in any campaign
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Backend: add not_in_any_campaign filter to list_tests (subquery on
CampaignTest) and expose it as a query param on GET /tests.
Frontend: the 'Existing Test' tab now requests only
  state=draft & not_in_any_campaign=true
so tests already linked to any campaign or not in draft state
are never shown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 09:55:02 +02:00
parent b19ecc0d5f
commit c467459b51
4 changed files with 13 additions and 2 deletions

View File

@@ -90,6 +90,9 @@ def list_tests(
pending_validation_side: Optional[str] = Query( pending_validation_side: Optional[str] = Query(
None, description="Filter in_review tests pending validation on 'red' or 'blue' side" 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), offset: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200), limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -103,6 +106,7 @@ def list_tests(
platform=platform, platform=platform,
created_by=created_by, created_by=created_by,
pending_validation_side=pending_validation_side, pending_validation_side=pending_validation_side,
not_in_any_campaign=not_in_any_campaign,
offset=offset, offset=offset,
limit=limit, limit=limit,
) )

View File

@@ -19,6 +19,7 @@ from app.models.enums import TestState
from app.models.technique import Technique from app.models.technique import Technique
from app.models.test import Test from app.models.test import Test
from app.models.test_template import TestTemplate from app.models.test_template import TestTemplate
from app.models.campaign import CampaignTest
from app.models.audit import AuditLog from app.models.audit import AuditLog
from app.utils import escape_like from app.utils import escape_like
@@ -31,6 +32,7 @@ def list_tests(
platform: str | None = None, platform: str | None = None,
created_by: uuid.UUID | None = None, created_by: uuid.UUID | None = None,
pending_validation_side: str | None = None, pending_validation_side: str | None = None,
not_in_any_campaign: bool = False,
offset: int = 0, offset: int = 0,
limit: int = 50, limit: int = 50,
) -> list[Test]: ) -> list[Test]:
@@ -55,6 +57,9 @@ def list_tests(
Test.state == TestState.in_review, Test.state == TestState.in_review,
Test.blue_validation_status.in_(["pending", None]), 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() return query.order_by(Test.created_at.desc()).offset(offset).limit(limit).all()

View File

@@ -62,6 +62,7 @@ export interface TestListFilters {
platform?: string; platform?: string;
created_by?: string; created_by?: string;
pending_validation_side?: "red" | "blue"; pending_validation_side?: "red" | "blue";
not_in_any_campaign?: boolean;
offset?: number; offset?: number;
limit?: number; limit?: number;
} }
@@ -76,6 +77,7 @@ export async function getTests(filters?: TestListFilters): Promise<Test[]> {
if (filters?.platform) params.append("platform", filters.platform); if (filters?.platform) params.append("platform", filters.platform);
if (filters?.created_by) params.append("created_by", filters.created_by); 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?.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?.offset !== undefined) params.append("offset", String(filters.offset));
if (filters?.limit !== undefined) params.append("limit", String(filters.limit)); if (filters?.limit !== undefined) params.append("limit", String(filters.limit));

View File

@@ -106,7 +106,7 @@ export default function AddTestToCampaignModal({
const { data: allTests, isLoading: testsLoading } = useQuery({ const { data: allTests, isLoading: testsLoading } = useQuery({
queryKey: ["tests", "for-campaign-picker"], queryKey: ["tests", "for-campaign-picker"],
queryFn: () => getTests({ limit: 200 }), queryFn: () => getTests({ state: "draft", not_in_any_campaign: true, limit: 200 }),
enabled: open && tab === "existing", enabled: open && tab === "existing",
}); });
@@ -506,7 +506,7 @@ export default function AddTestToCampaignModal({
<p className="text-sm"> <p className="text-sm">
{existingSearch {existingSearch
? "No tests match your search." ? "No tests match your search."
: "No available tests to add."} : "No draft tests available. All existing tests are already in a campaign."}
</p> </p>
</div> </div>
) : ( ) : (