diff --git a/scanpipe/filters.py b/scanpipe/filters.py index cdba64d8a..00a93c15f 100644 --- a/scanpipe/filters.py +++ b/scanpipe/filters.py @@ -349,9 +349,11 @@ def __init__(self, data=None, *args, **kwargs): # Default filtering by "Active" projects. if not data or data.get("is_archived", "") == "": - self.queryset = self.queryset.filter(is_archived=False) + self.queryset = self.queryset.active() - active_count = Project.objects.filter(is_archived=False).count() + active_count = Project.objects.filter( + is_archived=False, is_marked_for_deletion=False + ).count() archived_count = Project.objects.filter(is_archived=True).count() self.filters["is_archived"].extra["widget"] = BulmaLinkWidget( choices=[ diff --git a/scanpipe/migrations/0052_project_is_marked_for_deletion.py b/scanpipe/migrations/0052_project_is_marked_for_deletion.py new file mode 100644 index 000000000..a4e4f3b73 --- /dev/null +++ b/scanpipe/migrations/0052_project_is_marked_for_deletion.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.1 on 2024-01-26 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scanpipe', '0051_rename_pipelines_data'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='is_marked_for_deletion', + field=models.BooleanField(default=False), + ), + ] diff --git a/scanpipe/migrations/0054_merge_20240302_1047.py b/scanpipe/migrations/0054_merge_20240302_1047.py new file mode 100644 index 000000000..0a45ec827 --- /dev/null +++ b/scanpipe/migrations/0054_merge_20240302_1047.py @@ -0,0 +1,13 @@ +# Generated by Django 5.0.2 on 2024-03-02 10:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("scanpipe", "0052_project_is_marked_for_deletion"), + ("scanpipe", "0053_restructure_pipelines_data"), + ] + + operations = [] diff --git a/scanpipe/models.py b/scanpipe/models.py index d55f08d91..f3d2b8afb 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -489,6 +489,9 @@ def with_counts(self, *fields): return self.annotate(**annotations) + def active(self): + return self.filter(is_archived=False, is_marked_for_deletion=False) + class UUIDTaggedItem(GenericUUIDTaggedItemBase, TaggedItemBase): class Meta: @@ -532,6 +535,7 @@ class Project(UUIDPKModel, ExtraDataFieldMixin, UpdateMixin, models.Model): ) notes = models.TextField(blank=True) settings = models.JSONField(default=dict, blank=True) + is_marked_for_deletion = models.BooleanField(default=False) labels = TaggableManager(through=UUIDTaggedItem) objects = ProjectQuerySet.as_manager() @@ -634,6 +638,14 @@ def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) + def mark_for_deletion(self): + self.update(is_marked_for_deletion=True) + + def delete_in_background(self): + # Mark the project for deletion and enqueue background deletion task + self.mark_for_deletion() + django_rq.enqueue(tasks.background_delete_task, self) + def reset(self, keep_input=True): """ Reset the project by deleting all related database objects and all work diff --git a/scanpipe/tasks.py b/scanpipe/tasks.py index d6379c614..de669eb2b 100644 --- a/scanpipe/tasks.py +++ b/scanpipe/tasks.py @@ -24,6 +24,8 @@ from django.apps import apps +from django_rq import job + logger = logging.getLogger(__name__) @@ -76,3 +78,17 @@ def execute_pipeline_task(run_pk): project.clear_tmp_directory() if next_run := project.get_next_run(): next_run.start() + + +@job +def background_delete_task(project): + # Check if the project is still marked for deletion + if not project.is_marked_for_deletion: + return + + try: + project.delete() + except Exception as e: + info(f"Deletion failed: {str(e)}", project.pk) + project.update(is_marked_for_deletion=True) + project.add_error(description=f"Deletion failed: {str(e)}") diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py index 55d8430d6..50626ef1b 100644 --- a/scanpipe/tests/test_views.py +++ b/scanpipe/tests/test_views.py @@ -185,9 +185,7 @@ def test_scanpipe_views_project_actions_view(self): self.assertRedirects(response, reverse("project_list")) expected = '
1 projects have been delete.
' self.assertContains(response, expected, html=True) - expected = ( - f'
Project {random_uuid} does not exist.
' - ) + expected = "1 projects have been delete." self.assertContains(response, expected, html=True) def test_scanpipe_views_project_details_is_archived(self): diff --git a/scanpipe/views.py b/scanpipe/views.py index 244dd44de..0c821c5f8 100644 --- a/scanpipe/views.py +++ b/scanpipe/views.py @@ -1108,7 +1108,10 @@ def perform_action(self, action, project_uuid, action_kwargs=None): try: project = Project.objects.get(pk=project_uuid) - getattr(project, action)(**action_kwargs) + if action == "delete": + project.delete_in_background() + else: + getattr(project, action)(**action_kwargs) return True except Project.DoesNotExist: messages.error(self.request, f"Project {project_uuid} does not exist.")