diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index 497f9975..408549d5 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -5,8 +5,34 @@ {% block content %}
-

{% trans "Compliance Control Center" %}

- {{ total_products }} {% trans "active product" %}{{ total_products|pluralize }} +

+ {% trans "Compliance Control Center" %} +

+ + {{ total_products }} {% trans "active product" %}{{ total_products|pluralize }} + +
diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 48676ae8..ace3b8de 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -67,6 +67,7 @@ from openpyxl.styles import Font from openpyxl.styles import NamedStyle from openpyxl.styles import Side +from openpyxl.utils import get_column_letter from component_catalog.forms import ComponentAjaxForm from component_catalog.license_expression_dje import build_licensing @@ -2828,6 +2829,21 @@ class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView): model = Product filterset_class = ProductFilterSet paginate_by = settings.DEJACODE_PAGINATE_BY.get("compliance", 50) + export_filename = "compliance_dashboard" + export_fields = { + "name": "Product", + "version": "Version", + "package_count": "Packages", + "license_error_count": "License errors", + "license_warning_count": "License warnings", + "max_risk_level": "Max risk level", + "risk_threshold": "Risk threshold", + "critical_count": "Critical", + "high_count": "High", + "medium_count": "Medium", + "low_count": "Low", + "vulnerability_count": "Total vulnerabilities", + } def get_queryset(self): base_qs = Product.objects.get_queryset( @@ -2905,3 +2921,76 @@ def get_context_data(self, **kwargs): ) return context + + def get(self, request, *args, **kwargs): + export_format = request.GET.get("export") + if export_format in ("csv", "xlsx", "json"): + return self.export(export_format) + return super().get(request, *args, **kwargs) + + def export(self, export_format): + if export_format == "csv": + return self.export_csv() + if export_format == "xlsx": + return self.export_xlsx() + return self.export_json() + + def get_export_headers(self): + return list(self.export_fields.values()) + + def get_export_fields(self): + return list(self.export_fields.keys()) + + def get_export_rows(self): + return self.get_queryset().values_list(*self.get_export_fields()) + + def export_csv(self): + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.csv"' + + writer = csv.writer(response) + writer.writerow(self.get_export_headers()) + writer.writerows(self.get_export_rows()) + return response + + def export_xlsx(self): + workbook = Workbook() + worksheet = workbook.active + worksheet.title = "Compliance Dashboard" + + headers = self.get_export_headers() + worksheet.append(headers) + + # Header styling + header_style = NamedStyle(name="header") + header_style.font = Font(bold=True) + header_style.border = Border(bottom=Side(border_style="thin")) + header_style.alignment = Alignment(horizontal="center", vertical="center") + for cell in worksheet[1]: + cell.style = header_style + + worksheet.freeze_panes = "A2" + + for row in self.get_export_rows(): + worksheet.append(row) + + # Auto-width columns + for col_index, header in enumerate(headers, 1): + column_letter = get_column_letter(col_index) + worksheet.column_dimensions[column_letter].width = max(len(header) + 4, 12) + + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.xlsx"' + workbook.save(response) + return response + + def export_json(self): + data = list(self.get_queryset().values(*self.get_export_fields())) + response = HttpResponse( + json.dumps(data, indent=2, default=str), + content_type="application/json", + ) + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.json"' + return response