Skip to content

Commit 16a2f3e

Browse files
authored
Add a TODOs sheet containing on REQUIRES_REVIEW resources in XLSX #1524 (#1527)
Signed-off-by: tdruez <[email protected]>
1 parent f32e77e commit 16a2f3e

File tree

11 files changed

+148
-28
lines changed

11 files changed

+148
-28
lines changed

CHANGELOG.rst

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ v34.9.4 (unreleased)
3535
at once from a directory containing input files.
3636
https://github.com/aboutcode-org/scancode.io/issues/1437
3737

38+
- Add a "TODOS" sheet containing on REQUIRES_REVIEW resources in XLSX.
39+
https://github.com/aboutcode-org/scancode.io/issues/1524
40+
3841
v34.9.3 (2024-12-31)
3942
--------------------
4043

scanpipe/api/views.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,17 @@ def packages(self, request, *args, **kwargs):
232232
@action(detail=True, filterset_class=None)
233233
def dependencies(self, request, *args, **kwargs):
234234
project = self.get_object()
235-
queryset = project.discovereddependencies.all()
235+
queryset = project.discovereddependencies.prefetch_for_serializer()
236236
return self.get_filtered_response(
237237
request, queryset, DependencyFilterSet, DiscoveredDependencySerializer
238238
)
239239

240240
@action(detail=True, filterset_class=None)
241241
def relations(self, request, *args, **kwargs):
242242
project = self.get_object()
243-
queryset = project.codebaserelations.all()
243+
queryset = project.codebaserelations.select_related(
244+
"from_resource", "to_resource"
245+
)
244246
return self.get_filtered_response(
245247
request, queryset, RelationFilterSet, CodebaseRelationSerializer
246248
)

scanpipe/filters.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,10 @@ def __init__(self, data=None, *args, **kwargs):
423423
if not data or data.get("is_archived", "") == "":
424424
self.queryset = self.queryset.filter(is_archived=False)
425425

426-
active_count = Project.objects.filter(is_archived=False).count()
427-
archived_count = Project.objects.filter(is_archived=True).count()
426+
counts = Project.objects.get_active_archived_counts()
427+
active_count = counts["active_count"]
428+
archived_count = counts["archived_count"]
429+
428430
self.filters["is_archived"].extra["widget"] = BulmaLinkWidget(
429431
choices=[
430432
("", f'<i class="fa-solid fa-seedling"></i> {active_count} Active'),

scanpipe/forms.py

+17
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,23 @@ class ProjectOutputDownloadForm(forms.Form):
272272
)
273273

274274

275+
class ProjectReportForm(forms.Form):
276+
model_name = forms.ChoiceField(
277+
label="Choose the object type to include in the XLSX file",
278+
choices=[
279+
("discoveredpackage", "Packages"),
280+
("discovereddependency", "Dependencies"),
281+
("codebaseresource", "Resources"),
282+
("codebaserelation", "Relations"),
283+
("projectmessage", "Messages"),
284+
("todos", "TODOs"),
285+
],
286+
required=True,
287+
initial="discoveredpackage",
288+
widget=forms.RadioSelect,
289+
)
290+
291+
275292
class ListTextarea(forms.CharField):
276293
"""
277294
A Django form field that displays as a textarea and converts each line of input

scanpipe/models.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@
4444
from django.core.validators import EMPTY_VALUES
4545
from django.db import models
4646
from django.db import transaction
47+
from django.db.models import Case
4748
from django.db.models import Count
4849
from django.db.models import IntegerField
4950
from django.db.models import OuterRef
5051
from django.db.models import Prefetch
5152
from django.db.models import Q
5253
from django.db.models import Subquery
5354
from django.db.models import TextField
55+
from django.db.models import When
5456
from django.db.models.functions import Cast
5557
from django.db.models.functions import Lower
5658
from django.dispatch import receiver
@@ -518,6 +520,16 @@ def with_counts(self, *fields):
518520

519521
return self.annotate(**annotations)
520522

523+
def get_active_archived_counts(self):
524+
return self.aggregate(
525+
active_count=Count(
526+
Case(When(is_archived=False, then=1), output_field=IntegerField())
527+
),
528+
archived_count=Count(
529+
Case(When(is_archived=True, then=1), output_field=IntegerField())
530+
),
531+
)
532+
521533

522534
class UUIDTaggedItem(GenericUUIDTaggedItemBase, TaggedItemBase):
523535
class Meta:
@@ -2913,7 +2925,9 @@ def create_from_data(cls, project, resource_data):
29132925
cleaned_data = {
29142926
field_name: value
29152927
for field_name, value in resource_data.items()
2916-
if field_name in cls.model_fields() and value not in EMPTY_VALUES
2928+
if field_name in cls.model_fields()
2929+
and value not in EMPTY_VALUES
2930+
and field_name != "project"
29172931
}
29182932

29192933
return cls.objects.create(project=project, **cleaned_data)
@@ -3451,7 +3465,9 @@ def create_from_data(cls, project, package_data):
34513465
cleaned_data = {
34523466
field_name: value
34533467
for field_name, value in package_data.items()
3454-
if field_name in cls.model_fields() and value not in EMPTY_VALUES
3468+
if field_name in cls.model_fields()
3469+
and value not in EMPTY_VALUES
3470+
and field_name != "project"
34553471
}
34563472

34573473
discovered_package = cls(project=project, **cleaned_data)
@@ -3912,7 +3928,9 @@ def create_from_data(
39123928
cleaned_data = {
39133929
field_name: value
39143930
for field_name, value in dependency_data.items()
3915-
if field_name in cls.model_fields() and value not in EMPTY_VALUES
3931+
if field_name in cls.model_fields()
3932+
and value not in EMPTY_VALUES
3933+
and field_name != "project"
39163934
}
39173935

39183936
return cls.objects.create(

scanpipe/pipes/output.py

+35-11
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@
5252

5353
from scancodeio import SCAN_NOTICE
5454
from scancodeio import __version__ as scancodeio_version
55+
from scanpipe.models import CodebaseRelation
56+
from scanpipe.models import CodebaseResource
57+
from scanpipe.models import DiscoveredDependency
58+
from scanpipe.models import DiscoveredPackage
59+
from scanpipe.models import ProjectMessage
5560
from scanpipe.pipes import docker
61+
from scanpipe.pipes import flag
5662
from scanpipe.pipes import spdx
5763

5864
scanpipe_app = apps.get_app_config("scanpipe")
@@ -67,17 +73,15 @@ def get_queryset(project, model_name):
6773
"""Return a consistent QuerySet for all supported outputs (json, xlsx, csv, ...)"""
6874
querysets = {
6975
"discoveredpackage": (
70-
project.discoveredpackages.all().order_by(
76+
DiscoveredPackage.objects.order_by(
7177
"type",
7278
"namespace",
7379
"name",
7480
"version",
7581
)
7682
),
7783
"discovereddependency": (
78-
project.discovereddependencies.all()
79-
.prefetch_for_serializer()
80-
.order_by(
84+
DiscoveredDependency.objects.prefetch_for_serializer().order_by(
8185
"type",
8286
"namespace",
8387
"name",
@@ -86,14 +90,20 @@ def get_queryset(project, model_name):
8690
)
8791
),
8892
"codebaseresource": (
89-
project.codebaseresources.without_symlinks().prefetch_for_serializer()
93+
CodebaseResource.objects.without_symlinks().prefetch_for_serializer()
9094
),
9195
"codebaserelation": (
92-
project.codebaserelations.select_related("from_resource", "to_resource")
96+
CodebaseRelation.objects.select_related("from_resource", "to_resource")
9397
),
94-
"projectmessage": project.projectmessages.all(),
98+
"projectmessage": ProjectMessage.objects.all(),
99+
"todos": CodebaseResource.objects.files().status(flag.REQUIRES_REVIEW),
95100
}
96-
return querysets.get(model_name)
101+
102+
queryset = querysets.get(model_name)
103+
if project:
104+
queryset = queryset.filter(project=project)
105+
106+
return queryset
97107

98108

99109
def queryset_to_csv_file(queryset, fieldnames, output_file):
@@ -295,7 +305,11 @@ def to_json(project):
295305

296306

297307
def queryset_to_xlsx_worksheet(
298-
queryset, workbook, exclude_fields=None, extra_fields=None
308+
queryset,
309+
workbook,
310+
exclude_fields=None,
311+
extra_fields=None,
312+
worksheet_name=None,
299313
):
300314
"""
301315
Add a new worksheet to the ``workbook`` ``xlsxwriter.Workbook`` using the
@@ -310,7 +324,7 @@ def queryset_to_xlsx_worksheet(
310324

311325
model_class = queryset.model
312326
model_name = model_class._meta.model_name
313-
worksheet_name = model_name_to_worksheet_name.get(model_name)
327+
worksheet_name = worksheet_name or model_name_to_worksheet_name.get(model_name)
314328

315329
fields = get_serializer_fields(model_class)
316330
exclude_fields = exclude_fields or []
@@ -346,8 +360,12 @@ def _add_xlsx_worksheet(workbook, worksheet_name, rows, fields):
346360

347361
for row_index, record in enumerate(rows, start=1):
348362
row_errors = []
363+
record_is_dict = isinstance(record, dict)
349364
for col_index, field in enumerate(fields):
350-
value = getattr(record, field)
365+
if record_is_dict:
366+
value = record.get(field)
367+
else:
368+
value = getattr(record, field)
351369

352370
if not value:
353371
continue
@@ -481,6 +499,12 @@ def to_xlsx(project):
481499
if layers_data := docker.get_layers_data(project):
482500
_add_xlsx_worksheet(workbook, "LAYERS", layers_data, docker.layer_fields)
483501

502+
todos_queryset = get_queryset(project, "todos")
503+
if todos_queryset:
504+
queryset_to_xlsx_worksheet(
505+
todos_queryset, workbook, exclude_fields, worksheet_name="TODOS"
506+
)
507+
484508
return output_file
485509

486510

scanpipe/templates/scanpipe/modals/projects_report_modal.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
</header>
88
<form action="{% url 'project_action' %}" method="post" id="report-projects-form">{% csrf_token %}
99
<section class="modal-card-body">
10-
<div class="notification is-info has-text-weight-semibold">
11-
All the packages for the selected projects will be included in an XLSX report.
12-
</div>
10+
<ul class="mb-3">
11+
{{ report_form.as_ul }}
12+
</ul>
1313
</section>
1414
<input type="hidden" name="action" value="report">
1515
<footer class="modal-card-foot is-flex is-justify-content-space-between">

scanpipe/tests/pipes/test_d2d.py

+1
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,7 @@ def test_scanpipe_pipes_d2d_map_go_paths(self):
15301530
).count(),
15311531
)
15321532

1533+
@skipIf(sys.platform == "darwin", "Test is failing on macOS")
15331534
def test_scanpipe_pipes_d2d_map_rust_paths(self):
15341535
input_dir = self.project1.input_path
15351536
input_resources = [

scanpipe/tests/pipes/test_output.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from django.core.management import call_command
3434
from django.test import TestCase
3535

36+
import openpyxl
3637
import xlsxwriter
3738
from licensedcode.cache import get_licensing
3839
from lxml import etree
@@ -42,10 +43,12 @@
4243
from scanpipe.models import CodebaseResource
4344
from scanpipe.models import Project
4445
from scanpipe.models import ProjectMessage
46+
from scanpipe.pipes import flag
4547
from scanpipe.pipes import output
4648
from scanpipe.tests import FIXTURES_REGEN
4749
from scanpipe.tests import make_dependency
4850
from scanpipe.tests import make_package
51+
from scanpipe.tests import make_resource_file
4952
from scanpipe.tests import mocked_now
5053
from scanpipe.tests import package_data1
5154

@@ -210,16 +213,30 @@ def test_scanpipe_pipes_outputs_to_xlsx(self):
210213
model="Model",
211214
details={},
212215
)
216+
make_resource_file(
217+
project=project, path="path/file1.ext", status=flag.REQUIRES_REVIEW
218+
)
213219

214220
output_file = output.to_xlsx(project=project)
215221
self.assertIn(output_file.name, project.output_root)
216222

217223
# Make sure the output can be generated even if the work_directory was wiped
218224
shutil.rmtree(project.work_directory)
219-
with self.assertNumQueries(8):
225+
with self.assertNumQueries(10):
220226
output_file = output.to_xlsx(project=project)
221227
self.assertIn(output_file.name, project.output_root)
222228

229+
workbook = openpyxl.load_workbook(output_file, read_only=True, data_only=True)
230+
expected_sheet_names = [
231+
"PACKAGES",
232+
"DEPENDENCIES",
233+
"RESOURCES",
234+
"RELATIONS",
235+
"MESSAGES",
236+
"TODOS",
237+
]
238+
self.assertEqual(expected_sheet_names, workbook.get_sheet_names())
239+
223240
def test_scanpipe_pipes_outputs_vulnerability_as_cyclonedx(self):
224241
component_bom_ref = "pkg:pypi/[email protected]"
225242
data = self.data / "cyclonedx/django-4.0.10-vulnerability.json"

scanpipe/tests/test_views.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ def test_scanpipe_views_project_actions_view(self):
192192
)
193193
self.assertContains(response, expected, html=True)
194194

195+
def test_scanpipe_views_project_action_report_view(self):
196+
url = reverse("project_action")
197+
data = {
198+
"action": "report",
199+
"selected_ids": f"{self.project1.uuid}",
200+
"model_name": "todos",
201+
}
202+
response = self.client.post(url, data=data, follow=True)
203+
self.assertEqual("report.xlsx", response.filename)
204+
195205
def test_scanpipe_views_project_details_is_archived(self):
196206
url = self.project1.get_absolute_url()
197207
expected1 = "WARNING: This project is archived and read-only."
@@ -701,7 +711,7 @@ def test_scanpipe_views_project_views(self):
701711
project2.labels.add("label3", "label4")
702712

703713
url = reverse("project_list")
704-
with self.assertNumQueries(8):
714+
with self.assertNumQueries(7):
705715
self.client.get(url)
706716

707717
with self.assertNumQueries(13):

0 commit comments

Comments
 (0)