Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

word export #1151

Merged
merged 27 commits into from
Dec 8, 2024
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.DS_Store
*~$*
.env
venv
**/node_modules/
.vscode
*.sqlite3
Expand Down
323 changes: 323 additions & 0 deletions backend/core/generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import io
import matplotlib
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
import numpy as np

# from icecream import ic

from django.db.models import Count

matplotlib.use("Agg")


def plot_horizontal_bar(data, colors=None, title=None):
"""
Create a horizontal bar chart from the input data

Args:
data (list): List of dictionaries with 'category' and 'value' keys
colors (list, optional): Custom color palette
title (str, optional): Chart title

Returns:
io.BytesIO: Buffer containing the horizontal bar chart image
"""
plt.close("all")

categories = [item["category"] for item in data]
values = [item["value"] for item in data]

default_colors = [
"#2196F3", # Blue
"#4CAF50", # Green
"#FFC107", # Amber
"#F44336", # Red
"#9C27B0", # Purple
]

plt.figure(figsize=(10, 6))
plot_colors = colors if colors is not None else default_colors[: len(categories)]
plt.barh(categories, values, color=plot_colors)
for i, v in enumerate(values):
plt.text(v, i, f" {v}", va="center")

if title:
plt.title(title)

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 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.close("all")

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
]

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 plot_spider_chart(data, colors=None, title=None):
"""
Create a spider/radar chart from the input data

Args:
data (list): List of dictionaries with 'category' and 'value' keys
colors (list, optional): Custom color palette
title (str, optional): Chart title

Returns:
io.BytesIO: Buffer containing the spider chart image
"""
plt.close("all")

categories = [item["category"] for item in data]
values = [item["value"] for item in data]

N = len(categories)

default_colors = [
"#2196F3", # Blue
"#4CAF50", # Green
"#FFC107", # Amber
"#F44336", # Red
"#9C27B0", # Purple
]

# Compute angle for each axis
angles = [n / float(N) * 2 * np.pi for n in range(N)]

# Close the plot by appending the first value and angle
values += values[:1]
angles += angles[:1]

# Create the plot
plt.figure(figsize=(12, 12))
ax = plt.subplot(111, polar=True)

plot_colors = colors if colors is not None else default_colors[: len(categories)]

ax.plot(angles, values, "o-", linewidth=2, color=plot_colors[0])
ax.fill(angles, values, alpha=0.25, color=plot_colors[0])

# Fix axis to go in the right order and start at 12 o'clock
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)

# Draw axis lines for each angle and label
plt.xticks(angles[:-1], categories)

# Set y-axis limits (optional, adjust as needed)
ax.set_ylim(0, max(values) * 1.1)

plt.tight_layout()
chart_buffer = io.BytesIO()
plt.savefig(chart_buffer, format="png", dpi=300, bbox_inches="tight")
chart_buffer.seek(0)
plt.close()

return chart_buffer


def gen_audit_context(id, doc, tree):
def count_category_results(data):
def recursive_result_count(node_data):
# Initialize result counts for this node
result_counts = {}

# Check if the current node is assessable
if node_data.get("assessable", False):
result = node_data.get("result", "unknown")
result_counts[result] = 1

# Recursively process children
for child_id, child_data in node_data.get("children", {}).items():
child_results = recursive_result_count(child_data)

# Merge child results into current results
for result, count in child_results.items():
result_counts[result] = result_counts.get(result, 0) + count

return result_counts

# Dictionary to store result counts for top-level nodes
category_result_counts = {}

# Process only top-level nodes
for node_id, node_data in data.items():
if node_data.get("parent_urn") is None:
category_result_counts[node_data["urn"]] = recursive_result_count(
node_data
)

return category_result_counts

context = dict()
audit = ComplianceAssessment.objects.get(id=id)

authors = ", ".join([a.email for a in audit.authors.all()])
reviewers = ", ".join([a.email for a in audit.reviewers.all()])

spider_data = list()
result_counts = count_category_results(tree)

agg_drifts = list()

for key, content in tree.items():
total = sum(result_counts[content["urn"]].values())
ok_items = result_counts[content["urn"]].get("compliant", 0) + result_counts[
content["urn"]
].get("not_applicable", 0)
ok_perc = ceil(ok_items / total * 100) if total > 0 else 0
not_ok_count = total - ok_items
spider_data.append({"category": content["node_content"], "value": ok_perc})
agg_drifts.append(
{"name": content["node_content"], "drift_count": not_ok_count}
)

aggregated = {
"compliant": 0,
"non_compliant": 0,
"not_applicable": 0,
"not_assessed": 0,
"partially_compliant": 0,
}

for node in result_counts.values():
for status, count in node.items():
if status in aggregated:
aggregated[status] += count

total = sum([v for v in aggregated.values()])
if total == 0:
print("Error:: No requirments found, something is wrong. aborting ..")

aggregated["total"] = total

donut_data = [
{"category": "Conforme", "value": aggregated["compliant"]},
{
"category": "Partiellement conforme",
"value": aggregated["partially_compliant"],
},
{"category": "Non conforme", "value": aggregated["non_compliant"]},
{"category": "Non applicable", "value": aggregated["not_applicable"]},
{"category": "Non évalué", "value": aggregated["not_assessed"]},
]

custom_colors = ["#2196F3"]
spider_chart_buffer = plot_spider_chart(
spider_data,
colors=custom_colors,
)

requirement_assessments_objects = audit.get_requirement_assessments(
include_non_assessable=True
)
applied_controls = AppliedControl.objects.filter(
requirement_assessments__in=requirement_assessments_objects
).distinct()
ac_total = applied_controls.count()
status_cnt = applied_controls.values("status").annotate(count=Count("id"))
ac_chart_data = [
{"category": item["status"], "value": item["count"]} for item in status_cnt
]
p1_controls = list()
for ac in applied_controls.filter(priority=1):
requirements_count = (
RequirementAssessment.objects.filter(compliance_assessment=audit)
.filter(applied_controls=ac.id)
.count()
)
p1_controls.append(
{
"name": ac.name,
"description": ac.description,
"status": ac.status,
"category": ac.category,
"coverage": requirements_count,
}
)

custom_colors = [
"#CCC",
"#46D39A",
"#E55759",
"#392F5A",
"#F4D06F",
"#BFDBFE",
]
hbar_buffer = plot_horizontal_bar(ac_chart_data, colors=custom_colors)

res_donut = InlineImage(doc, plot_donut(donut_data), width=Cm(15))
chart_spider = InlineImage(doc, spider_chart_buffer, width=Cm(15))
ac_chart = InlineImage(doc, hbar_buffer, width=Cm(15))
IGs = ", ".join(audit.get_selected_implementation_groups())
context = {
"audit": audit,
"date": now().strftime("%d/%m/%Y"),
"contributors": f"{authors}\n{reviewers}",
"req": aggregated,
"compliance_donut": res_donut,
"compliance_radar": chart_spider,
"drifts_per_domain": agg_drifts,
"chart_controls": ac_chart,
"p1_controls": p1_controls,
"ac_count": ac_total,
"igs": IGs,
}

return context
Binary file not shown.
43 changes: 42 additions & 1 deletion backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import zipfile
from datetime import date, datetime, timedelta
import time
from django.views.generic import detail
import pytz
from typing import Any, Tuple
from uuid import UUID
Expand All @@ -17,7 +18,13 @@
from pathlib import Path
import humanize

# from icecream import ic
from django.http import StreamingHttpResponse
from wsgiref.util import FileWrapper

import io

from docxtpl import DocxTemplate
from .generators import gen_audit_context

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
Expand Down Expand Up @@ -2145,6 +2152,40 @@ def compliance_assessment_csv(self, request, pk):
{"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN
)

@action(detail=True, methods=["get"])
def word_report(self, request, pk):
template_path = (
Path(settings.BASE_DIR)
/ "core"
/ "templates"
/ "core"
/ "audit_report_template.docx"
)
doc = DocxTemplate(template_path)
_framework = self.get_object().framework
tree = get_sorted_requirement_nodes(
RequirementNode.objects.filter(framework=_framework).all(),
RequirementAssessment.objects.filter(
compliance_assessment=self.get_object()
).all(),
_framework.max_score,
)
implementation_groups = self.get_object().selected_implementation_groups
filter_graph_by_implementation_groups(tree, implementation_groups)
context = gen_audit_context(pk, doc, tree)
doc.render(context)
buffer_doc = io.BytesIO()
doc.save(buffer_doc)
buffer_doc.seek(0)

response = StreamingHttpResponse(
FileWrapper(buffer_doc),
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
response["Content-Disposition"] = "attachment; filename=sales_report.docx"

return response

@action(detail=True, name="Get action plan PDF")
def action_plan_pdf(self, request, pk):
(object_ids_view, _, _) = RoleAssignment.get_accessible_object_ids(
Expand Down
Loading
Loading