fix(campaigns): start_date modal + hide future-campaign tests from queue
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: activate endpoint returns 409 with structured warning when start_date is in the future; accepts force=true to bypass. test_crud_service: always excludes tests from draft campaigns with future start_date so they do not appear in the team queue prematurely. Frontend: catches 409 on activate and shows amber confirmation modal with Keep scheduled / Activate now anyway options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -273,28 +273,33 @@ def remove_test_from_campaign(
|
||||
@router.post("/{campaign_id}/activate")
|
||||
def activate_campaign(
|
||||
campaign_id: str,
|
||||
force: bool = Query(False, description="Activate even if start_date is in the future"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_any_role("red_lead", "blue_lead")),
|
||||
):
|
||||
"""Activate a campaign, moving it from draft to active.
|
||||
|
||||
If the campaign has a start_date in the future, manual activation is blocked —
|
||||
the campaign will be auto-activated by the scheduler when the date arrives.
|
||||
If the campaign has a start_date in the future and force=False, returns a 409
|
||||
with a warning so the frontend can show a confirmation modal. If force=True,
|
||||
activates immediately regardless of start_date.
|
||||
"""
|
||||
# Guard: start_date must have been reached before manual activation
|
||||
from fastapi import HTTPException
|
||||
campaign_obj = db.query(Campaign).filter(Campaign.id == campaign_id).first()
|
||||
if campaign_obj and campaign_obj.start_date:
|
||||
if campaign_obj and campaign_obj.start_date and not force:
|
||||
now = datetime.utcnow()
|
||||
if campaign_obj.start_date > now:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
f"This campaign is scheduled to start on "
|
||||
f"{campaign_obj.start_date.strftime('%Y-%m-%d')}. "
|
||||
f"It will be activated automatically on that date. "
|
||||
f"To activate it now, remove the start date first."
|
||||
),
|
||||
status_code=409,
|
||||
detail={
|
||||
"code": "start_date_in_future",
|
||||
"start_date": campaign_obj.start_date.strftime("%Y-%m-%d"),
|
||||
"message": (
|
||||
f"This campaign is scheduled to start on "
|
||||
f"{campaign_obj.start_date.strftime('%d %b %Y')}. "
|
||||
f"It will activate automatically on that date. "
|
||||
f"Do you want to activate it now anyway?"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
with UnitOfWork(db) as uow:
|
||||
|
||||
@@ -19,7 +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.campaign import Campaign, CampaignTest
|
||||
from app.models.audit import AuditLog
|
||||
from app.utils import escape_like
|
||||
|
||||
@@ -36,7 +36,12 @@ def list_tests(
|
||||
offset: int = 0,
|
||||
limit: int = 50,
|
||||
) -> list[Test]:
|
||||
"""Return a paginated list of tests with optional filters."""
|
||||
"""Return a paginated list of tests with optional filters.
|
||||
|
||||
Tests that belong to a campaign still in 'draft' status AND with a
|
||||
start_date in the future are always excluded — they should not appear
|
||||
in the team's queue until the campaign is activated on its start date.
|
||||
"""
|
||||
query = db.query(Test).options(joinedload(Test.technique))
|
||||
|
||||
if state:
|
||||
@@ -61,6 +66,22 @@ def list_tests(
|
||||
linked = db.query(CampaignTest.test_id).distinct().subquery()
|
||||
query = query.filter(~Test.id.in_(linked))
|
||||
|
||||
# Always hide tests from scheduled campaigns that haven't started yet.
|
||||
# A "scheduled-but-not-yet-active" campaign = draft status + start_date in future.
|
||||
now = datetime.utcnow()
|
||||
future_draft_tests = (
|
||||
db.query(CampaignTest.test_id)
|
||||
.join(Campaign, Campaign.id == CampaignTest.campaign_id)
|
||||
.filter(
|
||||
Campaign.status == "draft",
|
||||
Campaign.start_date.isnot(None),
|
||||
Campaign.start_date > now,
|
||||
)
|
||||
.distinct()
|
||||
.subquery()
|
||||
)
|
||||
query = query.filter(~Test.id.in_(future_draft_tests))
|
||||
|
||||
return query.order_by(Test.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user