Skip to content

Commit d4b299c

Browse files
authored
Improve the results_download API action to accept output_format #1091 (#1092)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 79ab9b9 commit d4b299c

File tree

6 files changed

+66
-17
lines changed

6 files changed

+66
-17
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
uses: actions/checkout@v4
3939

4040
- name: Set up Python ${{ matrix.python-version }}
41-
uses: actions/setup-python@v4
41+
uses: actions/setup-python@v5
4242
with:
4343
python-version: ${{ matrix.python-version }}
4444

.github/workflows/pypi-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v4
1616

1717
- name: Set up Python
18-
uses: actions/setup-python@v4
18+
uses: actions/setup-python@v5
1919
with:
2020
python-version: 3.11
2121

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ v33.2.0 (unreleased)
4444
- Add URL scheme validation with explicit error messages for input URLs.
4545
https://github.com/nexB/scancode.io/issues/1047
4646

47+
- All supported `output_format` can now be downloaded using the results_download API
48+
action providing a value for the new `output_format` parameter.
49+
https://github.com/nexB/scancode.io/issues/1091
50+
4751
- Update matchcode-toolkit to v3.0.0
4852

4953
v33.1.0 (2024-02-02)

docs/rest-api.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,19 @@ Displays the results as JSON content compatible with ScanCode data format.
427427
]
428428
}
429429
430-
Results (download)
430+
Results (Download)
431431
^^^^^^^^^^^^^^^^^^
432432

433-
Finally, this action downloads the JSON results as an attachment.
433+
Finally, use this action to download the project results in the provided
434+
``output_format`` as an attachment file.
434435

435-
``GET /api/projects/d4ed9405-5568-45ad-99f6-782a9b82d1d2/results_download/``
436+
Data:
437+
- ``output_format``: ``json``, ``xlsx``, ``spdx``, ``cyclonedx``, ``attribution``
438+
439+
``GET /api/projects/d4ed9405-5568-45ad-99f6-782a9b82d1d2/results_download/?output_format=cyclonedx``
440+
441+
.. tip::
442+
Refer to :ref:`output_files` to learn more about the available output formats.
436443

437444
Run details
438445
-----------

scanpipe/api/views.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,12 @@
4747
from scanpipe.models import Project
4848
from scanpipe.models import Run
4949
from scanpipe.models import RunInProgressError
50+
from scanpipe.pipes import output
5051
from scanpipe.views import project_results_json_response
5152

5253
scanpipe_app = apps.get_app_config("scanpipe")
5354

5455

55-
class PassThroughRenderer(renderers.BaseRenderer):
56-
media_type = ""
57-
58-
def render(self, data, **kwargs):
59-
return data
60-
61-
6256
class ProjectFilterSet(django_filters.rest_framework.FilterSet):
6357
name = django_filters.CharFilter()
6458
name__contains = django_filters.CharFilter(
@@ -140,12 +134,32 @@ def results(self, request, *args, **kwargs):
140134
"""
141135
return project_results_json_response(self.get_object())
142136

143-
@action(
144-
detail=True, name="Results (download)", renderer_classes=[PassThroughRenderer]
145-
)
137+
@action(detail=True, name="Results (download)")
146138
def results_download(self, request, *args, **kwargs):
147-
"""Return the results as an attachment."""
148-
return project_results_json_response(self.get_object(), as_attachment=True)
139+
"""Return the results in the provided `output_format` as an attachment."""
140+
project = self.get_object()
141+
format = request.query_params.get("output_format", "json")
142+
143+
if format == "json":
144+
return project_results_json_response(project, as_attachment=True)
145+
elif format == "xlsx":
146+
output_file = output.to_xlsx(project)
147+
elif format == "spdx":
148+
output_file = output.to_spdx(project)
149+
elif format == "cyclonedx":
150+
output_file = output.to_cyclonedx(project)
151+
elif format == "attribution":
152+
output_file = output.to_attribution(project)
153+
else:
154+
message = {"status": f"Format {format} not supported."}
155+
return Response(message, status=status.HTTP_400_BAD_REQUEST)
156+
157+
filename = output.safe_filename(f"scancodeio_{project.name}_{output_file.name}")
158+
return FileResponse(
159+
output_file.open("rb"),
160+
filename=filename,
161+
as_attachment=True,
162+
)
149163

150164
@action(detail=True)
151165
def summary(self, request, *args, **kwargs):

scanpipe/tests/test_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from scanpipe.pipes.input import copy_input
5454
from scanpipe.pipes.output import JSONResultsGenerator
5555
from scanpipe.tests import dependency_data1
56+
from scanpipe.tests import mocked_now
5657
from scanpipe.tests import package_data1
5758

5859

@@ -524,6 +525,29 @@ def test_scanpipe_api_project_action_results_download(self):
524525
expected = ["dependencies", "files", "headers", "packages", "relations"]
525526
self.assertEqual(expected, sorted(results.keys()))
526527

528+
@mock.patch("scanpipe.pipes.datetime", mocked_now)
529+
def test_scanpipe_api_project_action_results_download_output_formats(self):
530+
url = reverse("project-results-download", args=[self.project1.uuid])
531+
data = {"output_format": "cyclonedx"}
532+
response = self.csrf_client.get(url, data=data)
533+
534+
expected_filename = "scancodeio_analysis_results-2010-10-10-10-10-10.cdx.json"
535+
expected = f'attachment; filename="{expected_filename}"'
536+
self.assertEqual(expected, response["Content-Disposition"])
537+
self.assertEqual("application/json", response["Content-Type"])
538+
539+
response_value = response.getvalue()
540+
results = json.loads(response_value)
541+
self.assertIn("$schema", sorted(results.keys()))
542+
543+
data = {"output_format": "xlsx"}
544+
response = self.csrf_client.get(url, data=data)
545+
expected = [
546+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
547+
"application/octet-stream",
548+
]
549+
self.assertIn(response["Content-Type"], expected)
550+
527551
def test_scanpipe_api_project_action_pipelines(self):
528552
url = reverse("project-pipelines")
529553
response = self.csrf_client.get(url)

0 commit comments

Comments
 (0)