Skip to content

Commit 747b9cc

Browse files
authored
Add options to the Project reset action (#1568)
Signed-off-by: tdruez <[email protected]>
1 parent 790e1ef commit 747b9cc

File tree

7 files changed

+167
-57
lines changed

7 files changed

+167
-57
lines changed

scanpipe/forms.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ class BaseProjectActionForm(forms.Form):
252252
)
253253

254254

255-
class ArchiveProjectForm(BaseProjectActionForm):
255+
class ProjectArchiveForm(BaseProjectActionForm):
256+
prefix = "archive"
256257
remove_input = forms.BooleanField(
257258
label="Remove inputs",
258259
initial=True,
@@ -277,7 +278,34 @@ def get_action_kwargs(self):
277278
}
278279

279280

281+
class ProjectResetForm(BaseProjectActionForm):
282+
prefix = "reset"
283+
keep_input = forms.BooleanField(
284+
label="Keep inputs",
285+
initial=True,
286+
required=False,
287+
)
288+
restore_pipelines = forms.BooleanField(
289+
label="Restore existing pipelines",
290+
initial=False,
291+
required=False,
292+
)
293+
execute_now = forms.BooleanField(
294+
label="Execute restored pipeline(s) now",
295+
initial=False,
296+
required=False,
297+
)
298+
299+
def get_action_kwargs(self):
300+
return {
301+
"keep_input": self.cleaned_data["keep_input"],
302+
"restore_pipelines": self.cleaned_data["restore_pipelines"],
303+
"execute_now": self.cleaned_data["execute_now"],
304+
}
305+
306+
280307
class ProjectOutputDownloadForm(BaseProjectActionForm):
308+
prefix = "download"
281309
output_format = forms.ChoiceField(
282310
label="Choose the output format to include in the ZIP file",
283311
choices=[
@@ -294,6 +322,7 @@ class ProjectOutputDownloadForm(BaseProjectActionForm):
294322

295323

296324
class ProjectReportForm(BaseProjectActionForm):
325+
prefix = "report"
297326
model_name = forms.ChoiceField(
298327
label="Choose the object type to include in the XLSX file",
299328
choices=[

scanpipe/models.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -637,10 +637,9 @@ def archive(self, remove_input=False, remove_codebase=False, remove_output=False
637637
shutil.rmtree(self.tmp_path, ignore_errors=True)
638638
self.setup_work_directory()
639639

640-
self.is_archived = True
641-
self.save(update_fields=["is_archived"])
640+
self.update(is_archived=True)
642641

643-
def delete_related_objects(self, keep_input=False):
642+
def delete_related_objects(self, keep_input=False, keep_labels=False):
644643
"""
645644
Delete all related object instances using the private `_raw_delete` model API.
646645
This bypass the objects collection, cascade deletions, and signals.
@@ -657,7 +656,8 @@ def delete_related_objects(self, keep_input=False):
657656
_, deleted_counter = self.discoveredpackages.all().delete()
658657

659658
# Removes all tags from this project by deleting the UUIDTaggedItem instances.
660-
self.labels.clear()
659+
if not keep_labels:
660+
self.labels.clear()
661661

662662
relationships = [
663663
self.webhookdeliveries,
@@ -690,14 +690,25 @@ def delete(self, *args, **kwargs):
690690

691691
return super().delete(*args, **kwargs)
692692

693-
def reset(self, keep_input=True):
693+
def reset(self, keep_input=True, restore_pipelines=False, execute_now=False):
694694
"""
695695
Reset the project by deleting all related database objects and all work
696696
directories except the input directory—when the `keep_input` option is True.
697697
"""
698698
self._raise_if_run_in_progress()
699699

700-
self.delete_related_objects(keep_input=keep_input)
700+
restore_pipeline_kwargs = []
701+
if restore_pipelines:
702+
restore_pipeline_kwargs = [
703+
{
704+
"pipeline_name": run.pipeline_name,
705+
"execute_now": execute_now,
706+
"selected_groups": run.selected_groups,
707+
}
708+
for run in self.runs.all()
709+
]
710+
711+
self.delete_related_objects(keep_input=keep_input, keep_labels=True)
701712

702713
work_directories = [
703714
self.codebase_path,
@@ -717,6 +728,9 @@ def reset(self, keep_input=True):
717728

718729
self.setup_work_directory()
719730

731+
for pipeline_kwargs in restore_pipeline_kwargs:
732+
self.add_pipeline(**pipeline_kwargs)
733+
720734
def clone(
721735
self,
722736
clone_name,

scanpipe/templates/scanpipe/modals/project_reset_modal.html

+29-11
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,36 @@
55
<p class="modal-card-title">Reset this project, are you sure?</p>
66
<button class="delete" aria-label="close"></button>
77
</header>
8-
<section class="modal-card-body">
9-
<div class="notification is-danger has-text-weight-semibold">
10-
This action cannot be undone.
11-
</div>
12-
<p class="mb-2">
13-
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
14-
</p>
15-
<p class="mb-5">
16-
Are you sure you want to do this?
17-
</p>
18-
</section>
198
<form action="{% url 'project_reset' project.slug %}" method="post">{% csrf_token %}
9+
<section class="modal-card-body">
10+
<div class="notification is-danger has-text-weight-semibold">
11+
This action cannot be undone.
12+
</div>
13+
<p class="mb-2">
14+
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
15+
</p>
16+
<p class="mb-5">
17+
Are you sure you want to do this?
18+
</p>
19+
<div class="field">
20+
<label class="label">
21+
{{ reset_form.keep_input }}
22+
{{ reset_form.keep_input.label }}
23+
</label>
24+
</div>
25+
<div class="field">
26+
<label class="label">
27+
{{ reset_form.restore_pipelines }}
28+
{{ reset_form.restore_pipelines.label }}
29+
</label>
30+
</div>
31+
<div class="field">
32+
<label class="label">
33+
{{ reset_form.execute_now }}
34+
{{ reset_form.execute_now.label }}
35+
</label>
36+
</div>
37+
</section>
2038
<footer class="modal-card-foot is-flex is-justify-content-space-between">
2139
<button class="button has-text-weight-semibold" type="reset">No, Cancel</button>
2240
<button class="button is-danger is-no-close" type="submit">Yes, Reset Project</button>

scanpipe/templates/scanpipe/modals/projects_reset_modal.html

+41-23
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,48 @@
66
<p class="modal-card-title">Reset selected projects, are you sure?</p>
77
<button class="delete" aria-label="close"></button>
88
</header>
9-
<section class="modal-card-body">
10-
<div class="notification is-danger has-text-weight-semibold">
11-
This action cannot be undone.
12-
</div>
13-
<p class="mb-2">
14-
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
15-
</p>
16-
<p class="mb-5">
17-
Are you sure you want to do this?
18-
</p>
19-
{% if page_obj.paginator.num_pages > 1 %}
20-
<div class="show-on-all-checked">
21-
<hr>
22-
<div class="field include-all-field">
23-
<label class="checkbox" for="{{ archive_form.select_across.id_for_label }}">
24-
<input type="checkbox" name="{{ archive_form.select_across.name }}" id="{{ archive_form.select_across.id_for_label }}">
25-
Include all {{ paginator.count|intcomma }} projects
26-
</label>
27-
<p class="help">{{ outputs_download_form.select_across.help_text }}</p>
28-
</div>
29-
</div>
30-
{% endif %}
31-
</section>
329
<form action="{% url 'project_action' %}" method="post" id="reset-projects-form">{% csrf_token %}
10+
<section class="modal-card-body">
11+
<div class="notification is-danger has-text-weight-semibold">
12+
This action cannot be undone.
13+
</div>
14+
<p class="mb-2">
15+
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
16+
</p>
17+
<p class="mb-5">
18+
Are you sure you want to do this?
19+
</p>
20+
<div class="field">
21+
<label class="label">
22+
{{ reset_form.keep_input }}
23+
{{ reset_form.keep_input.label }}
24+
</label>
25+
</div>
26+
<div class="field">
27+
<label class="label">
28+
{{ reset_form.restore_pipelines }}
29+
{{ reset_form.restore_pipelines.label }}
30+
</label>
31+
</div>
32+
<div class="field">
33+
<label class="label">
34+
{{ reset_form.execute_now }}
35+
{{ reset_form.execute_now.label }}
36+
</label>
37+
</div>
38+
{% if page_obj.paginator.num_pages > 1 %}
39+
<div class="show-on-all-checked">
40+
<hr>
41+
<div class="field include-all-field">
42+
<label class="checkbox" for="{{ reset_form.select_across.id_for_label }}">
43+
<input type="checkbox" name="{{ reset_form.select_across.name }}" id="{{ reset_form.select_across.id_for_label }}">
44+
Include all {{ paginator.count|intcomma }} projects
45+
</label>
46+
<p class="help">{{ outputs_download_form.select_across.help_text }}</p>
47+
</div>
48+
</div>
49+
{% endif %}
50+
</section>
3351
<input type="hidden" name="{{ action_form.url_query.name }}" value="{{ request.GET.urlencode }}">
3452
<input type="hidden" name="action" value="reset">
3553
<footer class="modal-card-foot is-flex is-justify-content-space-between">

scanpipe/tests/test_models.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def test_scanpipe_project_model_delete_related_objects(self):
171171
package = DiscoveredPackage.objects.create(project=self.project1)
172172
resource.discovered_packages.add(package)
173173

174-
delete_log = self.project1.delete_related_objects()
174+
delete_log = self.project1.delete_related_objects(keep_labels=True)
175175
expected = {
176176
"scanpipe.CodebaseRelation": 0,
177177
"scanpipe.CodebaseResource": 1,
@@ -185,7 +185,10 @@ def test_scanpipe_project_model_delete_related_objects(self):
185185
"scanpipe.WebhookSubscription": 0,
186186
}
187187
self.assertEqual(expected, delete_log)
188+
188189
# Make sure the labels were deleted too.
190+
self.assertEqual(2, UUIDTaggedItem.objects.count())
191+
self.project1.delete_related_objects()
189192
self.assertEqual(0, UUIDTaggedItem.objects.count())
190193

191194
def test_scanpipe_project_model_delete(self):
@@ -224,8 +227,13 @@ def test_scanpipe_project_model_reset(self):
224227
self.assertEqual(1, self.project1.codebaseresources.count())
225228
self.assertEqual(1, self.project1.inputsources.count())
226229

227-
self.project1.reset()
230+
self.project1.reset(restore_pipelines=True, execute_now=False)
231+
self.assertEqual(0, self.project1.projectmessages.count())
232+
self.assertEqual(1, self.project1.runs.count())
233+
self.assertEqual(0, self.project1.discoveredpackages.count())
234+
self.assertEqual(0, self.project1.codebaseresources.count())
228235

236+
self.project1.reset()
229237
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
230238
self.assertEqual(0, self.project1.projectmessages.count())
231239
self.assertEqual(0, self.project1.runs.count())

scanpipe/tests/test_views.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def test_scanpipe_views_project_action_report_view(self):
227227
data = {
228228
"action": "report",
229229
"selected_ids": f"{self.project1.uuid}",
230-
"model_name": "todo",
230+
"report-model_name": "todo",
231231
}
232232
response = self.client.post(url, data=data, follow=True)
233233
self.assertTrue(response.filename.startswith("scancodeio-report-"))
@@ -237,6 +237,21 @@ def test_scanpipe_views_project_action_report_view(self):
237237
workbook = openpyxl.load_workbook(output_file, read_only=True, data_only=True)
238238
self.assertEqual(["TODOS"], workbook.get_sheet_names())
239239

240+
def test_scanpipe_views_project_action_reset_view(self):
241+
url = reverse("project_action")
242+
data = {
243+
"action": "reset",
244+
"selected_ids": f"{self.project1.uuid}",
245+
"reset-restore_pipelines": "on",
246+
}
247+
self.project1.add_pipeline(pipeline_name="scan_codebase")
248+
response = self.client.post(url, data=data, follow=True)
249+
expected = "1 projects have been reset."
250+
self.assertContains(response, expected)
251+
252+
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
253+
self.assertEqual(1, self.project1.runs.count())
254+
240255
def test_scanpipe_views_project_action_view_get_project_queryset(self):
241256
queryset = ProjectActionView.get_project_queryset(
242257
selected_project_ids=[self.project1.uuid],
@@ -733,10 +748,12 @@ def test_scanpipe_views_project_reset_view(self):
733748
self.assertContains(response, expected)
734749

735750
run.set_task_ended(exitcode=0)
736-
response = self.client.post(url, follow=True)
737-
expected = "have been removed."
751+
data = {"reset-restore_pipelines": "on"}
752+
response = self.client.post(url, data=data, follow=True)
753+
expected = "has been reset."
738754
self.assertContains(response, expected)
739755
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
756+
self.assertEqual(1, self.project1.runs.count())
740757

741758
def test_scanpipe_views_project_settings_view(self):
742759
url = reverse("project_settings", args=[self.project1.slug])

0 commit comments

Comments
 (0)