Skip to content

Commit 8da0d88

Browse files
Bulk delete findings: extension hook (#14740)
* Add bulk delete findings extension hook and optional cascade context - Split internal bulk delete implementation from entry point that can delegate via get_custom_method(BULK_DELETE_FINDINGS_METHOD). - Allow callers to pass optional cascade_root alongside chunk_size for downstream use when a custom method is configured. * Fix docstring layout for bulk_delete_findings (Ruff D213) * Pass product_id into bulk_delete_findings from async delete - Extend bulk_delete_findings entry point with optional product_id for custom methods. - async_delete_task: resolve product once for grading and metering; pass product_id to bulk_delete_findings; skip perform_product_grading when deleting a Product. * refactor
1 parent 842fd2b commit 8da0d88

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

dojo/finding/helper.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def bulk_clear_finding_m2m(finding_qs):
763763
Notes.objects.filter(id__in=note_ids).delete()
764764

765765

766-
def bulk_delete_findings(finding_qs, chunk_size=1000):
766+
def _bulk_delete_findings_internal(finding_qs, chunk_size=1000):
767767
"""
768768
Delete findings and all related objects efficiently. Including any related object in Dojo-Pro
769769
@@ -798,6 +798,25 @@ def bulk_delete_findings(finding_qs, chunk_size=1000):
798798
)
799799

800800

801+
def bulk_delete_findings(finding_qs, chunk_size=1000, cascade_root=None):
802+
"""
803+
Entry point; may delegate to Pro via settings.BULK_DELETE_FINDINGS_METHOD.
804+
805+
cascade_root: optional dict describing the top-level object whose cascade triggered
806+
this bulk delete (e.g. {"model": "dojo.engagement", "pk": 9}). Ignored by OSS
807+
when no custom method is configured.
808+
"""
809+
from dojo.utils import get_custom_method # noqa: PLC0415 circular import
810+
811+
if fn := get_custom_method("BULK_DELETE_FINDINGS_METHOD"):
812+
return fn(
813+
finding_qs,
814+
chunk_size=chunk_size,
815+
cascade_root=cascade_root,
816+
)
817+
return _bulk_delete_findings_internal(finding_qs, chunk_size=chunk_size)
818+
819+
801820
def fix_loop_duplicates(scope_qs=None):
802821
"""Due to bugs in the past and even currently when under high parallel load, there can be transitive duplicates."""
803822
""" i.e. A -> B -> C. This can lead to problems when deleting findingns, performing deduplication, etc """

dojo/utils.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,7 +2016,9 @@ def async_delete_task(obj, **kwargs):
20162016
# Capture product reference before deletion for product grading at the end
20172017
product = None
20182018
with suppress(Product.DoesNotExist, Engagement.DoesNotExist, Test.DoesNotExist):
2019-
if isinstance(obj, Engagement):
2019+
if isinstance(obj, Product):
2020+
product = obj
2021+
elif isinstance(obj, Engagement):
20202022
product = obj.product
20212023
elif isinstance(obj, Test):
20222024
product = obj.engagement.product
@@ -2025,6 +2027,8 @@ def async_delete_task(obj, **kwargs):
20252027
scope_field = FINDING_SCOPE_FILTERS.get(type(obj))
20262028
if scope_field:
20272029
finding_qs = Finding.objects.filter(**{scope_field: obj})
2030+
# cascade_root is some context we provide to the bulk_delete_findings function
2031+
cascade_root = {"model": obj._meta.label_lower, "pk": obj.pk}
20282032

20292033
# Step 2: Prepare duplicate clusters (must happen before any deletion)
20302034
# When CASCADE_DELETE=True, reconfigure_duplicate_cluster skips reconfiguration —
@@ -2043,10 +2047,18 @@ def async_delete_task(obj, **kwargs):
20432047
outside_count = outside_dupes_qs.count()
20442048
if outside_count:
20452049
logger.info("ASYNC_DELETE: Deleting %d outside-scope duplicates first", outside_count)
2046-
bulk_delete_findings(outside_dupes_qs, chunk_size=chunk_size)
2050+
bulk_delete_findings(
2051+
outside_dupes_qs,
2052+
chunk_size=chunk_size,
2053+
cascade_root=cascade_root,
2054+
)
20472055

20482056
# Step 4: Delete the main scope findings
2049-
bulk_delete_findings(finding_qs, chunk_size=chunk_size)
2057+
bulk_delete_findings(
2058+
finding_qs,
2059+
chunk_size=chunk_size,
2060+
cascade_root=cascade_root,
2061+
)
20502062

20512063
# Step 5: Delete all remaining related objects (Tests, Engagements,
20522064
# Endpoints, etc.) via SQL cascade. Findings are already gone, so
@@ -2062,8 +2074,10 @@ def async_delete_task(obj, **kwargs):
20622074
# All children are already gone so this is a single-row DELETE.
20632075
obj.delete()
20642076

2065-
# Step 7: Recalculate product grade once (not per-object)
2066-
if product:
2077+
# Step 7: Recalculate product grade once (Engagement/Test deletes only). Skip when the
2078+
# deleted object is the Product itself — it is removed in step 6 and grading is pointless.
2079+
# For Product TYpe deletiongs we don't have a product instance, so this never fires.
2080+
if product and not isinstance(obj, Product):
20672081
perform_product_grading(product)
20682082

20692083
logger.info("ASYNC_DELETE: Successfully deleted %s: %s", obj_name, obj)

0 commit comments

Comments
 (0)