Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add options to the Project reset action #1568

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion scanpipe/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ class BaseProjectActionForm(forms.Form):
)


class ArchiveProjectForm(BaseProjectActionForm):
class ProjectArchiveForm(BaseProjectActionForm):
prefix = "archive"
remove_input = forms.BooleanField(
label="Remove inputs",
initial=True,
Expand All @@ -277,7 +278,34 @@ def get_action_kwargs(self):
}


class ProjectResetForm(BaseProjectActionForm):
prefix = "reset"
keep_input = forms.BooleanField(
label="Keep inputs",
initial=True,
required=False,
)
restore_pipelines = forms.BooleanField(
label="Restore existing pipelines",
initial=False,
required=False,
)
execute_now = forms.BooleanField(
label="Execute restored pipeline(s) now",
initial=False,
required=False,
)

def get_action_kwargs(self):
return {
"keep_input": self.cleaned_data["keep_input"],
"restore_pipelines": self.cleaned_data["restore_pipelines"],
"execute_now": self.cleaned_data["execute_now"],
}


class ProjectOutputDownloadForm(BaseProjectActionForm):
prefix = "download"
output_format = forms.ChoiceField(
label="Choose the output format to include in the ZIP file",
choices=[
Expand All @@ -294,6 +322,7 @@ class ProjectOutputDownloadForm(BaseProjectActionForm):


class ProjectReportForm(BaseProjectActionForm):
prefix = "report"
model_name = forms.ChoiceField(
label="Choose the object type to include in the XLSX file",
choices=[
Expand Down
26 changes: 20 additions & 6 deletions scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,10 +637,9 @@ def archive(self, remove_input=False, remove_codebase=False, remove_output=False
shutil.rmtree(self.tmp_path, ignore_errors=True)
self.setup_work_directory()

self.is_archived = True
self.save(update_fields=["is_archived"])
self.update(is_archived=True)

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

# Removes all tags from this project by deleting the UUIDTaggedItem instances.
self.labels.clear()
if not keep_labels:
self.labels.clear()

relationships = [
self.webhookdeliveries,
Expand Down Expand Up @@ -690,14 +690,25 @@ def delete(self, *args, **kwargs):

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

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

self.delete_related_objects(keep_input=keep_input)
restore_pipeline_kwargs = []
if restore_pipelines:
restore_pipeline_kwargs = [
{
"pipeline_name": run.pipeline_name,
"execute_now": execute_now,
"selected_groups": run.selected_groups,
}
for run in self.runs.all()
]

self.delete_related_objects(keep_input=keep_input, keep_labels=True)

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

self.setup_work_directory()

for pipeline_kwargs in restore_pipeline_kwargs:
self.add_pipeline(**pipeline_kwargs)

def clone(
self,
clone_name,
Expand Down
40 changes: 29 additions & 11 deletions scanpipe/templates/scanpipe/modals/project_reset_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,36 @@
<p class="modal-card-title">Reset this project, are you sure?</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="notification is-danger has-text-weight-semibold">
This action cannot be undone.
</div>
<p class="mb-2">
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
</p>
<p class="mb-5">
Are you sure you want to do this?
</p>
</section>
<form action="{% url 'project_reset' project.slug %}" method="post">{% csrf_token %}
<section class="modal-card-body">
<div class="notification is-danger has-text-weight-semibold">
This action cannot be undone.
</div>
<p class="mb-2">
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
</p>
<p class="mb-5">
Are you sure you want to do this?
</p>
<div class="field">
<label class="label">
{{ reset_form.keep_input }}
{{ reset_form.keep_input.label }}
</label>
</div>
<div class="field">
<label class="label">
{{ reset_form.restore_pipelines }}
{{ reset_form.restore_pipelines.label }}
</label>
</div>
<div class="field">
<label class="label">
{{ reset_form.execute_now }}
{{ reset_form.execute_now.label }}
</label>
</div>
</section>
<footer class="modal-card-foot is-flex is-justify-content-space-between">
<button class="button has-text-weight-semibold" type="reset">No, Cancel</button>
<button class="button is-danger is-no-close" type="submit">Yes, Reset Project</button>
Expand Down
64 changes: 41 additions & 23 deletions scanpipe/templates/scanpipe/modals/projects_reset_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,48 @@
<p class="modal-card-title">Reset selected projects, are you sure?</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="notification is-danger has-text-weight-semibold">
This action cannot be undone.
</div>
<p class="mb-2">
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
</p>
<p class="mb-5">
Are you sure you want to do this?
</p>
{% if page_obj.paginator.num_pages > 1 %}
<div class="show-on-all-checked">
<hr>
<div class="field include-all-field">
<label class="checkbox" for="{{ archive_form.select_across.id_for_label }}">
<input type="checkbox" name="{{ archive_form.select_across.name }}" id="{{ archive_form.select_across.id_for_label }}">
Include all {{ paginator.count|intcomma }} projects
</label>
<p class="help">{{ outputs_download_form.select_across.help_text }}</p>
</div>
</div>
{% endif %}
</section>
<form action="{% url 'project_action' %}" method="post" id="reset-projects-form">{% csrf_token %}
<section class="modal-card-body">
<div class="notification is-danger has-text-weight-semibold">
This action cannot be undone.
</div>
<p class="mb-2">
This action will <strong>delete all related database entries and all data on disks</strong> except for the input/ directory.
</p>
<p class="mb-5">
Are you sure you want to do this?
</p>
<div class="field">
<label class="label">
{{ reset_form.keep_input }}
{{ reset_form.keep_input.label }}
</label>
</div>
<div class="field">
<label class="label">
{{ reset_form.restore_pipelines }}
{{ reset_form.restore_pipelines.label }}
</label>
</div>
<div class="field">
<label class="label">
{{ reset_form.execute_now }}
{{ reset_form.execute_now.label }}
</label>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="show-on-all-checked">
<hr>
<div class="field include-all-field">
<label class="checkbox" for="{{ reset_form.select_across.id_for_label }}">
<input type="checkbox" name="{{ reset_form.select_across.name }}" id="{{ reset_form.select_across.id_for_label }}">
Include all {{ paginator.count|intcomma }} projects
</label>
<p class="help">{{ outputs_download_form.select_across.help_text }}</p>
</div>
</div>
{% endif %}
</section>
<input type="hidden" name="{{ action_form.url_query.name }}" value="{{ request.GET.urlencode }}">
<input type="hidden" name="action" value="reset">
<footer class="modal-card-foot is-flex is-justify-content-space-between">
Expand Down
12 changes: 10 additions & 2 deletions scanpipe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def test_scanpipe_project_model_delete_related_objects(self):
package = DiscoveredPackage.objects.create(project=self.project1)
resource.discovered_packages.add(package)

delete_log = self.project1.delete_related_objects()
delete_log = self.project1.delete_related_objects(keep_labels=True)
expected = {
"scanpipe.CodebaseRelation": 0,
"scanpipe.CodebaseResource": 1,
Expand All @@ -185,7 +185,10 @@ def test_scanpipe_project_model_delete_related_objects(self):
"scanpipe.WebhookSubscription": 0,
}
self.assertEqual(expected, delete_log)

# Make sure the labels were deleted too.
self.assertEqual(2, UUIDTaggedItem.objects.count())
self.project1.delete_related_objects()
self.assertEqual(0, UUIDTaggedItem.objects.count())

def test_scanpipe_project_model_delete(self):
Expand Down Expand Up @@ -224,8 +227,13 @@ def test_scanpipe_project_model_reset(self):
self.assertEqual(1, self.project1.codebaseresources.count())
self.assertEqual(1, self.project1.inputsources.count())

self.project1.reset()
self.project1.reset(restore_pipelines=True, execute_now=False)
self.assertEqual(0, self.project1.projectmessages.count())
self.assertEqual(1, self.project1.runs.count())
self.assertEqual(0, self.project1.discoveredpackages.count())
self.assertEqual(0, self.project1.codebaseresources.count())

self.project1.reset()
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
self.assertEqual(0, self.project1.projectmessages.count())
self.assertEqual(0, self.project1.runs.count())
Expand Down
23 changes: 20 additions & 3 deletions scanpipe/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def test_scanpipe_views_project_action_report_view(self):
data = {
"action": "report",
"selected_ids": f"{self.project1.uuid}",
"model_name": "todo",
"report-model_name": "todo",
}
response = self.client.post(url, data=data, follow=True)
self.assertTrue(response.filename.startswith("scancodeio-report-"))
Expand All @@ -237,6 +237,21 @@ def test_scanpipe_views_project_action_report_view(self):
workbook = openpyxl.load_workbook(output_file, read_only=True, data_only=True)
self.assertEqual(["TODOS"], workbook.get_sheet_names())

def test_scanpipe_views_project_action_reset_view(self):
url = reverse("project_action")
data = {
"action": "reset",
"selected_ids": f"{self.project1.uuid}",
"reset-restore_pipelines": "on",
}
self.project1.add_pipeline(pipeline_name="scan_codebase")
response = self.client.post(url, data=data, follow=True)
expected = "1 projects have been reset."
self.assertContains(response, expected)

self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
self.assertEqual(1, self.project1.runs.count())

def test_scanpipe_views_project_action_view_get_project_queryset(self):
queryset = ProjectActionView.get_project_queryset(
selected_project_ids=[self.project1.uuid],
Expand Down Expand Up @@ -733,10 +748,12 @@ def test_scanpipe_views_project_reset_view(self):
self.assertContains(response, expected)

run.set_task_ended(exitcode=0)
response = self.client.post(url, follow=True)
expected = "have been removed."
data = {"reset-restore_pipelines": "on"}
response = self.client.post(url, data=data, follow=True)
expected = "has been reset."
self.assertContains(response, expected)
self.assertTrue(Project.objects.filter(name=self.project1.name).exists())
self.assertEqual(1, self.project1.runs.count())

def test_scanpipe_views_project_settings_view(self):
url = reverse("project_settings", args=[self.project1.slug])
Expand Down
Loading