Skip to content

Commit a55913d

Browse files
authored
Add arguments support for the reset action in REST API (#1948)
Signed-off-by: tdruez <[email protected]>
1 parent ebe599d commit a55913d

File tree

3 files changed

+56
-54
lines changed

3 files changed

+56
-54
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
v35.4.2 (unreleased)
5+
--------------------
6+
7+
- Add arguments support for the reset action in REST API.
8+
https://github.com/aboutcode-org/scancode.io/issues/1948
9+
410
v35.4.1 (2025-10-24)
511
--------------------
612

scanpipe/api/views.py

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222

2323
import json
24+
import logging
2425

2526
from django.apps import apps
2627
from django.core.exceptions import ObjectDoesNotExist
@@ -58,9 +59,22 @@
5859
from scanpipe.pipes.compliance import get_project_compliance_alerts
5960
from scanpipe.views import project_results_json_response
6061

62+
logger = logging.getLogger(__name__)
6163
scanpipe_app = apps.get_app_config("scanpipe")
6264

6365

66+
class ErrorResponse(Response):
67+
def __init__(self, message, status_code=status.HTTP_400_BAD_REQUEST, **kwargs):
68+
# If message is already a dict, use it as-is
69+
if isinstance(message, dict):
70+
data = message
71+
else:
72+
# Otherwise, wrap string in {"status": message}
73+
data = {"status": message}
74+
75+
super().__init__(data=data, status=status_code, **kwargs)
76+
77+
6478
class ProjectFilterSet(django_filters.rest_framework.FilterSet):
6579
name = django_filters.CharFilter()
6680
name__contains = django_filters.CharFilter(
@@ -176,8 +190,7 @@ def results_download(self, request, *args, **kwargs):
176190
elif format == "all_outputs":
177191
output_file = output.to_all_outputs(project)
178192
else:
179-
message = {"status": f"Format {format} not supported."}
180-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
193+
return ErrorResponse(f"Format {format} not supported.")
181194

182195
filename = output.safe_filename(f"scancodeio_{project.name}_{output_file.name}")
183196
return FileResponse(
@@ -196,8 +209,7 @@ def summary(self, request, *args, **kwargs):
196209
summary_file = project.get_latest_output(filename="summary")
197210

198211
if not summary_file:
199-
message = {"error": "Summary file not available"}
200-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
212+
return ErrorResponse({"error": "Summary file not available"})
201213

202214
summary_json = json.loads(summary_file.read_text())
203215
return Response(summary_json)
@@ -224,14 +236,14 @@ def report(self, request, *args, **kwargs):
224236
),
225237
"choices": ", ".join(model_choices),
226238
}
227-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
239+
return ErrorResponse(message)
228240

229241
if model not in model_choices:
230242
message = {
231243
"error": f"{model} is not on of the valid choices",
232244
"choices": ", ".join(model_choices),
233245
}
234-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
246+
return ErrorResponse(message)
235247

236248
output_file = output.get_xlsx_report(
237249
project_qs=project_qs,
@@ -254,8 +266,7 @@ def get_filtered_response(
254266
"""
255267
filterset = filterset_class(data=request.GET, queryset=queryset)
256268
if not filterset.is_valid():
257-
message = {"errors": filterset.errors}
258-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
269+
return ErrorResponse({"errors": filterset.errors})
259270

260271
queryset = filterset.qs
261272
paginated_qs = self.paginate_queryset(queryset)
@@ -313,14 +324,12 @@ def file_content(self, request, *args, **kwargs):
313324
try:
314325
codebase_resource = codebase_resources.get(path=path)
315326
except ObjectDoesNotExist:
316-
message = {"status": "Resource not found. Use ?path=<resource_path>"}
317-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
327+
return ErrorResponse("Resource not found. Use ?path=<resource_path>")
318328

319329
try:
320330
file_content = codebase_resource.file_content
321331
except OSError:
322-
message = {"status": "File not available"}
323-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
332+
return ErrorResponse("File not available")
324333

325334
return Response({"file_content": file_content})
326335

@@ -339,32 +348,29 @@ def add_pipeline(self, request, *args, **kwargs):
339348
{"status": "Pipeline added."}, status=status.HTTP_201_CREATED
340349
)
341350

342-
message = {"status": f"{pipeline} is not a valid pipeline."}
343-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
351+
return ErrorResponse(f"{pipeline} is not a valid pipeline.")
344352

345353
message = {
346354
"status": "Pipeline required.",
347355
"pipelines": list(scanpipe_app.pipelines.keys()),
348356
}
349-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
357+
return ErrorResponse(message)
350358

351359
@action(detail=True, methods=["get", "post"])
352360
def add_input(self, request, *args, **kwargs):
353361
project = self.get_object()
354362

355363
if not project.can_change_inputs:
356-
message = {
357-
"status": "Cannot add inputs once a pipeline has started to execute."
358-
}
359-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
364+
return ErrorResponse(
365+
"Cannot add inputs once a pipeline has started to execute."
366+
)
360367

361368
upload_file = request.data.get("upload_file")
362369
upload_file_tag = request.data.get("upload_file_tag", "")
363370
input_urls = request.data.get("input_urls", [])
364371

365372
if not (upload_file or input_urls):
366-
message = {"status": "upload_file or input_urls required."}
367-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
373+
return ErrorResponse("upload_file or input_urls required.")
368374

369375
if upload_file:
370376
project.add_upload(upload_file, tag=upload_file_tag)
@@ -396,13 +402,13 @@ def add_webhook(self, request, *args, **kwargs):
396402
)
397403

398404
# Return validation errors
399-
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
405+
return ErrorResponse(serializer.errors)
400406

401407
def destroy(self, request, *args, **kwargs):
402408
try:
403409
return super().destroy(request, *args, **kwargs)
404-
except RunInProgressError as error:
405-
return Response({"status": str(error)}, status=status.HTTP_400_BAD_REQUEST)
410+
except RunInProgressError:
411+
return ErrorResponse("Cannot delete project while a run is in progress.")
406412

407413
@action(detail=True, methods=["get", "post"])
408414
def archive(self, request, *args, **kwargs):
@@ -423,10 +429,10 @@ def archive(self, request, *args, **kwargs):
423429
remove_codebase=request.data.get("remove_codebase"),
424430
remove_output=request.data.get("remove_output"),
425431
)
426-
except RunInProgressError as error:
427-
return Response(error, status=status.HTTP_400_BAD_REQUEST)
428-
else:
429-
return Response({"status": f"The project {project} has been archived."})
432+
except RunInProgressError:
433+
return ErrorResponse("Cannot archive project while a run is in progress.")
434+
435+
return Response({"status": f"The project {project} has been archived."})
430436

431437
@action(detail=True, methods=["get", "post"])
432438
def reset(self, request, *args, **kwargs):
@@ -437,13 +443,15 @@ def reset(self, request, *args, **kwargs):
437443
return Response({"status": message})
438444

439445
try:
440-
project.reset(keep_input=True)
441-
except RunInProgressError as error:
442-
return Response(error, status=status.HTTP_400_BAD_REQUEST)
443-
else:
444-
message = (
445-
f"All data, except inputs, for the {project} project have been removed."
446+
project.reset(
447+
keep_input=request.data.get("keep_input", True),
448+
restore_pipelines=request.data.get("restore_pipelines", False),
449+
execute_now=request.data.get("execute_now", False),
446450
)
451+
except RunInProgressError:
452+
return ErrorResponse("Cannot reset project while a run is in progress.")
453+
else:
454+
message = f"The {project} project has been reset."
447455
return Response({"status": message})
448456

449457
@action(detail=True, methods=["get"])
@@ -455,8 +463,7 @@ def outputs(self, request, *args, **kwargs):
455463
if file_path.exists():
456464
return FileResponse(file_path.open("rb"))
457465

458-
message = {"status": f"Output file {filename} not found"}
459-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
466+
return ErrorResponse(f"Output file {filename} not found")
460467

461468
action_url = self.reverse_action(self.outputs.url_name, args=[project.pk])
462469
output_data = [
@@ -531,14 +538,11 @@ class RunViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
531538
def start_pipeline(self, request, *args, **kwargs):
532539
run = self.get_object()
533540
if run.task_end_date:
534-
message = {"status": "Pipeline already executed."}
535-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
541+
return ErrorResponse("Pipeline already executed.")
536542
elif run.task_start_date:
537-
message = {"status": "Pipeline already started."}
538-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
543+
return ErrorResponse("Pipeline already started.")
539544
elif run.task_id:
540-
message = {"status": "Pipeline already queued."}
541-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
545+
return ErrorResponse("Pipeline already queued.")
542546

543547
transaction.on_commit(run.start)
544548

@@ -549,8 +553,7 @@ def stop_pipeline(self, request, *args, **kwargs):
549553
run = self.get_object()
550554

551555
if run.status != run.Status.RUNNING:
552-
message = {"status": "Pipeline is not running."}
553-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
556+
return ErrorResponse("Pipeline is not running.")
554557

555558
run.stop_task()
556559
return Response({"status": f"Pipeline {run.pipeline_name} stopped."})
@@ -560,8 +563,7 @@ def delete_pipeline(self, request, *args, **kwargs):
560563
run = self.get_object()
561564

562565
if run.status not in [run.Status.NOT_STARTED, run.Status.QUEUED]:
563-
message = {"status": "Only non started or queued pipelines can be deleted."}
564-
return Response(message, status=status.HTTP_400_BAD_REQUEST)
566+
return ErrorResponse("Only non started or queued pipelines can be deleted.")
565567

566568
run.delete_task()
567569
return Response({"status": f"Pipeline {run.pipeline_name} deleted."})

scanpipe/tests/test_api.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,7 @@ def test_scanpipe_api_project_action_delete(self):
909909

910910
response = self.csrf_client.delete(self.project1_detail_url)
911911
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
912-
expected = (
913-
"Cannot execute this action until all associated pipeline runs are "
914-
"completed."
915-
)
912+
expected = "Cannot delete project while a run is in progress."
916913
self.assertEqual(expected, response.data["status"])
917914

918915
run.set_task_ended(exitcode=0)
@@ -962,10 +959,7 @@ def test_scanpipe_api_project_action_reset(self):
962959

963960
response = self.csrf_client.post(url)
964961
self.assertEqual(status.HTTP_200_OK, response.status_code)
965-
expected = {
966-
"status": "All data, except inputs, for the Analysis project have been "
967-
"removed."
968-
}
962+
expected = {"status": "The Analysis project has been reset."}
969963
self.assertEqual(expected, response.data)
970964
self.assertEqual(0, self.project1.runs.count())
971965
self.assertEqual(0, self.project1.codebaseresources.count())

0 commit comments

Comments
 (0)