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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ include
.settings
TAGS
.idea
.vscode
Include
Lib
.env
Expand Down
20 changes: 20 additions & 0 deletions product_portfolio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ class LoadSBOMsFormSerializer(serializers.Serializer):
default=False,
help_text=LoadSBOMsForm.base_fields["scan_all_packages"].help_text,
)
create_dependencies = serializers.BooleanField(
required=False,
default=False,
help_text=LoadSBOMsForm.base_fields["create_dependencies"].help_text,
)


class ImportManifestsFormSerializer(serializers.Serializer):
Expand All @@ -268,6 +273,11 @@ class ImportManifestsFormSerializer(serializers.Serializer):
default=False,
help_text=ImportManifestsForm.base_fields["scan_all_packages"].help_text,
)
create_dependencies = serializers.BooleanField(
required=False,
default=False,
help_text=ImportManifestsForm.base_fields["create_dependencies"].help_text,
)


class ImportFromScanSerializer(serializers.Serializer):
Expand All @@ -281,6 +291,11 @@ class ImportFromScanSerializer(serializers.Serializer):
default=False,
help_text=ImportFromScanForm.base_fields["create_codebase_resources"].help_text,
)
create_dependencies = serializers.BooleanField(
required=False,
default=False,
help_text=ImportFromScanForm.base_fields["create_dependencies"].help_text,
)
stop_on_error = serializers.BooleanField(
required=False,
default=False,
Expand All @@ -300,6 +315,11 @@ class PullProjectDataSerializer(serializers.Serializer):
default=False,
help_text=PullProjectDataForm.base_fields["update_existing_packages"].help_text,
)
create_dependencies = serializers.BooleanField(
required=False,
default=False,
help_text=PullProjectDataForm.base_fields["create_dependencies"].help_text,
)


class ScanCodeProjectSerializer(DataspacedSerializer):
Expand Down
36 changes: 36 additions & 0 deletions product_portfolio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,15 @@ class ImportFromScanForm(forms.Form):
"imported Packages."
),
)
create_dependencies = forms.BooleanField(
label=_("Create Dependencies"),
required=False,
initial=False,
help_text=_(
"When checked, dependency relationships between packages discovered in the "
"import will be created on the Product."
),
)
stop_on_error = forms.BooleanField(
label=_("Stop and cancel import on data validation error"),
required=False,
Expand All @@ -580,6 +589,7 @@ def helper(self):
None,
"upload_file",
"create_codebase_resources",
"create_dependencies",
"stop_on_error",
StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
),
Expand All @@ -595,6 +605,7 @@ def save(self, product):
self.user,
upload_file=self.cleaned_data.get("upload_file"),
create_codebase_resources=self.cleaned_data.get("create_codebase_resources"),
create_dependencies=self.cleaned_data.get("create_dependencies"),
stop_on_error=self.cleaned_data.get("stop_on_error"),
)

Expand Down Expand Up @@ -650,6 +661,15 @@ class BaseProductImportFormView(forms.Form):
"from the Package URL (purl). A download URL is required for package scanning."
),
)
create_dependencies = forms.BooleanField(
label=_("Create Dependencies"),
required=False,
initial=False,
help_text=_(
"When checked, dependency relationships between packages discovered in the "
"import will be created on the Product."
),
)

@property
def helper(self):
Expand All @@ -664,6 +684,7 @@ def helper(self):
"infer_download_urls",
"update_existing_packages",
"scan_all_packages",
"create_dependencies",
StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
),
)
Expand All @@ -678,6 +699,9 @@ def submit(self, product, user):
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
scan_all_packages=self.cleaned_data.get("scan_all_packages"),
infer_download_urls=self.cleaned_data.get("infer_download_urls"),
import_options={
"create_dependencies": self.cleaned_data.get("create_dependencies", False),
},
created_by=user,
)

Expand Down Expand Up @@ -975,6 +999,15 @@ class PullProjectDataForm(forms.Form):
"without any modification."
),
)
create_dependencies = forms.BooleanField(
label=_("Create Dependencies"),
required=False,
initial=False,
help_text=_(
"When checked, dependency relationships between packages discovered in the "
"import will be created on the Product."
),
)

@property
def helper(self):
Expand Down Expand Up @@ -1007,6 +1040,9 @@ def submit(self, product, user):
project_uuid=project_data.get("uuid"),
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
scan_all_packages=False,
import_options={
"create_dependencies": self.cleaned_data.get("create_dependencies", False),
},
status=ScanCodeProject.Status.SUBMITTED,
created_by=user,
)
Expand Down
36 changes: 27 additions & 9 deletions product_portfolio/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,20 @@ def save_all(self):

class ImportFromScan:
def __init__(
self, product, user, upload_file, create_codebase_resources=True, stop_on_error=False
self,
product,
user,
upload_file,
create_codebase_resources=True,
create_dependencies=False,
stop_on_error=False,
):
self.product = product
self.dataspace = product.dataspace
self.user = user
self.upload_file = upload_file
self.create_codebase_resources = create_codebase_resources
self.create_dependencies = create_dependencies
self.stop_on_error = stop_on_error

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

def _handle_package_dependencies(self, package_data, package_uid, dependencies_by_package_uid):
if self.create_dependencies:
if not package_data.get("dependencies"):
package_data["dependencies"] = dependencies_by_package_uid.get(package_uid, [])
else:
package_data.pop("dependencies", None)

def import_packages(self):
product_packages_count = 0
packages_count = 0
Expand All @@ -475,17 +489,18 @@ def import_packages(self):
'"packages" is empty in the uploaded json file.'
)

dependencies = self.data.get("dependencies", [])
dependencies_by_package_uid = defaultdict(list)
for dependency in dependencies:
for_package_uid = dependency.get("for_package_uid")
dependencies_by_package_uid[for_package_uid].append(dependency)
if self.create_dependencies:
dependencies = self.data.get("dependencies", [])
for dependency in dependencies:
for_package_uid = dependency.get("for_package_uid")
dependencies_by_package_uid[for_package_uid].append(dependency)

for package_data in packages:
package_uid = package_data.get("package_uid")
package_dependencies = package_data.get("dependencies", [])
if not package_dependencies:
package_data["dependencies"] = dependencies_by_package_uid.get(package_uid, [])
self._handle_package_dependencies(
package_data, package_uid, dependencies_by_package_uid
)

prepared = PackageImporter.prepare_package(package_data, path="/")
if not prepared:
Expand Down Expand Up @@ -658,6 +673,7 @@ def __init__(
update_existing=False,
scan_all_packages=False,
infer_download_urls=False,
create_dependencies=False,
):
self.licensing = Licensing()
self.created = defaultdict(list)
Expand All @@ -672,6 +688,7 @@ def __init__(
self.update_existing = update_existing
self.scan_all_packages = scan_all_packages
self.infer_download_urls = infer_download_urls
self.create_dependencies = create_dependencies

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

def save(self):
self.import_packages()
self.import_dependencies()
if self.create_dependencies:
self.import_dependencies()

if self.scan_all_packages:
transaction.on_commit(lambda: self.product.scan_all_packages_task(self.user))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-06-08 05:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('product_portfolio', '0016_alter_productcomponent_weighted_risk_score_and_more'),
]

operations = [
migrations.AddField(
model_name='scancodeproject',
name='import_options',
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.'),
),
]
9 changes: 9 additions & 0 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,14 @@ class Status(models.TextChoices):
infer_download_urls = models.BooleanField(
default=False,
)
import_options = 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."
),
)
status = models.CharField(
max_length=10,
choices=Status.choices,
Expand Down Expand Up @@ -1773,6 +1781,7 @@ def import_data_from_scancodeio(self):
update_existing=self.update_existing_packages,
scan_all_packages=self.scan_all_packages,
infer_download_urls=self.infer_download_urls,
create_dependencies=self.import_options.get("create_dependencies", False),
)
created, existing, errors = importer.save()

Expand Down
Loading
Loading