Skip to content
Open
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
3 changes: 3 additions & 0 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,9 @@ def get_export_csaf_url(self):
def get_export_openvex_url(self):
return self.get_url("export_openvex")

def get_export_license_compliance_url(self):
return self.get_url("export_license_compliance")

@property
def cyclonedx_bom_ref(self):
return str(self.uuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,49 @@
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fs-6 fw-medium mb-0">{% trans "License compliance" %}</h3>
{% if license_issues_count == 0 %}
<span class="badge text-bg-success">
{% trans "OK" %}
</span>
{% elif license_error_count > 0 %}
<span class="badge text-bg-danger">
{% trans "Error" %}
</span>
{% else %}
<span class="badge text-bg-warning">
{% trans "Warning" %}
</span>
{% endif %}
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="dropdown">
<button class="btn btn-sm btn-link text-body-secondary p-0" data-bs-toggle="dropdown">
<i class="fas fa-download"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% with export_license_compliance_url=product.get_export_license_compliance_url %}
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=csv">
<i class="fas fa-download me-1"></i>{% trans "Comma-separated Values (.csv)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=json">
<i class="fas fa-download me-1"></i>{% trans "JSON (.json)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=ods">
<i class="fas fa-download me-1"></i>{% trans "OpenDocument (.ods)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=xlsx">
<i class="fas fa-download me-1"></i>{% trans "Microsoft Excel (.xlsx)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=yaml">
<i class="fas fa-download me-1"></i>{% trans "YAML (.yaml)" %}
</a>
</li>
{% endwith %}
</ul>
</div>
{% if license_issues_count == 0 %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% elif license_error_count > 0 %}
<span class="badge text-bg-danger">{% trans "Error" %}</span>
{% else %}
<span class="badge text-bg-warning">{% trans "Warning" %}</span>
{% endif %}
</div>
</div>

<p class="text-secondary small mb-2">
Expand Down
2 changes: 2 additions & 0 deletions product_portfolio/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from product_portfolio.views import ProductExportCycloneDXBOMView
from product_portfolio.views import ProductExportOpenVEXView
from product_portfolio.views import ProductExportSPDXDocumentView
from product_portfolio.views import ProductLicenseComplianceExportView
from product_portfolio.views import ProductListView
from product_portfolio.views import ProductSendAboutFilesView
from product_portfolio.views import ProductTabCodebaseView
Expand Down Expand Up @@ -115,6 +116,7 @@ def product_path(path_segment, view):
*product_path("export_cyclonedx", ProductExportCycloneDXBOMView.as_view()),
*product_path("export_csaf", ProductExportCSAFDocumentView.as_view()),
*product_path("export_openvex", ProductExportOpenVEXView.as_view()),
*product_path("export_license_compliance", ProductLicenseComplianceExportView.as_view()),
*product_path("attribution", AttributionView.as_view()),
*product_path("change", ProductUpdateView.as_view()),
*product_path("delete", ProductDeleteView.as_view()),
Expand Down
63 changes: 63 additions & 0 deletions product_portfolio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from django.views.generic import DetailView
from django.views.generic import FormView
from django.views.generic import TemplateView
from django.views.generic.detail import BaseDetailView

import odfdo
import saneyaml
Expand Down Expand Up @@ -3026,3 +3027,65 @@ def get_context_data(self, **kwargs):
)

return context


class ProductLicenseComplianceExportView(
LoginRequiredMixin,
ExportComplianceMixin,
BaseProductViewMixin,
DataspaceScopeMixin,
GetDataspacedObjectMixin,
BaseDetailView,
):
"""Export license compliance data for a single product."""

export_filename = "license_compliance"
export_fields = {
"spdx_license_key": "SPDX license key",
"short_name": "Short name",
"key": "Key",
"package_count": "Packages",
"compliance_alert": "Compliance alert",
}

def get_export_filename(self, extension):
timestamp = timezone.now().strftime("%Y-%m-%d_%H%M%S")
product = self.get_object()
slug = f"{product.name}_{product.version}".replace(" ", "_")
return f"{self.export_filename}_{slug}_{timestamp}.{extension}"

def get_export_queryset(self):
productpackages = self.object.productpackages.all()
licenses = License.objects.filter(productpackage__in=productpackages)
return (
licenses.values("key", "short_name", "spdx_license_key")
.annotate(
package_count=Count("productpackage"),
compliance_alert=F("usage_policy__compliance_alert"),
)
.order_by("-package_count")
)

def get_export_rows(self):
return [
tuple(entry.get(field, "") or "" for field in self.export_fields)
for entry in self.get_export_queryset()
]

def export_json(self):
data = list(self.get_export_queryset())
response = HttpResponse(
json.dumps(data, indent=2, default=str),
content_type="application/json",
)
response["Content-Disposition"] = self.get_content_disposition("json")
return response

def export_yaml(self):
data = json.loads(json.dumps(list(self.get_export_queryset()), default=str))
response = HttpResponse(
saneyaml.dump(data),
content_type="application/x-yaml",
)
response["Content-Disposition"] = self.get_content_disposition("yaml")
return response
Loading