Skip to content

Commit 84f4f99

Browse files
authored
1524 report api (#1559)
* Use new tab for download and report action #1524 Signed-off-by: tdruez <tdruez@nexb.com> * Refactor the XLSX report logic into a single function #1524 For re-usability: `get_xlsx_report` Signed-off-by: tdruez <tdruez@nexb.com> * Add unit test for get_xlsx_report #1524 Signed-off-by: tdruez <tdruez@nexb.com> * Rename the --sheet option to --model #1524 Signed-off-by: tdruez <tdruez@nexb.com> * Add support for the XLSX report in REST API #1524 Signed-off-by: tdruez <tdruez@nexb.com> --------- Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 0297b40 commit 84f4f99

File tree

14 files changed

+281
-78
lines changed

14 files changed

+281
-78
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Changelog
44
v34.9.5 (unreleased)
55
--------------------
66

7+
- Add support for the XLSX report in REST API.
8+
https://github.com/aboutcode-org/scancode.io/issues/1524
9+
710
v34.9.4 (2025-01-21)
811
--------------------
912

docs/command-line-interface.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,15 +396,15 @@ your outputs on the host machine when running with Docker.
396396

397397
.. _cli_report:
398398

399-
`$ scanpipe report --sheet SHEET`
399+
`$ scanpipe report --model MODEL`
400400
---------------------------------
401401

402402
Generates an XLSX report of selected projects based on the provided criteria.
403403

404404
Required arguments:
405405

406-
- ``--sheet {package,dependency,resource,relation,message,todo}``
407-
Specifies the sheet to include in the XLSX report. Available choices are based on
406+
- ``--model {package,dependency,resource,relation,message,todo}``
407+
Specifies the model to include in the XLSX report. Available choices are based on
408408
predefined object types.
409409

410410
Optional arguments:
@@ -428,12 +428,12 @@ Example usage:
428428
1. Generate a report for all projects tagged with "d2d" and include the **TODOS**
429429
worksheet::
430430

431-
$ scanpipe report --sheet todo --label d2d
431+
$ scanpipe report --model todo --label d2d
432432

433433
2. Generate a report for projects whose names contain the word "audit" and include the
434434
**PACKAGES** worksheet::
435435

436-
$ scanpipe report --sheet package --search audit
436+
$ scanpipe report --model package --search audit
437437

438438
.. _cli_check_compliance:
439439

docs/rest-api.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,69 @@ This action deletes a "not started" or "queued" pipeline run.
587587
{
588588
"status": "Pipeline pipeline_name deleted."
589589
}
590+
591+
XLSX Report
592+
-----------
593+
594+
Generates an XLSX report of selected projects based on the provided criteria.
595+
The model needs to be provided using the ``model`` query parameter.
596+
597+
``GET /api/projects/?model=MODEL``
598+
599+
Data:
600+
- ``model``: ``package``, ``dependency``, ``resource``, ``relation``, ``message``,
601+
``todo``.
602+
603+
**Any available filters can be applied** to **select the set of projects** you want to
604+
include in the report, such as a string contained in the name, or filter by labels:
605+
606+
Example usage:
607+
608+
1. Generate a report for all projects tagged with "d2d" and include the **TODOS**
609+
worksheet::
610+
611+
GET /api/projects/?model=todo&label=d2d
612+
613+
614+
2. Generate a report for projects whose names contain the word "audit" and include the
615+
**PACKAGES** worksheet::
616+
617+
GET /api/projects/?model=package&name__contains=audit
618+
619+
620+
XLSX Report
621+
-----------
622+
623+
Generates an XLSX report for selected projects based on specified criteria. The
624+
``model`` query parameter is required to determine the type of data to include in the
625+
report.
626+
627+
Endpoint:
628+
``GET /api/projects/report/?model=MODEL``
629+
630+
Parameters:
631+
632+
- ``model``: Defines the type of data to include in the report.
633+
Accepted values: ``package``, ``dependency``, ``resource``, ``relation``, ``message``,
634+
``todo``.
635+
636+
.. note::
637+
638+
You can apply any available filters to select the projects to include in the
639+
report. Filters can be based on project attributes, such as a substring in the
640+
name or specific labels.
641+
642+
Example Usage:
643+
644+
1. Generate a report for projects tagged with "d2d" and include the ``TODOS`` worksheet:
645+
646+
.. code-block::
647+
648+
GET /api/projects/report/?model=todo&label=d2d
649+
650+
2. Generate a report for projects whose names contain "audit" and include the
651+
``PACKAGES`` worksheet:
652+
653+
.. code-block::
654+
655+
GET /api/projects/report/?model=package&name__contains=audit

scanpipe/api/views.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from scanpipe.models import Project
5353
from scanpipe.models import Run
5454
from scanpipe.models import RunInProgressError
55+
from scanpipe.pipes import filename_now
5556
from scanpipe.pipes import output
5657
from scanpipe.pipes.compliance import get_project_compliance_alerts
5758
from scanpipe.views import project_results_json_response
@@ -79,6 +80,11 @@ class ProjectFilterSet(django_filters.rest_framework.FilterSet):
7980
method="filter_names",
8081
)
8182
uuid = django_filters.CharFilter()
83+
label = django_filters.CharFilter(
84+
label="Label",
85+
field_name="labels__slug",
86+
distinct=True,
87+
)
8288

8389
class Meta:
8490
model = Project
@@ -90,6 +96,7 @@ class Meta:
9096
"names",
9197
"uuid",
9298
"is_archived",
99+
"label",
93100
]
94101

95102
def filter_names(self, qs, name, value):
@@ -195,6 +202,40 @@ def pipelines(self, request, *args, **kwargs):
195202
]
196203
return Response(pipeline_data)
197204

205+
@action(detail=False)
206+
def report(self, request, *args, **kwargs):
207+
project_qs = self.filter_queryset(self.get_queryset())
208+
209+
model_choices = list(output.object_type_to_model_name.keys())
210+
model = request.GET.get("model")
211+
if not model:
212+
message = {
213+
"error": (
214+
"Specifies the model to include in the XLSX report. "
215+
"Using: ?model=MODEL"
216+
),
217+
"choices": ", ".join(model_choices),
218+
}
219+
return Response(message, status=status.HTTP_400_BAD_REQUEST)
220+
221+
if model not in model_choices:
222+
message = {
223+
"error": f"{model} is not on of the valid choices",
224+
"choices": ", ".join(model_choices),
225+
}
226+
return Response(message, status=status.HTTP_400_BAD_REQUEST)
227+
228+
output_file = output.get_xlsx_report(
229+
project_qs=project_qs,
230+
model_short_name=model,
231+
)
232+
output_file.seek(0)
233+
return FileResponse(
234+
output_file,
235+
filename=f"scancodeio-report-{filename_now()}.xlsx",
236+
as_attachment=True,
237+
)
238+
198239
def get_filtered_response(
199240
self, request, queryset, filterset_class, serializer_class
200241
):

scanpipe/forms.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,15 @@ class ProjectReportForm(BaseProjectActionForm):
297297
model_name = forms.ChoiceField(
298298
label="Choose the object type to include in the XLSX file",
299299
choices=[
300-
("discoveredpackage", "Packages"),
301-
("discovereddependency", "Dependencies"),
302-
("codebaseresource", "Resources"),
303-
("codebaserelation", "Relations"),
304-
("projectmessage", "Messages"),
300+
("package", "Packages"),
301+
("dependency", "Dependencies"),
302+
("resource", "Resources"),
303+
("relation", "Relations"),
304+
("message", "Messages"),
305305
("todo", "TODOs"),
306306
],
307307
required=True,
308-
initial="discoveredpackage",
308+
initial="package",
309309
widget=forms.RadioSelect,
310310
)
311311

scanpipe/management/commands/report.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
from django.core.management import CommandError
2727
from django.core.management.base import BaseCommand
2828

29-
import xlsxwriter
30-
3129
from aboutcode.pipeline import humanize_time
3230
from scanpipe.models import Project
3331
from scanpipe.pipes import filename_now
@@ -48,10 +46,10 @@ def add_arguments(self, parser):
4846
),
4947
)
5048
parser.add_argument(
51-
"--sheet",
49+
"--model",
5250
required=True,
5351
choices=list(output.object_type_to_model_name.keys()),
54-
help="Specifies the sheet to include in the XLSX report.",
52+
help="Specifies the model to include in the XLSX report.",
5553
)
5654
parser.add_argument(
5755
"--search",
@@ -75,8 +73,7 @@ def handle(self, *args, **options):
7573
output_directory = options["output_directory"]
7674
labels = options["labels"]
7775
search = options["search"]
78-
sheet = options["sheet"]
79-
model_name = output.object_type_to_model_name.get(sheet)
76+
model = options["model"]
8077

8178
if not (labels or search):
8279
raise CommandError(
@@ -97,23 +94,13 @@ def handle(self, *args, **options):
9794
msg = f"{project_count} project(s) will be included in the report."
9895
self.stdout.write(msg, self.style.SUCCESS)
9996

100-
worksheet_queryset = output.get_queryset(project=None, model_name=model_name)
101-
worksheet_queryset = worksheet_queryset.filter(project__in=project_qs)
102-
10397
filename = f"scancodeio-report-{filename_now()}.xlsx"
10498
if output_directory:
10599
output_file = Path(f"{output_directory}/{filename}")
106100
else:
107101
output_file = Path(filename)
108102

109-
with xlsxwriter.Workbook(output_file) as workbook:
110-
output.queryset_to_xlsx_worksheet(
111-
worksheet_queryset,
112-
workbook,
113-
exclude_fields=output.XLSX_EXCLUDE_FIELDS,
114-
prepend_fields=["project"],
115-
worksheet_name="TODOS",
116-
)
103+
output_file = output.get_xlsx_report(project_qs, model, output_file)
117104

118105
run_time = timer() - start_time
119106
if self.verbosity > 0:

scanpipe/pipes/output.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import csv
2424
import decimal
25+
import io
2526
import json
2627
import re
2728
from operator import attrgetter
@@ -301,6 +302,7 @@ def to_json(project):
301302
"codebaseresource": "RESOURCES",
302303
"codebaserelation": "RELATIONS",
303304
"projectmessage": "MESSAGES",
305+
"todo": "TODOS",
304306
}
305307

306308
model_name_to_object_type = {
@@ -399,6 +401,31 @@ def add_xlsx_worksheet(workbook, worksheet_name, rows, fields):
399401
return errors_count
400402

401403

404+
def get_xlsx_report(project_qs, model_short_name, output_file=None):
405+
model_name = object_type_to_model_name.get(model_short_name)
406+
if not model_name:
407+
raise ValueError(f"{model_short_name} is not valid.")
408+
409+
worksheet_name = model_name_to_worksheet_name.get(model_short_name)
410+
411+
worksheet_queryset = get_queryset(project=None, model_name=model_name)
412+
worksheet_queryset = worksheet_queryset.filter(project__in=project_qs)
413+
414+
if not output_file:
415+
output_file = io.BytesIO()
416+
417+
with xlsxwriter.Workbook(output_file) as workbook:
418+
queryset_to_xlsx_worksheet(
419+
worksheet_queryset,
420+
workbook,
421+
exclude_fields=XLSX_EXCLUDE_FIELDS,
422+
prepend_fields=["project"],
423+
worksheet_name=worksheet_name,
424+
)
425+
426+
return output_file
427+
428+
402429
# Some scan attributes such as "copyrights" are list of dicts.
403430
#
404431
# 'authors': [{'end_line': 7, 'start_line': 7, 'author': 'John Doe'}],

scanpipe/templates/scanpipe/modals/projects_download_modal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<p class="modal-card-title">Download outputs for selected projects as ZIP file</p>
77
<button class="delete" aria-label="close"></button>
88
</header>
9-
<form action="{% url 'project_action' %}" method="post" id="download-projects-form">{% csrf_token %}
9+
<form action="{% url 'project_action' %}" method="post" id="download-projects-form" target="_blank">{% csrf_token %}
1010
<section class="modal-card-body">
1111
<div class="field">
1212
<label class="label">{{ outputs_download_form.output_format.label }}</label>

scanpipe/templates/scanpipe/modals/projects_report_modal.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<p class="modal-card-title">Report of selected projects</p>
77
<button class="delete" aria-label="close"></button>
88
</header>
9-
<form action="{% url 'project_action' %}" method="post" id="report-projects-form">{% csrf_token %}
9+
<form action="{% url 'project_action' %}" method="post" id="report-projects-form" target="_blank">{% csrf_token %}
1010
<section class="modal-card-body">
1111
<div class="field">
1212
<label class="label">{{ report_form.model_name.label }}</label>

0 commit comments

Comments
 (0)