Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5b7b5cf
Add prototype for Dependency model and view #138
tdruez Jul 5, 2024
e9b7fde
Merge branch 'main' into 138-dependencies
tdruez Jul 15, 2024
108db2d
Remove code duplication to generate SCIO API URLs #138
tdruez Jul 15, 2024
2b35f62
Refactor fetch_results to remove duplication #138
tdruez Jul 15, 2024
872c144
Add admin for the new ProductDependency model #138
tdruez Jul 15, 2024
db04c6d
Add API endpoint for the ProductDependency model #138
tdruez Jul 16, 2024
17b8f6b
Add filters in the Dependencies tab #138
tdruez Jul 16, 2024
c75204e
Refine the rendering of license_expression #138
tdruez Jul 16, 2024
760d92b
Merge branch 'main' into 138-dependencies
tdruez Jul 17, 2024
6522945
Merge branch 'main' into 138-dependencies
tdruez Jul 17, 2024
ce9311c
Consolidate migration files #138
tdruez Jul 17, 2024
565ad1b
Fix couple of failing tests #138
tdruez Jul 17, 2024
144797e
Refine the import_dependency method #138
tdruez Jul 17, 2024
bc6476a
Refactor the whole results system #138
tdruez Jul 17, 2024
6b22815
Proper pagination for Dependencies tab #138
tdruez Jul 17, 2024
a684f70
Refine the Import tab table input column #138
tdruez Jul 17, 2024
984ecdd
Fix the results count #138
tdruez Jul 17, 2024
a8b79f9
Use htmx to load only the tab content on page change #138
tdruez Jul 17, 2024
38e4c8c
Display the dependency Package URL in the tab table #138
tdruez Jul 17, 2024
6d0bca9
Fix template #138
tdruez Jul 17, 2024
ce7faf7
Add inline help in the headers of the Dependencies tab #138
tdruez Jul 18, 2024
67cb308
Add filter by package relationships availability #138
tdruez Jul 18, 2024
72c9c20
Add link from the Inventory tab to the filtered Dependencies #138
tdruez Jul 18, 2024
341e153
Add new Dependency model in reporting #138
tdruez Jul 18, 2024
b7076bd
Display the active filters breadcrumbs in dependencies tab #138
tdruez Jul 24, 2024
0f0dafd
Reorganise the dependencies tab layout #138
tdruez Jul 24, 2024
c1530b9
Merge branch 'main' into 138-dependencies
tdruez Jul 25, 2024
eb07b7f
Refine the Dependencies tab UI #138
tdruez Jul 25, 2024
6d3f4ce
Add full purl search for the Dependencies tab UI #138
tdruez Jul 25, 2024
0d5592c
Add some navigation counts in Dependencies UI #138
tdruez Jul 25, 2024
32ebfc5
Refine the global search UI #138
tdruez Jul 26, 2024
e6c4248
Move the declared_dependency help text to the view #138
tdruez Jul 26, 2024
f70c48a
Round of queryset optimizations #138
tdruez Jul 26, 2024
93724bc
Add declared_dependency field on ProductDependency model #138
tdruez Aug 2, 2024
6cee948
Remove the restrictive pipeline name validation in import #138
tdruez Aug 2, 2024
dbc3a54
WIP on the product scoping for dependency count #138
tdruez Aug 2, 2024
e3d4376
Refine the QuerySet and count Product scoping #138
tdruez Aug 2, 2024
6b4d4ca
Turn off the license_expression rendering #138
tdruez Aug 2, 2024
dc4ca0a
Improve unit tests #138
tdruez Aug 2, 2024
692cfef
Fix failing unit tests #138
tdruez Aug 2, 2024
4d6ca9c
Fix remaining failing unit tests #138
tdruez Aug 2, 2024
a6ee9f6
Consolidate migration files #138
tdruez Aug 2, 2024
4256988
Add unit tests #138
tdruez Aug 2, 2024
6c1cd66
Make sure a Package dependency cannot resolve to itself #138
tdruez Aug 2, 2024
e5820e2
Use assertMaxQueries in place of exact query number #138
tdruez Aug 2, 2024
52d9b41
Add unit test #138
tdruez Aug 5, 2024
2803fcc
Improve the global search UI #138
tdruez Aug 5, 2024
03f4601
Add changelog entry #138
tdruez Aug 5, 2024
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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Release notes
- Use the declared_license_expression_spdx value in SPDX outputs.
https://github.com/nexB/dejacode/issues/63

- Add new ProductDependency model to support relating Packages in the context of a
Product.
https://github.com/nexB/dejacode/issues/138

### Version 5.1.0

- Upgrade Python version to 3.12 and Django to 5.0.x
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ doc8:
--ignore-path docs/installation_and_sysadmin/ --quiet docs/

valid:
@echo "-> Run Ruff linter"
@${ACTIVATE} ruff check --fix
@echo "-> Run Ruff format"
@${ACTIVATE} ruff format
@echo "-> Run Ruff linter"
@${ACTIVATE} ruff check --fix

check:
@echo "-> Run Ruff linter validation (pycodestyle, bandit, isort, and more)"
Expand Down
6 changes: 5 additions & 1 deletion component_catalog/license_expression_dje.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.forms import widgets
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe

from boolean.boolean import PARSE_ERRORS
from license_expression import ExpressionError
Expand Down Expand Up @@ -432,7 +433,10 @@ def render_expression_as_html(expression, dataspace):
licensing = get_dataspace_licensing(dataspace)

formatted_expression = get_formatted_expression(licensing, expression, show_policy)
return format_html(formatted_expression)
return format_html(
'<span class="license-expression">{}</span>',
mark_safe(formatted_expression),
)


def get_expression_as_spdx(expression, dataspace):
Expand Down
30 changes: 30 additions & 0 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from component_catalog.license_expression_dje import get_expression_as_spdx
from component_catalog.license_expression_dje import get_license_objects
from component_catalog.license_expression_dje import parse_expression
from component_catalog.license_expression_dje import render_expression_as_html
from dejacode_toolkit import spdx
from dejacode_toolkit.download import DataCollectionException
from dejacode_toolkit.download import collect_package_data
Expand Down Expand Up @@ -228,6 +229,11 @@ def get_expression_as_spdx(self, expression):
def concluded_license_expression_spdx(self):
return self.get_expression_as_spdx(self.license_expression)

@property
def license_expression_html(self):
if self.license_expression:
return render_expression_as_html(self.license_expression, self.dataspace)

def save(self, *args, **kwargs):
"""
Call the handle_assigned_licenses method on save, except during copy.
Expand Down Expand Up @@ -1627,6 +1633,30 @@ def annotate_sortable_identifier(self):
sortable_identifier=Concat(*PACKAGE_URL_FIELDS, "filename", output_field=CharField())
)

def only_rendering_fields(self):
"""Minimum requirements to render a Package element in the UI."""
return self.only(
"uuid",
*PACKAGE_URL_FIELDS,
"filename",
"license_expression",
"dataspace__name",
"dataspace__show_usage_policy_in_user_views",
)

def declared_dependencies_count(self, product):
"""
Annotate the QuerySet with this each Package declared_dependencies count.
A ``product`` context need to be provided to get the proper counts as
dependencies are always scoped to a Product.
"""
return self.annotate(
declared_dependencies_count=models.Count(
"declared_dependencies",
filter=models.Q(declared_dependencies__product=product),
)
)


class Package(
ExternalReferenceMixin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
<button type="button" data-bs-toggle="tooltip" title="Edit" class="btn btn-link p-0" aria-label="Edit object"><i class="far fa-edit fa-sm"></i></button>
</span>
{% endif %}
{% if relation.package_id and relation.package.declared_dependencies.all %}
<a class="btn badge text-bg-primary rounded-pill ms-1"
href="{{ product.get_absolute_url }}?dependencies-for_package__uuid={{ relation.package.uuid }}#dependencies" class="ms-1" data-bs-toggle="tooltip" title="Dependencies" aria-label="Dependencies">
{{ relation.package.declared_dependencies.all|length }}<i class="fa-solid fa-share-nodes ms-1"></i>
</a>
{% endif %}
{% elif instance.is_active or is_product %}
<a href="{{ instance.get_absolute_url }}#hierarchy">{{ instance }}</a>
{% if relation.component_id and has_edit_productcomponent %}
Expand Down
9 changes: 7 additions & 2 deletions component_catalog/tests/test_license_expression_dje.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,16 @@ def test_get_dataspace_licensing(self):

def test_render_expression_as_html(self):
expression_as_html = render_expression_as_html(str(self.license1.key), self.dataspace)
expected = '<a href="/licenses/Starship/apache-2.0/" title="Apache 2.0">apache-2.0</a>'
expected = (
'<span class="license-expression">'
'<a href="/licenses/Starship/apache-2.0/" title="Apache 2.0">apache-2.0</a>'
"</span>"
)
self.assertEqual(expected, expression_as_html)

expression_as_html = render_expression_as_html("unknown", self.dataspace)
self.assertEqual("unknown", expression_as_html)
expected = '<span class="license-expression">unknown</span>'
self.assertEqual(expected, expression_as_html)


def _print_sequence_diff(left, right):
Expand Down
10 changes: 5 additions & 5 deletions component_catalog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,27 +716,27 @@ def test_component_catalog_list_view_filters_breadcrumbs(self):

expected = f"""
<div class="my-1">
<a href="{href1}" class="text-decoration-none">
<a href="{href1}" class="text-decoration-none me-1">
<span class="badge text-bg-secondary rounded-pill">
Type: "not_a_valid_entry" <i class="fas fa-times-circle"></i>
</span>
</a>
<a href="{href2}" class="text-decoration-none">
<a href="{href2}" class="text-decoration-none me-1">
<span class="badge text-bg-secondary rounded-pill">
License: "license1" <i class="fas fa-times-circle"></i>
</span>
</a>
<a href="{href3}" class="text-decoration-none">
<a href="{href3}" class="text-decoration-none me-1">
<span class="badge text-bg-secondary rounded-pill">
License: "license2" <i class="fas fa-times-circle"></i>
</span>
</a>
<a href="{href4}" class="text-decoration-none">
<a href="{href4}" class="text-decoration-none me-1">
<span class="badge text-bg-secondary rounded-pill">
Search: "a" <i class="fas fa-times-circle"></i>
</span>
</a>
<a href="{href5}" class="text-decoration-none">
<a href="{href5}" class="text-decoration-none me-1">
<span class="badge text-bg-secondary rounded-pill">
Sort: "name" <i class="fas fa-times-circle"></i>
</span>
Expand Down
4 changes: 2 additions & 2 deletions component_catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1682,9 +1682,9 @@ def send_scan_data_as_file_view(request, project_uuid, filename):
raise Http404

scancodeio = ScanCodeIO(dataspace)
scan_results_url = scancodeio.get_scan_results_url(project_uuid)
scan_results_url = scancodeio.get_scan_action_url(project_uuid, "results")
scan_results = scancodeio.fetch_scan_data(scan_results_url)
scan_summary_url = scancodeio.get_scan_summary_url(project_uuid)
scan_summary_url = scancodeio.get_scan_action_url(project_uuid, "summary")
scan_summary = scancodeio.fetch_scan_data(scan_summary_url)

in_memory_zip = io.BytesIO()
Expand Down
2 changes: 2 additions & 0 deletions dejacode/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from policy.api import UsagePolicyViewSet
from product_portfolio.api import CodebaseResourceViewSet
from product_portfolio.api import ProductComponentViewSet
from product_portfolio.api import ProductDependencyViewSet
from product_portfolio.api import ProductPackageViewSet
from product_portfolio.api import ProductViewSet
from reporting.api import ReportViewSet
Expand All @@ -69,6 +70,7 @@
api_router.register("packages", PackageViewSet)
api_router.register("products", ProductViewSet)
api_router.register("product_components", ProductComponentViewSet)
api_router.register("product_dependencies", ProductDependencyViewSet)
api_router.register("product_packages", ProductPackageViewSet)
api_router.register("codebase_resources", CodebaseResourceViewSet)
api_router.register("request_templates", RequestTemplateViewSet)
Expand Down
37 changes: 19 additions & 18 deletions dejacode_toolkit/scancodeio.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,9 @@ def __init__(self, *args, **kwargs):
def get_scan_detail_url(self, project_uuid):
return f"{self.project_api_url}{project_uuid}/"

def get_scan_results_url(self, project_uuid):
def get_scan_action_url(self, project_uuid, action_name):
detail_url = self.get_scan_detail_url(project_uuid)
return f"{detail_url}results/"

def get_scan_summary_url(self, project_uuid):
detail_url = self.get_scan_detail_url(project_uuid)
return f"{detail_url}summary/"

def get_project_packages_url(self, project_uuid):
return f"{self.project_api_url}{project_uuid}/packages/"
return f"{detail_url}{action_name}/"

def get_scan_results(self, download_url, dataspace):
scan_info = self.fetch_scan_info(uri=download_url, dataspace=dataspace)
Expand Down Expand Up @@ -231,22 +224,30 @@ def update_from_scan(self, package, user):

return updated_fields

def fetch_project_packages(self, project_uuid):
"""Return the list of packages for the provided `project_uuid`."""
project_packages_url = self.get_project_packages_url(project_uuid)
packages = []
def fetch_results(self, api_url):
results = []

next_url = project_packages_url
next_url = api_url
while next_url:
logger.debug(f"{self.label}: fetch packages from project_packages_url={next_url}")
logger.debug(f"{self.label}: fetch results from api_url={next_url}")
response = self.request_get(url=next_url)
if not response:
raise Exception("Error fetching project packages")
raise Exception("Error fetching results")

packages.extend(response["results"])
results.extend(response["results"])
next_url = response["next"]

return packages
return results

def fetch_project_packages(self, project_uuid):
"""Return the list of packages for the provided `project_uuid`."""
api_url = self.get_scan_action_url(project_uuid, "packages")
return self.fetch_results(api_url)

def fetch_project_dependencies(self, project_uuid):
"""Return the list of dependencies for the provided `project_uuid`."""
api_url = self.get_scan_action_url(project_uuid, "dependencies")
return self.fetch_results(api_url)

# (label, scan_field, model_field, input_type)
SCAN_SUMMARY_FIELDS = [
Expand Down
24 changes: 15 additions & 9 deletions dje/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,22 @@ def is_active(self):
def get_query_no_sort(self):
return remove_field_from_query_dict(self.data, "sort")

def get_filter_breadcrumb(self, field_name, data_field_name, value):
return {
"label": self.filters[field_name].label,
"value": value,
"remove_url": remove_field_from_query_dict(self.data, data_field_name, value),
}

def get_filters_breadcrumbs(self):
return [
{
"label": self.filters[field_name].label,
"value": value,
"remove_url": remove_field_from_query_dict(self.data, field_name, value),
}
for field_name in self.form.changed_data
for value in self.data.getlist(field_name)
]
breadcrumbs = []

for field_name in self.form.changed_data:
data_field_name = f"{self.form_prefix}-{field_name}" if self.form_prefix else field_name
for value in self.data.getlist(data_field_name):
breadcrumbs.append(self.get_filter_breadcrumb(field_name, data_field_name, value))

return breadcrumbs


class DataspacedFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
Expand Down
19 changes: 13 additions & 6 deletions dje/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,22 @@ def pull_project_data_from_scancodeio(scancodeproject_uuid):
return

scancode_project.status = ScanCodeProject.Status.SUCCESS
msg = f"- Imported {len(created)} package{pluralize(created)}."
scancode_project.append_to_log(msg)

if existing:
msg = f"- {len(existing)} package(s) was/were already available in the Dataspace."
for object_type, values in created.items():
object_type_plural = f"{object_type}{pluralize(values)}"
object_type_plural = object_type_plural.replace("dependencys", "dependencies")
msg = f"- Imported {len(values)} {object_type_plural}."
scancode_project.append_to_log(msg)

if errors:
scancode_project.append_to_log(f"- {len(errors)} errors occurred during import.")
for object_type, values in existing.items():
msg = (
f"- {len(values)} {object_type}{pluralize(values)} already available in the Dataspace."
)
scancode_project.append_to_log(msg)

for object_type, values in errors.items():
msg = f"- {len(values)} {object_type} error{pluralize(values)} " f"occurred during import."
scancode_project.append_to_log(msg)

scancode_project.save()
description = "\n".join(scancode_project.import_log)
Expand Down
Loading