fix(campaigns): filter existing-test picker to draft + not in any campaign
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user