diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f239e0cd..e1ace9cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -109,6 +109,17 @@ Release notes Owner not found in the Dataspace are now automatically created. https://github.com/aboutcode-org/dejacode/issues/239 +- Updated the label of the following Product actions. + The labels were updated everywhere in the UI (page title, documentation, + import log, etc...) for consistency: + - Import data from Scan -> Import ScanCode scan results + - Load Packages from SBOMs -> Import SBOM + - Import Packages from manifests -> Import Package manifests + - Pull ScanCode.io Project data -> Import ScanCode.io project + Improve the rendering and layout of the Import related forms for consistency, + simplicity, and readability. + https://github.com/aboutcode-org/dejacode/issues/241 + ### Version 5.2.1 - Fix the models documentation navigation. diff --git a/component_catalog/templates/component_catalog/includes/scan_status.html b/component_catalog/templates/component_catalog/includes/scan_status.html index 7b534220..2bcad92e 100644 --- a/component_catalog/templates/component_catalog/includes/scan_status.html +++ b/component_catalog/templates/component_catalog/includes/scan_status.html @@ -3,6 +3,8 @@
{% elif status == 'failure' or status == "stopped" or status == "stale" %} + {% elif status == 'warning' %} + {% elif status == 'running' %} {% elif status == 'not_started' or status == 'queued' %} diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index b637c8e5..a8a97f2a 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -590,10 +590,12 @@ div.awesomplete { [data-bs-theme=dark] .awesomplete > ul { background: var(--bs-black); } - #div_id_component .awesomplete { display: inline-block !important; } +label.requiredField { + font-weight: bolder; +} /* -- Products comparison -- */ body.product-comparison tr.unchanged { diff --git a/dje/tasks.py b/dje/tasks.py index fd9a3d88..723e5dc5 100644 --- a/dje/tasks.py +++ b/dje/tasks.py @@ -197,7 +197,7 @@ def pull_project_data_from_scancodeio(scancodeproject_uuid): ) if scancode_project.type == scancode_project.ProjectType.LOAD_SBOMS: - notification_verb = "Load Packages from SBOMs" + notification_verb = "Import SBOM" else: notification_verb = "Import packages from ScanCode.io" diff --git a/dje/templates/includes/form_errors_alert.html b/dje/templates/includes/form_errors_alert.html index f9715da1..d51a80d0 100644 --- a/dje/templates/includes/form_errors_alert.html +++ b/dje/templates/includes/form_errors_alert.html @@ -1,6 +1,5 @@ {% if form.errors %} -Please correct the error{{ form.errors|pluralize }} below.
"files")'),
+ label=_('Create Codebase Resources (from "files" resources)'),
required=False,
initial=False,
help_text=_(
@@ -566,7 +572,16 @@ def __init__(self, user, *args, **kwargs):
@property
def helper(self):
helper = FormHelper()
- helper.add_input(Submit("import", "Import"))
+ helper.attrs = {"autocomplete": "off"}
+ helper.layout = Layout(
+ Fieldset(
+ None,
+ "upload_file",
+ "create_codebase_resources",
+ "stop_on_error",
+ StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
+ ),
+ )
return helper
def save(self, product):
@@ -597,7 +612,6 @@ def save(self, product):
class BaseProductImportFormView(forms.Form):
project_type = None
- input_label = ""
input_file = SmartFileField(
label=_("file or zip archive"),
@@ -626,17 +640,21 @@ class BaseProductImportFormView(forms.Form):
),
)
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["input_file"].label = _(f"{self.input_label} file or zip archive")
-
@property
def helper(self):
helper = FormHelper()
helper.form_method = "post"
helper.form_id = "import-manifest-form"
helper.attrs = {"autocomplete": "off"}
- helper.add_input(Submit("submit", "Load Packages", css_class="btn-success"))
+ helper.layout = Layout(
+ Fieldset(
+ None,
+ "input_file",
+ "update_existing_packages",
+ "scan_all_packages",
+ StrictSubmit("submit", _("Import"), css_class="btn-success col-2"),
+ ),
+ )
return helper
def submit(self, product, user):
@@ -659,17 +677,50 @@ def submit(self, product, user):
)
+def validate_sbom_file(value):
+ """Validate SBOM JSON file content."""
+ filename = value.name.lower()
+ if not filename.endswith(".json"):
+ return
+
+ try:
+ file_content = value.read().decode("utf-8")
+ json_data = json.loads(file_content)
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ raise ValidationError(_("Invalid JSON file. Please provide a properly formatted JSON."))
+ finally:
+ value.seek(0) # Reset file pointer after reading
+
+ if headers := json_data.get("headers", []):
+ tool_name = headers[0].get("tool_name", "")
+ if "scan" in tool_name.lower():
+ raise ValidationError(
+ "Your file appears to be a ScanCode scan results. "
+ 'You want to use the "Import ScanCode scan results" action instead.'
+ )
+
+
class LoadSBOMsForm(BaseProductImportFormView):
project_type = ScanCodeProject.ProjectType.LOAD_SBOMS
- input_label = "SBOM"
pipeline_name = "load_sbom"
+ input_file = SmartFileField(
+ label=_("SBOM file or zip archive"),
+ extensions=["json", "ABOUT", "zip"],
+ validators=[validate_sbom_file],
+ required=True,
+ )
+
class ImportManifestsForm(BaseProductImportFormView):
project_type = ScanCodeProject.ProjectType.IMPORT_FROM_MANIFEST
- input_label = "Manifest"
pipeline_name = "resolve_dependencies"
+ input_file = SmartFileField(
+ label=_("Manifest file or zip archive"),
+ required=True,
+ )
+
class StrongTextWidget(forms.Widget):
def render(self, name, value, attrs=None, renderer=None):
diff --git a/product_portfolio/importers.py b/product_portfolio/importers.py
index 68d632fd..782e5efd 100644
--- a/product_portfolio/importers.py
+++ b/product_portfolio/importers.py
@@ -45,6 +45,7 @@
from product_portfolio.models import ProductItemPurpose
from product_portfolio.models import ProductPackage
from product_portfolio.models import ProductRelationStatus
+from product_portfolio.models import ScanCodeProject
class CleanProductMixin(ComponentRelatedFieldImportMixin):
@@ -394,6 +395,7 @@ def __init__(
self.created_counts = {}
self.warnings = []
self.product_packages_by_id = {}
+ self.scancode_project = None
self.resource_additional_fields = [
"programming_language",
@@ -408,22 +410,21 @@ def __init__(
]
def save(self):
+ self.create_scancode_project()
self.load_data_from_file()
self.validate_headers()
self.import_packages()
if self.create_codebase_resources:
self.import_codebase_resources()
-
+ self.update_scancode_project()
return self.warnings, self.created_counts
def load_data_from_file(self):
- with self.upload_file.open() as f:
- file_content = f.read()
-
try:
- self.data = json.loads(file_content)
- except json.JSONDecodeError:
- raise ValidationError("The file content is not proper JSON.")
+ with self.upload_file.open("r", encoding="utf-8") as f:
+ self.data = json.load(f)
+ except json.JSONDecodeError as e:
+ raise ValidationError(f"Invalid JSON file: {e}")
def validate_headers(self):
"""
@@ -604,6 +605,37 @@ def import_codebase_resources(self):
if codebase_resources_count:
self.created_counts["Codebase Resources"] = codebase_resources_count
+ def create_scancode_project(self):
+ """Create a ScanCodeProject entry to log this import on the Product."""
+ self.scancode_project = ScanCodeProject.objects.create(
+ product=self.product,
+ dataspace=self.product.dataspace,
+ type=ScanCodeProject.ProjectType.IMPORT_SCAN_RESULTS,
+ input_file=self.upload_file,
+ created_by=self.user,
+ status=ScanCodeProject.Status.IMPORT_STARTED,
+ )
+
+ def update_scancode_project(self):
+ """Update the ScanCodeProject entry with import results."""
+ if self.created_counts:
+ status = ScanCodeProject.Status.SUCCESS
+ import_log = [
+ f"- Imported {count} {label.lower()}"
+ for label, count in self.created_counts.items()
+ ]
+ else:
+ status = ScanCodeProject.Status.WARNING
+ import_log = ["Nothing imported."]
+
+ if self.warnings:
+ import_log.extend(self.warnings)
+
+ self.scancode_project.update(
+ status=status,
+ import_log=import_log,
+ )
+
class ImportPackageFromScanCodeIO:
"""
diff --git a/product_portfolio/migrations/0012_alter_scancodeproject_status_and_more.py b/product_portfolio/migrations/0012_alter_scancodeproject_status_and_more.py
new file mode 100644
index 00000000..30548ef9
--- /dev/null
+++ b/product_portfolio/migrations/0012_alter_scancodeproject_status_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.6 on 2025-02-21 22:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('product_portfolio', '0011_alter_product_owner'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='scancodeproject',
+ name='status',
+ field=models.CharField(choices=[('submitted', 'Submitted'), ('importing', 'Import Started'), ('success', 'Completed'), ('failure', 'Failure'), ('warning', 'Warning')], db_index=True, max_length=10),
+ ),
+ migrations.AlterField(
+ model_name='scancodeproject',
+ name='type',
+ field=models.CharField(choices=[('IMPORT_FROM_MANIFEST', 'Import Package manifests'), ('LOAD_SBOMS', 'Import SBOM'), ('PULL_FROM_SCANCODEIO', 'Import ScanCode.io project'), ('IMPROVE_FROM_PURLDB', 'Improve from PurlDB'), ('IMPORT_SCAN_RESULTS', 'Import ScanCode scan results')], db_index=True, help_text='The type of import, for the ProjectType choices.', max_length=50),
+ ),
+ ]
diff --git a/product_portfolio/models.py b/product_portfolio/models.py
index 2c6d1f75..10310bec 100644
--- a/product_portfolio/models.py
+++ b/product_portfolio/models.py
@@ -1457,19 +1457,21 @@ def in_progress(self):
class ScanCodeProject(HistoryFieldsMixin, DataspacedModel):
- """Wrap a ScanCode.io Project."""
+ """Wrap Product imports, such as a ScanCode.io Project."""
class ProjectType(models.TextChoices):
- IMPORT_FROM_MANIFEST = "IMPORT_FROM_MANIFEST", _("Import from Manifest")
- LOAD_SBOMS = "LOAD_SBOMS", _("Load SBOMs")
- PULL_FROM_SCANCODEIO = "PULL_FROM_SCANCODEIO", _("Pull from ScanCode.io")
+ IMPORT_FROM_MANIFEST = "IMPORT_FROM_MANIFEST", _("Import Package manifests")
+ LOAD_SBOMS = "LOAD_SBOMS", _("Import SBOM")
+ PULL_FROM_SCANCODEIO = "PULL_FROM_SCANCODEIO", _("Import ScanCode.io project")
IMPROVE_FROM_PURLDB = "IMPROVE_FROM_PURLDB", _("Improve from PurlDB")
+ IMPORT_SCAN_RESULTS = "IMPORT_SCAN_RESULTS", _("Import ScanCode scan results")
class Status(models.TextChoices):
SUBMITTED = "submitted"
IMPORT_STARTED = "importing"
SUCCESS = "success", _("Completed")
FAILURE = "failure"
+ WARNING = "warning"
uuid = models.UUIDField(
_("UUID"),
diff --git a/product_portfolio/templates/product_portfolio/import_from_scan.html b/product_portfolio/templates/product_portfolio/import_from_scan.html
index 8e702e93..1f734e79 100644
--- a/product_portfolio/templates/product_portfolio/import_from_scan.html
+++ b/product_portfolio/templates/product_portfolio/import_from_scan.html
@@ -3,7 +3,7 @@
{% load crispy_forms_tags %}
{% block page_title %}
- Import from Scan
+ Import ScanCode scan results
{% endblock %}
{% block content %}
@@ -17,50 +17,56 @@
{{ product }}
- Upload a ScanCode.io JSON output file, generated with one of the following pipelines: -
-
- analyze_docker_image,
- analyze_windows_docker_image,
- inspect_packages,
- scan_codebase,
- scan_single_package
-
+ Upload a ScanCode.io JSON output file, generated with one of the following pipelines: +
+
+ analyze_docker_image,
+ analyze_windows_docker_image,
+ inspect_packages,
+ scan_codebase,
+ scan_single_package
+