Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pypi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: 3.11

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ v33.2.0 (unreleased)
- Add URL scheme validation with explicit error messages for input URLs.
https://github.com/nexB/scancode.io/issues/1047

- All supported `output_format` can now be downloaded using the results_download API
action providing a value for the new `output_format` parameter.
https://github.com/nexB/scancode.io/issues/1091

- Update matchcode-toolkit to v3.0.0

v33.1.0 (2024-02-02)
Expand Down
13 changes: 10 additions & 3 deletions docs/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,19 @@ Displays the results as JSON content compatible with ScanCode data format.
]
}

Results (download)
Results (Download)
^^^^^^^^^^^^^^^^^^

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

``GET /api/projects/d4ed9405-5568-45ad-99f6-782a9b82d1d2/results_download/``
Data:
- ``output_format``: ``json``, ``xlsx``, ``spdx``, ``cyclonedx``, ``attribution``

``GET /api/projects/d4ed9405-5568-45ad-99f6-782a9b82d1d2/results_download/?output_format=cyclonedx``

.. tip::
Refer to :ref:`output_files` to learn more about the available output formats.

Run details
-----------
Expand Down
38 changes: 26 additions & 12 deletions scanpipe/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,12 @@
from scanpipe.models import Project
from scanpipe.models import Run
from scanpipe.models import RunInProgressError
from scanpipe.pipes import output
from scanpipe.views import project_results_json_response

scanpipe_app = apps.get_app_config("scanpipe")


class PassThroughRenderer(renderers.BaseRenderer):
media_type = ""

def render(self, data, **kwargs):
return data


class ProjectFilterSet(django_filters.rest_framework.FilterSet):
name = django_filters.CharFilter()
name__contains = django_filters.CharFilter(
Expand Down Expand Up @@ -140,12 +134,32 @@ def results(self, request, *args, **kwargs):
"""
return project_results_json_response(self.get_object())

@action(
detail=True, name="Results (download)", renderer_classes=[PassThroughRenderer]
)
@action(detail=True, name="Results (download)")
def results_download(self, request, *args, **kwargs):
"""Return the results as an attachment."""
return project_results_json_response(self.get_object(), as_attachment=True)
"""Return the results in the provided `output_format` as an attachment."""
project = self.get_object()
format = request.query_params.get("output_format", "json")

if format == "json":
return project_results_json_response(project, as_attachment=True)
elif format == "xlsx":
output_file = output.to_xlsx(project)
elif format == "spdx":
output_file = output.to_spdx(project)
elif format == "cyclonedx":
output_file = output.to_cyclonedx(project)
elif format == "attribution":
output_file = output.to_attribution(project)
else:
message = {"status": f"Format {format} not supported."}
return Response(message, status=status.HTTP_400_BAD_REQUEST)

filename = output.safe_filename(f"scancodeio_{project.name}_{output_file.name}")
return FileResponse(
output_file.open("rb"),
filename=filename,
as_attachment=True,
)

@action(detail=True)
def summary(self, request, *args, **kwargs):
Expand Down
24 changes: 24 additions & 0 deletions scanpipe/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from scanpipe.pipes.input import copy_input
from scanpipe.pipes.output import JSONResultsGenerator
from scanpipe.tests import dependency_data1
from scanpipe.tests import mocked_now
from scanpipe.tests import package_data1


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

@mock.patch("scanpipe.pipes.datetime", mocked_now)
def test_scanpipe_api_project_action_results_download_output_formats(self):
url = reverse("project-results-download", args=[self.project1.uuid])
data = {"output_format": "cyclonedx"}
response = self.csrf_client.get(url, data=data)

expected_filename = "scancodeio_analysis_results-2010-10-10-10-10-10.cdx.json"
expected = f'attachment; filename="{expected_filename}"'
self.assertEqual(expected, response["Content-Disposition"])
self.assertEqual("application/json", response["Content-Type"])

response_value = response.getvalue()
results = json.loads(response_value)
self.assertIn("$schema", sorted(results.keys()))

data = {"output_format": "xlsx"}
response = self.csrf_client.get(url, data=data)
expected = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/octet-stream",
]
self.assertIn(response["Content-Type"], expected)

def test_scanpipe_api_project_action_pipelines(self):
url = reverse("project-pipelines")
response = self.csrf_client.get(url)
Expand Down