Skip to content

Commit c338408

Browse files
authored
feat: add create_dependencies option to all import forms (#540)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent aa71933 commit c338408

9 files changed

Lines changed: 235 additions & 9 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ include
1616
.settings
1717
TAGS
1818
.idea
19+
.vscode
1920
Include
2021
Lib
2122
.env

product_portfolio/api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ class LoadSBOMsFormSerializer(serializers.Serializer):
243243
default=False,
244244
help_text=LoadSBOMsForm.base_fields["scan_all_packages"].help_text,
245245
)
246+
create_dependencies = serializers.BooleanField(
247+
required=False,
248+
default=False,
249+
help_text=LoadSBOMsForm.base_fields["create_dependencies"].help_text,
250+
)
246251

247252

248253
class ImportManifestsFormSerializer(serializers.Serializer):
@@ -268,6 +273,11 @@ class ImportManifestsFormSerializer(serializers.Serializer):
268273
default=False,
269274
help_text=ImportManifestsForm.base_fields["scan_all_packages"].help_text,
270275
)
276+
create_dependencies = serializers.BooleanField(
277+
required=False,
278+
default=False,
279+
help_text=ImportManifestsForm.base_fields["create_dependencies"].help_text,
280+
)
271281

272282

273283
class ImportFromScanSerializer(serializers.Serializer):
@@ -281,6 +291,11 @@ class ImportFromScanSerializer(serializers.Serializer):
281291
default=False,
282292
help_text=ImportFromScanForm.base_fields["create_codebase_resources"].help_text,
283293
)
294+
create_dependencies = serializers.BooleanField(
295+
required=False,
296+
default=False,
297+
help_text=ImportFromScanForm.base_fields["create_dependencies"].help_text,
298+
)
284299
stop_on_error = serializers.BooleanField(
285300
required=False,
286301
default=False,
@@ -300,6 +315,11 @@ class PullProjectDataSerializer(serializers.Serializer):
300315
default=False,
301316
help_text=PullProjectDataForm.base_fields["update_existing_packages"].help_text,
302317
)
318+
create_dependencies = serializers.BooleanField(
319+
required=False,
320+
default=False,
321+
help_text=PullProjectDataForm.base_fields["create_dependencies"].help_text,
322+
)
303323

304324

305325
class ScanCodeProjectSerializer(DataspacedSerializer):

product_portfolio/forms.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,15 @@ class ImportFromScanForm(forms.Form):
554554
"imported Packages."
555555
),
556556
)
557+
create_dependencies = forms.BooleanField(
558+
label=_("Create Dependencies"),
559+
required=False,
560+
initial=False,
561+
help_text=_(
562+
"When checked, dependency relationships between packages discovered in the "
563+
"import will be created on the Product."
564+
),
565+
)
557566
stop_on_error = forms.BooleanField(
558567
label=_("Stop and cancel import on data validation error"),
559568
required=False,
@@ -580,6 +589,7 @@ def helper(self):
580589
None,
581590
"upload_file",
582591
"create_codebase_resources",
592+
"create_dependencies",
583593
"stop_on_error",
584594
StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
585595
),
@@ -595,6 +605,7 @@ def save(self, product):
595605
self.user,
596606
upload_file=self.cleaned_data.get("upload_file"),
597607
create_codebase_resources=self.cleaned_data.get("create_codebase_resources"),
608+
create_dependencies=self.cleaned_data.get("create_dependencies"),
598609
stop_on_error=self.cleaned_data.get("stop_on_error"),
599610
)
600611

@@ -650,6 +661,15 @@ class BaseProductImportFormView(forms.Form):
650661
"from the Package URL (purl). A download URL is required for package scanning."
651662
),
652663
)
664+
create_dependencies = forms.BooleanField(
665+
label=_("Create Dependencies"),
666+
required=False,
667+
initial=False,
668+
help_text=_(
669+
"When checked, dependency relationships between packages discovered in the "
670+
"import will be created on the Product."
671+
),
672+
)
653673

654674
@property
655675
def helper(self):
@@ -664,6 +684,7 @@ def helper(self):
664684
"infer_download_urls",
665685
"update_existing_packages",
666686
"scan_all_packages",
687+
"create_dependencies",
667688
StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
668689
),
669690
)
@@ -678,6 +699,9 @@ def submit(self, product, user):
678699
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
679700
scan_all_packages=self.cleaned_data.get("scan_all_packages"),
680701
infer_download_urls=self.cleaned_data.get("infer_download_urls"),
702+
import_options={
703+
"create_dependencies": self.cleaned_data.get("create_dependencies", False),
704+
},
681705
created_by=user,
682706
)
683707

@@ -975,6 +999,15 @@ class PullProjectDataForm(forms.Form):
975999
"without any modification."
9761000
),
9771001
)
1002+
create_dependencies = forms.BooleanField(
1003+
label=_("Create Dependencies"),
1004+
required=False,
1005+
initial=False,
1006+
help_text=_(
1007+
"When checked, dependency relationships between packages discovered in the "
1008+
"import will be created on the Product."
1009+
),
1010+
)
9781011

9791012
@property
9801013
def helper(self):
@@ -1007,6 +1040,9 @@ def submit(self, product, user):
10071040
project_uuid=project_data.get("uuid"),
10081041
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
10091042
scan_all_packages=False,
1043+
import_options={
1044+
"create_dependencies": self.cleaned_data.get("create_dependencies", False),
1045+
},
10101046
status=ScanCodeProject.Status.SUBMITTED,
10111047
created_by=user,
10121048
)

product_portfolio/importers.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,20 @@ def save_all(self):
383383

384384
class ImportFromScan:
385385
def __init__(
386-
self, product, user, upload_file, create_codebase_resources=True, stop_on_error=False
386+
self,
387+
product,
388+
user,
389+
upload_file,
390+
create_codebase_resources=True,
391+
create_dependencies=False,
392+
stop_on_error=False,
387393
):
388394
self.product = product
389395
self.dataspace = product.dataspace
390396
self.user = user
391397
self.upload_file = upload_file
392398
self.create_codebase_resources = create_codebase_resources
399+
self.create_dependencies = create_dependencies
393400
self.stop_on_error = stop_on_error
394401

395402
self.data = {}
@@ -465,6 +472,13 @@ def validate_toolkit_options(scan_options):
465472
options_str = " ".join(missing_options)
466473
raise ValidationError(f"The Scan run is missing those required options: {options_str}")
467474

475+
def _handle_package_dependencies(self, package_data, package_uid, dependencies_by_package_uid):
476+
if self.create_dependencies:
477+
if not package_data.get("dependencies"):
478+
package_data["dependencies"] = dependencies_by_package_uid.get(package_uid, [])
479+
else:
480+
package_data.pop("dependencies", None)
481+
468482
def import_packages(self):
469483
product_packages_count = 0
470484
packages_count = 0
@@ -475,17 +489,18 @@ def import_packages(self):
475489
'"packages" is empty in the uploaded json file.'
476490
)
477491

478-
dependencies = self.data.get("dependencies", [])
479492
dependencies_by_package_uid = defaultdict(list)
480-
for dependency in dependencies:
481-
for_package_uid = dependency.get("for_package_uid")
482-
dependencies_by_package_uid[for_package_uid].append(dependency)
493+
if self.create_dependencies:
494+
dependencies = self.data.get("dependencies", [])
495+
for dependency in dependencies:
496+
for_package_uid = dependency.get("for_package_uid")
497+
dependencies_by_package_uid[for_package_uid].append(dependency)
483498

484499
for package_data in packages:
485500
package_uid = package_data.get("package_uid")
486-
package_dependencies = package_data.get("dependencies", [])
487-
if not package_dependencies:
488-
package_data["dependencies"] = dependencies_by_package_uid.get(package_uid, [])
501+
self._handle_package_dependencies(
502+
package_data, package_uid, dependencies_by_package_uid
503+
)
489504

490505
prepared = PackageImporter.prepare_package(package_data, path="/")
491506
if not prepared:
@@ -658,6 +673,7 @@ def __init__(
658673
update_existing=False,
659674
scan_all_packages=False,
660675
infer_download_urls=False,
676+
create_dependencies=False,
661677
):
662678
self.licensing = Licensing()
663679
self.created = defaultdict(list)
@@ -672,6 +688,7 @@ def __init__(
672688
self.update_existing = update_existing
673689
self.scan_all_packages = scan_all_packages
674690
self.infer_download_urls = infer_download_urls
691+
self.create_dependencies = create_dependencies
675692

676693
scancodeio = ScanCodeIO(user.dataspace)
677694
self.packages = scancodeio.fetch_project_packages(self.project_uuid)
@@ -681,7 +698,8 @@ def __init__(
681698

682699
def save(self):
683700
self.import_packages()
684-
self.import_dependencies()
701+
if self.create_dependencies:
702+
self.import_dependencies()
685703

686704
if self.scan_all_packages:
687705
transaction.on_commit(lambda: self.product.scan_all_packages_task(self.user))
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 6.0.5 on 2026-06-08 05:45
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('product_portfolio', '0016_alter_productcomponent_weighted_risk_score_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='scancodeproject',
15+
name='import_options',
16+
field=models.JSONField(blank=True, default=dict, help_text='A dictionary of options used to configure the import process. New options can be added here without requiring a database migration.'),
17+
),
18+
]

product_portfolio/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,14 @@ class Status(models.TextChoices):
17121712
infer_download_urls = models.BooleanField(
17131713
default=False,
17141714
)
1715+
import_options = models.JSONField(
1716+
blank=True,
1717+
default=dict,
1718+
help_text=_(
1719+
"A dictionary of options used to configure the import process. "
1720+
"New options can be added here without requiring a database migration."
1721+
),
1722+
)
17151723
status = models.CharField(
17161724
max_length=10,
17171725
choices=Status.choices,
@@ -1773,6 +1781,7 @@ def import_data_from_scancodeio(self):
17731781
update_existing=self.update_existing_packages,
17741782
scan_all_packages=self.scan_all_packages,
17751783
infer_download_urls=self.infer_download_urls,
1784+
create_dependencies=self.import_options.get("create_dependencies", False),
17761785
)
17771786
created, existing, errors = importer.save()
17781787

0 commit comments

Comments
 (0)