Skip to content

Commit 5eafc94

Browse files
committed
Async Delete: Handle exceptions for duplicate requests
1 parent 34a24e2 commit 5eafc94

7 files changed

Lines changed: 90 additions & 73 deletions

File tree

dojo/engagement/signals.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from auditlog.models import LogEntry
23
from django.conf import settings
34
from django.contrib.contenttypes.models import ContentType
@@ -6,7 +7,7 @@
67
from django.urls import reverse
78
from django.utils.translation import gettext as _
89

9-
from dojo.models import Engagement
10+
from dojo.models import Engagement, Product
1011
from dojo.notes.helper import delete_related_notes
1112
from dojo.notifications.helper import create_notification
1213

@@ -38,25 +39,28 @@ def engagement_pre_save(sender, instance, **kwargs):
3839

3940
@receiver(post_delete, sender=Engagement)
4041
def engagement_post_delete(sender, instance, using, origin, **kwargs):
41-
if instance == origin:
42-
description = _('The engagement "%(name)s" was deleted') % {"name": instance.name}
43-
if settings.ENABLE_AUDITLOG:
44-
if le := LogEntry.objects.filter(
45-
action=LogEntry.Action.DELETE,
46-
content_type=ContentType.objects.get(app_label="dojo", model="engagement"),
47-
object_id=instance.id,
48-
).order_by("-id").first():
49-
description = _('The engagement "%(name)s" was deleted by %(user)s') % {
50-
"name": instance.name, "user": le.actor}
51-
create_notification(event="engagement_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
52-
title=_("Deletion of %(name)s") % {"name": instance.name},
53-
description=description,
54-
product=instance.product,
55-
url=reverse("view_product", args=(instance.product.id, )),
56-
recipients=[instance.lead],
57-
icon="exclamation-triangle")
42+
# Catch instances in async delete where a single object is deleted more than once
43+
with contextlib.suppress(sender.DoesNotExist, Product.DoesNotExist):
44+
if instance == origin:
45+
description = _('The engagement "%(name)s" was deleted') % {"name": instance.name}
46+
if settings.ENABLE_AUDITLOG:
47+
if le := LogEntry.objects.filter(
48+
action=LogEntry.Action.DELETE,
49+
content_type=ContentType.objects.get(app_label="dojo", model="engagement"),
50+
object_id=instance.id,
51+
).order_by("-id").first():
52+
description = _('The engagement "%(name)s" was deleted by %(user)s') % {
53+
"name": instance.name, "user": le.actor}
54+
create_notification(event="engagement_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
55+
title=_("Deletion of %(name)s") % {"name": instance.name},
56+
description=description,
57+
product=instance.product,
58+
url=reverse("view_product", args=(instance.product.id, )),
59+
recipients=[instance.lead],
60+
icon="exclamation-triangle")
5861

5962

6063
@receiver(pre_delete, sender=Engagement)
6164
def engagement_pre_delete(sender, instance, **kwargs):
62-
delete_related_notes(instance)
65+
with contextlib.suppress(sender.DoesNotExist):
66+
delete_related_notes(instance)

dojo/finding/helper.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from contextlib import suppress
23
from time import strftime
34

45
from django.conf import settings
@@ -446,8 +447,10 @@ def finding_delete(instance, **kwargs):
446447

447448
@receiver(post_delete, sender=Finding)
448449
def finding_post_delete(sender, instance, **kwargs):
449-
logger.debug("finding post_delete, sender: %s instance: %s", to_str_typed(sender), to_str_typed(instance))
450-
# calculate_grade(instance.test.engagement.product)
450+
# Catch instances in async delete where a single object is deleted more than once
451+
with suppress(Finding.DoesNotExist):
452+
logger.debug("finding post_delete, sender: %s instance: %s", to_str_typed(sender), to_str_typed(instance))
453+
# calculate_grade(instance.test.engagement.product)
451454

452455

453456
def reset_duplicate_before_delete(dupe):

dojo/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,7 +1631,7 @@ def delete(self, *args, **kwargs):
16311631
from dojo.finding import helper
16321632
helper.prepare_duplicates_for_delete(engagement=self)
16331633
super().delete(*args, **kwargs)
1634-
with suppress(Product.DoesNotExist):
1634+
with suppress(Engagement.DoesNotExist, Product.DoesNotExist):
16351635
# Suppressing a potential issue created from async delete removing
16361636
# related objects in a separate task
16371637
calculate_grade(self.product)
@@ -2239,7 +2239,7 @@ def hash_code_allows_null_cwe(self):
22392239
def delete(self, *args, **kwargs):
22402240
logger.debug("%d test delete", self.id)
22412241
super().delete(*args, **kwargs)
2242-
with suppress(Engagement.DoesNotExist, Product.DoesNotExist):
2242+
with suppress(Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
22432243
# Suppressing a potential issue created from async delete removing
22442244
# related objects in a separate task
22452245
calculate_grade(self.engagement.product)
@@ -2820,7 +2820,7 @@ def delete(self, *args, **kwargs):
28202820
from dojo.finding import helper
28212821
helper.finding_delete(self)
28222822
super().delete(*args, **kwargs)
2823-
with suppress(Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
2823+
with suppress(Finding.DoesNotExist, Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
28242824
# Suppressing a potential issue created from async delete removing
28252825
# related objects in a separate task
28262826
calculate_grade(self.test.engagement.product)

dojo/product/signals.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from auditlog.models import LogEntry
23
from django.conf import settings
34
from django.contrib.contenttypes.models import ContentType
@@ -23,17 +24,19 @@ def product_post_save(sender, instance, created, **kwargs):
2324

2425
@receiver(post_delete, sender=Product)
2526
def product_post_delete(sender, instance, **kwargs):
26-
description = _('The product "%(name)s" was deleted') % {"name": instance.name}
27-
if settings.ENABLE_AUDITLOG:
28-
if le := LogEntry.objects.filter(
29-
action=LogEntry.Action.DELETE,
30-
content_type=ContentType.objects.get(app_label="dojo", model="product"),
31-
object_id=instance.id,
32-
).order_by("-id").first():
33-
description = _('The product "%(name)s" was deleted by %(user)s') % {
34-
"name": instance.name, "user": le.actor}
35-
create_notification(event="product_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
36-
title=_("Deletion of %(name)s") % {"name": instance.name},
37-
description=description,
38-
url=reverse("product"),
39-
icon="exclamation-triangle")
27+
# Catch instances in async delete where a single object is deleted more than once
28+
with contextlib.suppress(sender.DoesNotExist):
29+
description = _('The product "%(name)s" was deleted') % {"name": instance.name}
30+
if settings.ENABLE_AUDITLOG:
31+
if le := LogEntry.objects.filter(
32+
action=LogEntry.Action.DELETE,
33+
content_type=ContentType.objects.get(app_label="dojo", model="product"),
34+
object_id=instance.id,
35+
).order_by("-id").first():
36+
description = _('The product "%(name)s" was deleted by %(user)s') % {
37+
"name": instance.name, "user": le.actor}
38+
create_notification(event="product_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
39+
title=_("Deletion of %(name)s") % {"name": instance.name},
40+
description=description,
41+
url=reverse("product"),
42+
icon="exclamation-triangle")

dojo/product_type/signals.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from auditlog.models import LogEntry
23
from django.conf import settings
34
from django.contrib.contenttypes.models import ContentType
@@ -23,18 +24,20 @@ def product_type_post_save(sender, instance, created, **kwargs):
2324

2425
@receiver(post_delete, sender=Product_Type)
2526
def product_type_post_delete(sender, instance, **kwargs):
26-
description = _('The product type "%(name)s" was deleted') % {"name": instance.name}
27-
if settings.ENABLE_AUDITLOG:
28-
if le := LogEntry.objects.filter(
29-
action=LogEntry.Action.DELETE,
30-
content_type=ContentType.objects.get(app_label="dojo", model="product_type"),
31-
object_id=instance.id,
32-
).order_by("-id").first():
33-
description = _('The product type "%(name)s" was deleted by %(user)s') % {
34-
"name": instance.name, "user": le.actor}
35-
create_notification(event="product_type_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
36-
title=_("Deletion of %(name)s") % {"name": instance.name},
37-
description=description,
38-
no_users=True,
39-
url=reverse("product_type"),
40-
icon="exclamation-triangle")
27+
# Catch instances in async delete where a single object is deleted more than once
28+
with contextlib.suppress(sender.DoesNotExist):
29+
description = _('The product type "%(name)s" was deleted') % {"name": instance.name}
30+
if settings.ENABLE_AUDITLOG:
31+
if le := LogEntry.objects.filter(
32+
action=LogEntry.Action.DELETE,
33+
content_type=ContentType.objects.get(app_label="dojo", model="product_type"),
34+
object_id=instance.id,
35+
).order_by("-id").first():
36+
description = _('The product type "%(name)s" was deleted by %(user)s') % {
37+
"name": instance.name, "user": le.actor}
38+
create_notification(event="product_type_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
39+
title=_("Deletion of %(name)s") % {"name": instance.name},
40+
description=description,
41+
no_users=True,
42+
url=reverse("product_type"),
43+
icon="exclamation-triangle")

dojo/test/signals.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,32 @@
88
from django.urls import reverse
99
from django.utils.translation import gettext as _
1010

11-
from dojo.models import Finding, Test
11+
from dojo.models import Finding, Test, Engagement, Product
1212
from dojo.notes.helper import delete_related_notes
1313
from dojo.notifications.helper import create_notification
1414

1515

1616
@receiver(post_delete, sender=Test)
1717
def test_post_delete(sender, instance, using, origin, **kwargs):
18-
if instance == origin:
19-
description = _('The test "%(name)s" was deleted') % {"name": str(instance)}
20-
if settings.ENABLE_AUDITLOG:
21-
if le := LogEntry.objects.filter(
22-
action=LogEntry.Action.DELETE,
23-
content_type=ContentType.objects.get(app_label="dojo", model="test"),
24-
object_id=instance.id,
25-
).order_by("-id").first():
26-
description = _('The test "%(name)s" was deleted by %(user)s') % {
27-
"name": str(instance), "user": le.actor}
28-
create_notification(event="test_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
29-
title=_("Deletion of %(name)s") % {"name": str(instance)},
30-
description=description,
31-
product=instance.engagement.product,
32-
url=reverse("view_engagement", args=(instance.engagement.id, )),
33-
recipients=[instance.engagement.lead],
34-
icon="exclamation-triangle")
18+
# Catch instances in async delete where a single object is deleted more than once
19+
with contextlib.suppress(sender.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
20+
if instance == origin:
21+
description = _('The test "%(name)s" was deleted') % {"name": str(instance)}
22+
if settings.ENABLE_AUDITLOG:
23+
if le := LogEntry.objects.filter(
24+
action=LogEntry.Action.DELETE,
25+
content_type=ContentType.objects.get(app_label="dojo", model="test"),
26+
object_id=instance.id,
27+
).order_by("-id").first():
28+
description = _('The test "%(name)s" was deleted by %(user)s') % {
29+
"name": str(instance), "user": le.actor}
30+
create_notification(event="test_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
31+
title=_("Deletion of %(name)s") % {"name": str(instance)},
32+
description=description,
33+
product=instance.engagement.product,
34+
url=reverse("view_engagement", args=(instance.engagement.id, )),
35+
recipients=[instance.engagement.lead],
36+
icon="exclamation-triangle")
3537

3638

3739
@receiver(pre_save, sender=Test)
@@ -53,4 +55,5 @@ def update_found_by_for_findings(sender, instance, **kwargs):
5355

5456
@receiver(pre_delete, sender=Test)
5557
def test_pre_delete(sender, instance, **kwargs):
56-
delete_related_notes(instance)
58+
with contextlib.suppress(sender.DoesNotExist):
59+
delete_related_notes(instance)

dojo/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2367,7 +2367,8 @@ def crawl(self, obj, model_list, **kwargs):
23672367
model = model_info[0]
23682368
model_query = model_info[1]
23692369
filter_dict = {model_query: obj}
2370-
objects_to_delete = model.objects.filter(**filter_dict)
2370+
# Oly fetch the IDs since we will make a list of IDs in the following function call
2371+
objects_to_delete = model.objects.only("id").filter(**filter_dict)
23712372
logger.debug("ASYNC_DELETE: Deleting " + str(len(objects_to_delete)) + " " + self.get_object_name(model) + "s in chunks")
23722373
chunks = self.chunk_list(model, objects_to_delete)
23732374
for chunk in chunks:

0 commit comments

Comments
 (0)