Skip to content

Commit 18eb2ac

Browse files
authored
Merge pull request #221 from maykinmedia/feature/93-retry-deletion
[#93] Retry deletion (backend)
2 parents 6a76c80 + 681598a commit 18eb2ac

File tree

12 files changed

+337
-28
lines changed

12 files changed

+337
-28
lines changed

backend/src/openarchiefbeheer/destruction/models.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,10 @@ def process_deletion(self) -> None:
271271
)
272272
raise exc
273273

274-
delete_zaak_and_related_objects(
275-
zaak=zaak, result_store=ResultStore(store=self)
276-
)
274+
store = ResultStore(store=self)
275+
store.clear_traceback()
276+
277+
delete_zaak_and_related_objects(zaak=zaak, result_store=store)
277278

278279
zaak.delete()
279280

backend/src/openarchiefbeheer/destruction/signals.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
from django.db.models.signals import post_save
33
from django.dispatch import receiver
44

5+
from openarchiefbeheer.emails.models import EmailConfig
6+
57
from .constants import ListRole, ReviewDecisionChoices
6-
from .models import DestructionListAssignee, DestructionListReview
8+
from .models import DestructionList, DestructionListAssignee, DestructionListReview
79
from .utils import (
10+
notify,
811
notify_author_changes_requested,
912
notify_author_last_review,
1013
notify_author_positive_review,
1114
notify_reviewer,
1215
)
1316

1417
user_assigned = django.dispatch.Signal()
18+
deletion_failure = django.dispatch.Signal()
1519

1620

1721
@receiver(post_save, sender=DestructionListReview)
@@ -46,3 +50,15 @@ def notify_reviewer_of_assignment(sender, assignee, **kwargs):
4650
return
4751

4852
notify_reviewer(assignee.user, assignee.destruction_list)
53+
54+
55+
@receiver(deletion_failure)
56+
def notify_author_of_failure(sender: DestructionList, **kwargs):
57+
config = EmailConfig.get_solo()
58+
59+
notify(
60+
subject=config.subject_error_during_deletion,
61+
body=config.body_error_during_deletion,
62+
context={"list": sender},
63+
recipients=[sender.author.email],
64+
)

backend/src/openarchiefbeheer/destruction/tasks.py

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .constants import InternalStatus, ListItemStatus, ListStatus
1010
from .models import DestructionList, DestructionListItem, ReviewResponse
11+
from .signals import deletion_failure
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -81,6 +82,8 @@ def handle_processing_error(pk: int) -> None:
8182
destruction_list.processing_status = InternalStatus.failed
8283
destruction_list.save()
8384

85+
deletion_failure.send(sender=destruction_list)
86+
8487

8588
@app.task
8689
def delete_destruction_list_item(pk: int) -> None:

backend/src/openarchiefbeheer/destruction/tests/test_signals.py

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
from unittest.mock import patch
22

33
from django.core import mail
4-
from django.test import TestCase
4+
from django.test import TestCase, override_settings
55

6+
from openarchiefbeheer.destruction.tasks import delete_destruction_list
67
from openarchiefbeheer.emails.models import EmailConfig
78

8-
from ..constants import ReviewDecisionChoices
9-
from .factories import DestructionListReviewFactory
9+
from ..constants import ListStatus, ReviewDecisionChoices
10+
from .factories import (
11+
DestructionListFactory,
12+
DestructionListItemFactory,
13+
DestructionListReviewFactory,
14+
)
1015

1116

1217
class SignalsTests(TestCase):
@@ -30,3 +35,34 @@ def test_no_email_sent_if_not_review_created(self, m):
3035

3136
# No extra email sent on update event
3237
self.assertEqual(len(mail.outbox), 1)
38+
39+
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
40+
def test_failure_during_deletion_sends_signal(self):
41+
destruction_list = DestructionListFactory.create(
42+
status=ListStatus.ready_to_delete, author__email="[email protected]"
43+
)
44+
DestructionListItemFactory.create_batch(2, destruction_list=destruction_list)
45+
46+
with (
47+
patch(
48+
"openarchiefbeheer.destruction.models.delete_zaak_and_related_objects",
49+
side_effect=Exception,
50+
),
51+
patch(
52+
"openarchiefbeheer.destruction.utils.EmailConfig.get_solo",
53+
return_value=EmailConfig(
54+
subject_error_during_deletion="FAILURE!!",
55+
body_error_during_deletion="ERROR AAAh!",
56+
),
57+
),
58+
self.assertRaises(Exception),
59+
):
60+
delete_destruction_list(destruction_list)
61+
62+
self.assertEqual(len(mail.outbox), 1)
63+
64+
email = mail.outbox[0]
65+
66+
self.assertEqual(email.subject, "FAILURE!!")
67+
self.assertEqual(email.body, "ERROR AAAh!")
68+
self.assertEqual(email.to[0], "[email protected]")

backend/src/openarchiefbeheer/destruction/tests/test_tasks.py

+33
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,36 @@ def test_item_skipped_if_already_succeeded(self, logs):
302302
),
303303
logs[0],
304304
)
305+
306+
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
307+
def test_processing_list_with_failed_item(self):
308+
destruction_list = DestructionListFactory.create(
309+
status=ListStatus.ready_to_delete, processing_status=InternalStatus.failed
310+
)
311+
zaak = ZaakFactory.create()
312+
DestructionListItemFactory.create(
313+
zaak=zaak.url,
314+
destruction_list=destruction_list,
315+
processing_status=InternalStatus.failed,
316+
internal_results={"traceback": "Some traceback"},
317+
)
318+
319+
with (
320+
patch(
321+
"openarchiefbeheer.destruction.models.delete_zaak_and_related_objects",
322+
),
323+
):
324+
delete_destruction_list(destruction_list)
325+
326+
destruction_list.refresh_from_db()
327+
328+
self.assertEqual(destruction_list.processing_status, InternalStatus.succeeded)
329+
self.assertEqual(destruction_list.status, ListStatus.deleted)
330+
331+
item = destruction_list.items.first()
332+
333+
self.assertEqual(item.processing_status, InternalStatus.succeeded)
334+
self.assertEqual(
335+
item.internal_results,
336+
{"deleted_resources": {}, "resources_to_delete": {}, "traceback": ""},
337+
)

backend/src/openarchiefbeheer/emails/admin.py

+9
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,13 @@ class EmailConfigAdmin(SingletonModelAdmin):
3838
]
3939
},
4040
),
41+
(
42+
_("Templates error during deletion"),
43+
{
44+
"fields": [
45+
"subject_error_during_deletion",
46+
"body_error_during_deletion",
47+
]
48+
},
49+
),
4150
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 4.2.14 on 2024-07-26 11:33
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("emails", "0002_emailconfig_body_last_review_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="emailconfig",
15+
name="body_error_during_deletion",
16+
field=models.TextField(
17+
blank=True,
18+
help_text="Body of the email that will be sent to the record manager when an error happened during deletion.",
19+
verbose_name="body error during deletion",
20+
),
21+
),
22+
migrations.AddField(
23+
model_name="emailconfig",
24+
name="subject_error_during_deletion",
25+
field=models.CharField(
26+
blank=True,
27+
help_text="Subject of the email that will be sent to the record manager when an error happened during deletion.",
28+
max_length=250,
29+
verbose_name="subject error during deletion",
30+
),
31+
),
32+
]

backend/src/openarchiefbeheer/emails/models.py

+17
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ class EmailConfig(SingletonModel):
8484
),
8585
blank=True,
8686
)
87+
subject_error_during_deletion = models.CharField(
88+
max_length=250,
89+
verbose_name=_("subject error during deletion"),
90+
help_text=_(
91+
"Subject of the email that will be sent to the record manager "
92+
"when an error happened during deletion."
93+
),
94+
blank=True,
95+
)
96+
body_error_during_deletion = models.TextField(
97+
verbose_name=_("body error during deletion"),
98+
help_text=_(
99+
"Body of the email that will be sent to the record manager "
100+
"when an error happened during deletion."
101+
),
102+
blank=True,
103+
)
87104

88105
class Meta:
89106
verbose_name = _("email configuration")

backend/src/openarchiefbeheer/fixtures/default_emails.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
"subject_last_review": "Vernietigingslijst klaar om vernietigd te worden",
1313
"body_last_review": "Beste {{ user }}, \r\n\r\nReviewer {{ last_reviewer }} heeft de Vernietigingslijst {{ list }} goedgekeurd. Alle reviewers hebben de lijst goedgekeurd dus die is klaar om te worden vernietigd.",
1414
"subject_changes_requested": "Voorstel voor wijziging van uw vernietigingslijst",
15-
"body_changes_requested": "Beste {{ user }},\r\n\r\nEr is een voorstel tot aanpassing van uw vernietigingslijst {{ list }}. U kunt de lijst en de voorgestelde wijziging in de Open-Archiefbeheer web app bekijken en af te handelen."
15+
"body_changes_requested": "Beste {{ user }},\r\n\r\nEr is een voorstel tot aanpassing van uw vernietigingslijst {{ list }}. U kunt de lijst en de voorgestelde wijziging in de Open-Archiefbeheer web app bekijken en af te handelen.",
16+
"subject_error_during_deletion": "Fout tijdens vernietiging",
17+
"body_error_during_deletion": "Beste {{ user }},\r\n\r\nEr is een fout opgetreden tijdens het vernietigen van vernietigingslijst {{ list }}. Probeer het opnieuw of neem contact op met de IT afdeling."
1618
}
1719
}
1820
]

backend/src/openarchiefbeheer/utils/results_store.py

+21-11
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@
66
class InternalResults(TypedDict):
77
deleted_resources: dict[str, list]
88
resources_to_delete: dict[str, list]
9+
traceback: str = ""
910

1011

1112
class Store(Protocol):
1213
internal_results: InternalResults
1314

1415
def save(self) -> None: ...
16+
def refresh_from_db(self) -> None: ...
1517

1618

1719
@dataclass
1820
class ResultStore:
1921
store: Store
2022

21-
def _get_internal_results(self) -> InternalResults:
23+
def get_internal_results(self) -> InternalResults:
2224
results = self.store.internal_results
2325
if not results.get("deleted_resources"):
2426
results["deleted_resources"] = defaultdict(list)
@@ -27,37 +29,45 @@ def _get_internal_results(self) -> InternalResults:
2729
results["resources_to_delete"] = defaultdict(list)
2830
return results
2931

32+
def refresh_from_db(self) -> None:
33+
self.store.refresh_from_db()
34+
3035
def add_deleted_resource(self, resource_type: str, value: str) -> None:
31-
results = self._get_internal_results()
36+
results = self.get_internal_results()
3237
results["deleted_resources"][resource_type].append(value)
3338
self.save()
3439

3540
def save(self) -> None:
3641
self.store.save()
3742

38-
def has_deleted_resource(self, resource_type: str, value: str) -> bool:
39-
results = self._get_internal_results()
40-
41-
return value in results["deleted_resources"][resource_type]
42-
4343
def has_resource_to_delete(self, resource_type: str) -> bool:
44-
results = self._get_internal_results()
44+
results = self.get_internal_results()
4545

4646
return (
4747
resource_type in results["resources_to_delete"]
4848
and len(results["resources_to_delete"][resource_type]) > 0
4949
)
5050

5151
def add_resource_to_delete(self, resource_type: str, value: str) -> None:
52-
results = self._get_internal_results()
52+
results = self.get_internal_results()
5353

5454
results["resources_to_delete"][resource_type].append(value)
5555
self.save()
5656

5757
def get_resources_to_delete(self, resource_type: str) -> list[str]:
58-
results = self._get_internal_results()
58+
results = self.get_internal_results()
5959
return results["resources_to_delete"][resource_type]
6060

6161
def clear_resources_to_delete(self, resource_type: str) -> None:
62-
results = self._get_internal_results()
62+
results = self.get_internal_results()
6363
del results["resources_to_delete"][resource_type]
64+
65+
def add_traceback(self, formatted_traceback: str) -> None:
66+
results = self.get_internal_results()
67+
results["traceback"] = formatted_traceback
68+
self.save()
69+
70+
def clear_traceback(self):
71+
results = self.get_internal_results()
72+
results["traceback"] = ""
73+
self.save()

0 commit comments

Comments
 (0)