diff --git a/backend/core/generators.py b/backend/core/generators.py index ec85c8976..ba7983e23 100644 --- a/backend/core/generators.py +++ b/backend/core/generators.py @@ -1,10 +1,81 @@ -import os +import io +import matplotlib from icecream import ic +from numpy import char from .models import * from math import ceil +from docxtpl import InlineImage +from docx.shared import Cm +import matplotlib.pyplot as plt +matplotlib.use("Agg") -def gen_audit_context(id): + +def plot_bar(data): + plt.figure(figsize=(10, 6)) + plt.bar( + [item["category"] for item in data], + [item["value"] for item in data], + ) + plt.tight_layout() + chart_buffer = io.BytesIO() + plt.savefig(chart_buffer, format="png") + chart_buffer.seek(0) + plt.close() + return chart_buffer + + +def plot_donut(data, colors=None): + """ + Create a donut chart from the input data + + Args: + data (list): List of dictionaries with 'category' and 'value' keys + + Returns: + io.BytesIO: Buffer containing the donut chart image + """ + plt.figure(figsize=(10, 6)) + + values = [item["value"] for item in data] + labels = [item["category"] for item in data] + + default_colors = [ + "#4CAF50", # Green for Compliant + "#FFC107", # Amber for Partially Compliant + "#F44336", # Red for Non-Compliant + "#9C27B0", # Purple for Not Applicable + "#2196F3", # Blue for Not Assessed + ] + + # Use provided colors or fall back to default + plot_colors = colors if colors is not None else default_colors[: len(values)] + plt.pie( + values, + labels=labels, + colors=plot_colors, + autopct="%1.f%%", # Show percentage + startangle=90, + pctdistance=0.85, # Distance of percentage from the center + wedgeprops={"edgecolor": "white", "linewidth": 1}, + ) + + center_circle = plt.Circle((0, 0), 0.60, fc="white", ec="white") + fig = plt.gcf() + fig.gca().add_artist(center_circle) + + plt.axis("equal") # Equal aspect ratio ensures that pie is drawn as a circle + plt.tight_layout() + + chart_buffer = io.BytesIO() + plt.savefig(chart_buffer, format="png", dpi=300) + chart_buffer.seek(0) + plt.close() + + return chart_buffer + + +def gen_audit_context(id, doc): context = dict() audit = ComplianceAssessment.objects.get(id=id) @@ -12,20 +83,22 @@ def gen_audit_context(id): reviewers = ", ".join([a.email for a in audit.reviewers.all()]) cnt_per_result = audit.get_requirements_result_count() - ic(cnt_per_result) total = sum([res[0] for res in cnt_per_result]) - perc_res = [ - ceil((res[0] / total) * 100) if total > 0 else 0 for res in cnt_per_result + + donut_data = [ + {"category": "Conforme", "value": cnt_per_result[3][0]}, + {"category": "Partiellement conforme", "value": cnt_per_result[1][0]}, + {"category": "Non conforme", "value": cnt_per_result[2][0]}, + {"category": "Non applicable", "value": cnt_per_result[4][0]}, + {"category": "Non évalué", "value": cnt_per_result[0][0]}, ] - ic(perc_res) + + res_donut = InlineImage(doc, plot_donut(donut_data), width=Cm(15)) + context = { - "name": audit.name, - "framework": audit.framework.name, - "date": now().strftime("%Y-%m-%d %H:%M"), + "audit": audit, + "date": now().strftime("%d/%m/%Y"), "contributors": f"{authors}\n{reviewers}", - "description": audit.description, - "domain": audit.project, - "observation": audit.observation, "req": { "total": total, "compliant": cnt_per_result[3][0], @@ -33,12 +106,8 @@ def gen_audit_context(id): "non_compliant": cnt_per_result[2][0], "not_applicable": cnt_per_result[4][0], "not_assessed": cnt_per_result[0][0], - "compliant_p": perc_res[3], - "part_compliant_p": perc_res[1], - "non_compliant_p": perc_res[2], - "not_applicable_p": perc_res[4], - "not_assessed_p": perc_res[0], }, + "chart_progress_donut": res_donut, } return context diff --git a/backend/core/templates/core/audit_report_template.docx b/backend/core/templates/core/audit_report_template.docx index 78768e133..18fa3cd5e 100644 Binary files a/backend/core/templates/core/audit_report_template.docx and b/backend/core/templates/core/audit_report_template.docx differ diff --git a/backend/core/views.py b/backend/core/views.py index 9e89c3a97..9f1e52bce 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -2166,7 +2166,7 @@ def word_report(self, request, pk): / "audit_report_template.docx" ) doc = DocxTemplate(template_path) - context = gen_audit_context(pk) + context = gen_audit_context(pk, doc) doc.render(context) buffer_doc = io.BytesIO() doc.save(buffer_doc)