From 7c432301885986c09c89a16460d4224cfa7acde3 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 17 Apr 2026 18:56:17 +0400 Subject: [PATCH 1/4] Add CSV export for the compliance dashboard Signed-off-by: tdruez --- .../compliance_dashboard.html | 11 ++++- product_portfolio/views.py | 46 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index 497f9975..5fe8223b 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -5,8 +5,15 @@ {% 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 }} + + + {% trans "Export CSV" %} +
diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 48676ae8..4d022bf2 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -58,7 +58,8 @@ from django.views.generic import DetailView from django.views.generic import FormView from django.views.generic import TemplateView - +import csv +from django.http import HttpResponse from crispy_forms.utils import render_crispy_form from guardian.shortcuts import get_perms as guardian_get_perms from openpyxl import Workbook @@ -2829,6 +2830,11 @@ class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView): filterset_class = ProductFilterSet paginate_by = settings.DEJACODE_PAGINATE_BY.get("compliance", 50) + def get(self, request, *args, **kwargs): + if request.GET.get("export") == "csv": + return self.export_csv() + return super().get(request, *args, **kwargs) + def get_queryset(self): base_qs = Product.objects.get_queryset( user=self.request.user, @@ -2905,3 +2911,41 @@ def get_context_data(self, **kwargs): ) return context + + def export_csv(self): + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.csv"' + + products = self.get_queryset() + writer = csv.writer(response) + writer.writerow([ + "Product", + "Version", + "Packages", + "License errors", + "License warnings", + "Max risk level", + "Risk threshold", + "Critical", + "High", + "Medium", + "Low", + "Total vulnerabilities", + ]) + rows = products.values_list( + "name", + "version", + "package_count", + "license_error_count", + "license_warning_count", + "max_risk_level", + "risk_threshold", + "critical_count", + "high_count", + "medium_count", + "low_count", + "vulnerability_count", + ) + writer.writerows(rows) + + return response From b4c10c49efad721c233d5e8fbf46d8285a9564a7 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 17 Apr 2026 19:27:55 +0400 Subject: [PATCH 2/4] Add XLSX export for the compliance dashboard Signed-off-by: tdruez --- .../compliance_dashboard.html | 20 ++++- product_portfolio/views.py | 74 +++++++++++++++---- 2 files changed, 75 insertions(+), 19 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index 5fe8223b..d00eafdd 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -11,9 +11,23 @@

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

diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 4d022bf2..9d3e4660 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -58,8 +58,7 @@ from django.views.generic import DetailView from django.views.generic import FormView from django.views.generic import TemplateView -import csv -from django.http import HttpResponse + from crispy_forms.utils import render_crispy_form from guardian.shortcuts import get_perms as guardian_get_perms from openpyxl import Workbook @@ -68,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 @@ -2830,11 +2830,6 @@ class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView): filterset_class = ProductFilterSet paginate_by = settings.DEJACODE_PAGINATE_BY.get("compliance", 50) - def get(self, request, *args, **kwargs): - if request.GET.get("export") == "csv": - return self.export_csv() - return super().get(request, *args, **kwargs) - def get_queryset(self): base_qs = Product.objects.get_queryset( user=self.request.user, @@ -2912,13 +2907,14 @@ def get_context_data(self, **kwargs): return context - def export_csv(self): - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.csv"' + def get(self, request, *args, **kwargs): + export_format = request.GET.get("export") + if export_format in ("csv", "xlsx"): + return self.export(export_format) + return super().get(request, *args, **kwargs) - products = self.get_queryset() - writer = csv.writer(response) - writer.writerow([ + def get_export_headers(self): + return [ "Product", "Version", "Packages", @@ -2931,8 +2927,10 @@ def export_csv(self): "Medium", "Low", "Total vulnerabilities", - ]) - rows = products.values_list( + ] + + def get_export_rows(self): + return self.get_queryset().values_list( "name", "version", "package_count", @@ -2946,6 +2944,50 @@ def export_csv(self): "low_count", "vulnerability_count", ) - writer.writerows(rows) + def export(self, export_format): + if export_format == "csv": + return self.export_csv() + return self.export_xlsx() + + def export_csv(self): + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.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"] = 'attachment; filename="compliance_dashboard.xlsx"' + workbook.save(response) return response From 6650ab97d2688632ee94eda51aa3d19b31d029fa Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 17 Apr 2026 19:37:28 +0400 Subject: [PATCH 3/4] Add JSON export for the compliance dashboard Signed-off-by: tdruez --- .../compliance_dashboard.html | 5 ++ product_portfolio/views.py | 70 ++++++++++--------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html index d00eafdd..408549d5 100644 --- a/product_portfolio/templates/product_portfolio/compliance_dashboard.html +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -26,6 +26,11 @@

{% trans "XLSX" %} +
  • + + {% trans "JSON" %} + +
  • diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 9d3e4660..afe85fab 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -2829,6 +2829,20 @@ class ComplianceDashboardView(LoginRequiredMixin, DataspacedFilterView): model = Product filterset_class = ProductFilterSet paginate_by = settings.DEJACODE_PAGINATE_BY.get("compliance", 50) + 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( @@ -2909,46 +2923,25 @@ def get_context_data(self, **kwargs): def get(self, request, *args, **kwargs): export_format = request.GET.get("export") - if export_format in ("csv", "xlsx"): + if export_format in ("csv", "xlsx", "json"): return self.export(export_format) return super().get(request, *args, **kwargs) - def get_export_headers(self): - return [ - "Product", - "Version", - "Packages", - "License errors", - "License warnings", - "Max risk level", - "Risk threshold", - "Critical", - "High", - "Medium", - "Low", - "Total vulnerabilities", - ] - - def get_export_rows(self): - return self.get_queryset().values_list( - "name", - "version", - "package_count", - "license_error_count", - "license_warning_count", - "max_risk_level", - "risk_threshold", - "critical_count", - "high_count", - "medium_count", - "low_count", - "vulnerability_count", - ) - def export(self, export_format): if export_format == "csv": return self.export_csv() - return self.export_xlsx() + 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") @@ -2991,3 +2984,12 @@ def export_xlsx(self): response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.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"] = 'attachment; filename="compliance_dashboard.json"' + return response From 14ca47a7ea096de2724ece312ee4ec7e9726f456 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 17 Apr 2026 19:39:19 +0400 Subject: [PATCH 4/4] refactor the compliance_dashboard filename as a var Signed-off-by: tdruez --- product_portfolio/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/product_portfolio/views.py b/product_portfolio/views.py index afe85fab..ace3b8de 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -2829,6 +2829,7 @@ 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", @@ -2945,7 +2946,7 @@ def get_export_rows(self): def export_csv(self): response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.csv"' + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.csv"' writer = csv.writer(response) writer.writerow(self.get_export_headers()) @@ -2981,7 +2982,7 @@ def export_xlsx(self): response = HttpResponse( content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.xlsx"' + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.xlsx"' workbook.save(response) return response @@ -2991,5 +2992,5 @@ def export_json(self): json.dumps(data, indent=2, default=str), content_type="application/json", ) - response["Content-Disposition"] = 'attachment; filename="compliance_dashboard.json"' + response["Content-Disposition"] = f'attachment; filename="{self.export_filename}.json"' return response