feat(campaigns): delete campaign button + defer Jira to Activate
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 DELETE /campaigns/{id}?delete_tests=bool endpoint
- Backend: add delete_campaign() service — handles draft-only restriction,
optional test deletion, nullifies child campaign FKs
- Backend: remove early Jira ticket creation from POST /campaigns,
POST /campaigns/{id}/tests, and POST /campaigns/from-threat-actor
- Backend: activate endpoint now creates campaign Jira ticket if missing,
then creates test tickets (all deferred from creation to activation)
- Frontend: add deleteCampaign() API function to campaigns.ts
- Frontend: two-step confirmation dialog on CampaignDetailPage —
first confirms deletion, then asks whether to also delete associated tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -425,6 +425,62 @@ def schedule_campaign(
|
||||
return campaign
|
||||
|
||||
|
||||
def delete_campaign(
|
||||
db: Session,
|
||||
campaign_id: str,
|
||||
*,
|
||||
deleter_id: uuid.UUID,
|
||||
deleter_role: str,
|
||||
delete_tests: bool = False,
|
||||
) -> None:
|
||||
"""Delete a campaign.
|
||||
|
||||
Only draft campaigns can be deleted unless the caller is admin.
|
||||
If delete_tests=True, the associated Test objects are also deleted.
|
||||
Does not commit; caller commits.
|
||||
"""
|
||||
campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first()
|
||||
if not campaign:
|
||||
raise EntityNotFoundError("Campaign", campaign_id)
|
||||
|
||||
if campaign.status != "draft" and deleter_role != "admin":
|
||||
raise BusinessRuleViolation("Only draft campaigns can be deleted")
|
||||
|
||||
if str(campaign.created_by) != str(deleter_id) and deleter_role != "admin":
|
||||
raise PermissionViolation("Only the creator or admin can delete this campaign")
|
||||
|
||||
# Collect test IDs before removing associations
|
||||
campaign_tests = (
|
||||
db.query(CampaignTest).filter(CampaignTest.campaign_id == campaign_id).all()
|
||||
)
|
||||
test_ids = [ct.test_id for ct in campaign_tests]
|
||||
|
||||
# Remove CampaignTest join rows (clear depends_on refs first to avoid FK cycles)
|
||||
for ct in campaign_tests:
|
||||
ct.depends_on = None
|
||||
db.flush()
|
||||
for ct in campaign_tests:
|
||||
db.delete(ct)
|
||||
db.flush()
|
||||
|
||||
# Optionally delete the associated tests
|
||||
if delete_tests:
|
||||
for test_id in test_ids:
|
||||
test = db.query(Test).filter(Test.id == test_id).first()
|
||||
if test:
|
||||
db.delete(test)
|
||||
db.flush()
|
||||
|
||||
# Null-out parent_campaign_id on child campaigns to avoid FK violation
|
||||
db.query(Campaign).filter(Campaign.parent_campaign_id == campaign.id).update(
|
||||
{"parent_campaign_id": None}
|
||||
)
|
||||
db.flush()
|
||||
|
||||
db.delete(campaign)
|
||||
db.flush()
|
||||
|
||||
|
||||
def get_campaign_history(db: Session, campaign_id: str) -> dict:
|
||||
"""List all child campaigns (execution history) of a recurring campaign.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user