Pormenjen naicn rendovanja podatak iz template, popunjeni ceo dokument
This commit is contained in:
87
backend/core/processors.py
Normal file
87
backend/core/processors.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.template import Template, Context
|
||||
import re
|
||||
|
||||
|
||||
def render_universal_segment(segment, context_data):
|
||||
segment_type = segment.get('segment_type', 'unknown')
|
||||
raw_content = segment.get('content')
|
||||
|
||||
if raw_content is None:
|
||||
content = []
|
||||
elif isinstance(raw_content, dict):
|
||||
content = [raw_content]
|
||||
elif isinstance(raw_content, list):
|
||||
content = raw_content
|
||||
else:
|
||||
content = [raw_content]
|
||||
|
||||
rendered = []
|
||||
context = Context(context_data)
|
||||
|
||||
for item in content:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
title = Template(item.get('title', '')).render(context)
|
||||
subtitle = Template(item.get('subtitle', '')).render(context)
|
||||
description = Template(item.get('description', '')).render(context)
|
||||
|
||||
if title:
|
||||
rendered.append(f'<h2 style="color: #2c3e50; margin-top: 30px;">{title}</h2>')
|
||||
|
||||
if subtitle:
|
||||
rendered.append(f'<h3 style="color: #34495e; margin-top: 20px;">{subtitle}</h3>')
|
||||
|
||||
if description:
|
||||
processed_desc = []
|
||||
in_list = False
|
||||
for line in description.split('\n'):
|
||||
line = line.strip()
|
||||
if re.match(r'^[-•*]\s', line):
|
||||
if not in_list:
|
||||
processed_desc.append('<ul style="list-style-type: disc; margin-left: 20px;">')
|
||||
in_list = True
|
||||
processed_desc.append(f'<li>{line[2:].strip()}</li>')
|
||||
else:
|
||||
if in_list:
|
||||
processed_desc.append('</ul>')
|
||||
in_list = False
|
||||
if line:
|
||||
processed_desc.append(f'<p style="margin: 10px 0; line-height: 1.6;">{line}</p>')
|
||||
if in_list:
|
||||
processed_desc.append('</ul>')
|
||||
rendered.append('\n'.join(processed_desc))
|
||||
|
||||
if 'headers' in item and 'rows' in item:
|
||||
table_html = ['<table class="report-table" style="width: 100%; border-collapse: collapse; margin: 20px 0;">']
|
||||
table_html.append('<thead><tr>')
|
||||
for header in item['headers']:
|
||||
table_html.append(f'<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">{Template(header).render(context)}</th>')
|
||||
table_html.append('</tr></thead><tbody>')
|
||||
|
||||
for row in item['rows']:
|
||||
table_html.append('<tr>')
|
||||
for cell in row:
|
||||
cell_content = Template(cell).render(context) if isinstance(cell, str) else ', '.join([Template(str(c)).render(context) for c in cell])
|
||||
table_html.append(f'<td style="border: 1px solid #ddd; padding: 8px;">{cell_content}</td>')
|
||||
table_html.append('</tr>')
|
||||
table_html.append('</tbody></table>')
|
||||
rendered.append('\n'.join(table_html))
|
||||
|
||||
if 'image' in item:
|
||||
image_url = Template(item['image']).render(context)
|
||||
rendered.append(f'<img src="{image_url}" alt="{title}" style="max-width: 100%; height: auto; margin: 20px 0;">')
|
||||
|
||||
if 'html' in segment:
|
||||
html_template = Template(segment['html'])
|
||||
rendered_html = html_template.render(context)
|
||||
rendered.append(rendered_html)
|
||||
|
||||
return '\n'.join(rendered)
|
||||
|
||||
def render_template(template_segments, context_data):
|
||||
final_output = []
|
||||
for segment in template_segments:
|
||||
segment_html = render_universal_segment(segment, context_data)
|
||||
final_output.append(f'<div class="segment {segment.get("segment_type", "")}">{segment_html}</div>')
|
||||
return '\n'.join(final_output)
|
||||
108
backend/core/tables.py
Normal file
108
backend/core/tables.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from backend.core.models import DocumentRiskControl
|
||||
from backend.core.utils import calculate_aggregate_likelihood, calculate_aggregate_weight, map_weight_to_impact_likelihood
|
||||
|
||||
|
||||
def risk_matrix_table():
|
||||
likelihood_labels = [
|
||||
"Certain (90-100%)",
|
||||
"Almost Certain (80-89%)",
|
||||
"Very Probable (70-79%)",
|
||||
"Probable (60-69%)",
|
||||
"Highly Likely (50-59%)",
|
||||
"Likely (40-49%)",
|
||||
"Occasional (30-39%)",
|
||||
"Possible (20-29%)",
|
||||
"Unlikely (10-19%)",
|
||||
"Rare (0-9%)"
|
||||
]
|
||||
|
||||
impact_labels = [
|
||||
"Insignificant",
|
||||
"Minor",
|
||||
"Moderate",
|
||||
"Major",
|
||||
"Severe",
|
||||
"Catastrophic",
|
||||
"Critical",
|
||||
"Extreme",
|
||||
"Disastrous",
|
||||
"Unrecoverable"
|
||||
]
|
||||
|
||||
color_mapping = {
|
||||
"Very Low": "green",
|
||||
"Low": "lightgreen",
|
||||
"Medium": "yellow",
|
||||
"High": "orange",
|
||||
"Critical": "red"
|
||||
}
|
||||
|
||||
table_matrix_risk = [["Impact ↓ / Likelihood →"] + impact_labels]
|
||||
|
||||
for likelihood_index, likelihood_label in enumerate(likelihood_labels, start=1):
|
||||
reversed_index = 11 - likelihood_index
|
||||
row = [likelihood_label]
|
||||
for impact_index in range(1, 11):
|
||||
score = reversed_index * impact_index
|
||||
if score <= 20:
|
||||
label = "Very Low"
|
||||
elif score <= 40:
|
||||
label = "Low"
|
||||
elif score <= 60:
|
||||
label = "Medium"
|
||||
elif score <= 80:
|
||||
label = "High"
|
||||
else:
|
||||
label = "Critical"
|
||||
color_class = color_mapping[label]
|
||||
row.append((score, label, color_class))
|
||||
table_matrix_risk.append(row)
|
||||
|
||||
return table_matrix_risk
|
||||
|
||||
def get_risk_table(document):
|
||||
risks = (
|
||||
DocumentRiskControl.objects
|
||||
.filter(document=document)
|
||||
.values('risk', 'risk__risk_name')
|
||||
.distinct()
|
||||
)
|
||||
|
||||
risks_with_controls = []
|
||||
|
||||
for risk_entry in risks:
|
||||
risk = {
|
||||
'id': risk_entry['risk'],
|
||||
'name': risk_entry['risk__risk_name']
|
||||
}
|
||||
|
||||
controls = (
|
||||
DocumentRiskControl.objects
|
||||
.filter(document=document, risk_id=risk['id'])
|
||||
.values('control', 'control__name', 'weight', 'likelihood')
|
||||
.distinct()
|
||||
)
|
||||
max_weight = 10*10
|
||||
total_weight = calculate_aggregate_weight(controls)
|
||||
total_likelihood = calculate_aggregate_likelihood(controls)
|
||||
impact, likelihood = map_weight_to_impact_likelihood(total_weight, total_likelihood, max_weight)
|
||||
r_impact = round(impact)
|
||||
r_likelihood = round(likelihood)
|
||||
residua_impact = r_impact - 1 if r_impact > 2 else r_impact
|
||||
residual_likelihood = r_likelihood - 1 if r_likelihood > 2 else r_likelihood
|
||||
risks_with_controls.append({
|
||||
'risk': risk,
|
||||
'controls': list(controls),
|
||||
'total_weight': total_weight,
|
||||
'impact': impact,
|
||||
'likelihood': likelihood,
|
||||
'r_impact': r_impact,
|
||||
'r_likelihood': r_likelihood,
|
||||
'risk_score': r_impact * r_likelihood,
|
||||
'residual_impact': residua_impact,
|
||||
'residual_likelihood': residual_likelihood,
|
||||
'residual_risk_score': residua_impact * residual_likelihood,
|
||||
})
|
||||
risks_with_controls.sort(key=lambda x: x['risk_score'], reverse=True)
|
||||
|
||||
return risks_with_controls
|
||||
@@ -1,66 +1,159 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document PDF</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm;
|
||||
}
|
||||
|
||||
<div class="document-container">
|
||||
{% if error %}
|
||||
<p style="color: red;">{{ error }}</p>
|
||||
{% endif %}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media print {
|
||||
table {
|
||||
page-break-inside: avoid; /* Prevent table from breaking across pages */
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid; /* Prevent table rows from breaking across pages */
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group; /* Ensure table headers repeat on each page */
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-footer-group; /* Ensure table footers repeat on each page */
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always; /* Force a page break before this element */
|
||||
}
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 12pt;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
<div>
|
||||
{{ rendered_html|safe }}
|
||||
.document-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.document-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.document-meta {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.document-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.document-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.document-subtitle {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.document-h1 {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.document-h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.document-h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.document-quote {
|
||||
border-left: 4px solid #ccc;
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.document-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.green { background-color: green; color: white; }
|
||||
.lightgreen { background-color: lightgreen; }
|
||||
.yellow { background-color: yellow; }
|
||||
.orange { background-color: orange; }
|
||||
.red { background-color: red; color: white; }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
word-wrap: break-word;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 4px 6px;
|
||||
font-size: 10pt;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
caption {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cmmi thead th {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-after: always;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document-container">
|
||||
{% if error %}
|
||||
<p style="color: red;">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{{ rendered_html|safe }}
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.document-container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.document-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.document-meta {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.document-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.document-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.document-subtitle {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.document-h1 {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.document-h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.document-h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.document-quote {
|
||||
border-left: 4px solid #ccc;
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.document-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
64
backend/core/tests/test_processors.py
Normal file
64
backend/core/tests/test_processors.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import unittest
|
||||
from django.template import Context
|
||||
from ..processors import render_universal_segment, render_template
|
||||
|
||||
class TestProcessors(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.context_data = {
|
||||
"document": {
|
||||
"organization": {"name": "Example Corp"},
|
||||
"created_at": "2025-04-08",
|
||||
"third_party_vendor_access": 50
|
||||
}
|
||||
}
|
||||
self.template_segments = [
|
||||
{
|
||||
"segment_type": "example_segment",
|
||||
"content": [
|
||||
{
|
||||
"title": "Main Title",
|
||||
"subtitle": "Subtitle 1",
|
||||
"description": "This is the first description.\n- Bullet 1\n- Bullet 2"
|
||||
},
|
||||
{
|
||||
"subtitle": "Subtitle 2",
|
||||
"description": "This is the second description.\nAnother paragraph here."
|
||||
}
|
||||
],
|
||||
"html": "<div><p>Custom HTML content with {{ document.organization.name }}</p></div>"
|
||||
}
|
||||
]
|
||||
|
||||
def test_render_universal_segment(self):
|
||||
segment = self.template_segments[0]
|
||||
result = render_universal_segment(segment, self.context_data)
|
||||
self.assertIn("<h2 style=", result)
|
||||
self.assertIn("<h3 style=", result)
|
||||
self.assertIn("<ul style=", result)
|
||||
self.assertIn("<div><p>Custom HTML content with Example Corp</p></div>", result)
|
||||
|
||||
def test_render_template(self):
|
||||
result = render_template(self.template_segments, self.context_data)
|
||||
self.assertIn('<div class="segment example_segment">', result)
|
||||
self.assertIn("Main Title", result)
|
||||
self.assertIn("Subtitle 1", result)
|
||||
self.assertIn("Custom HTML content with Example Corp", result)
|
||||
|
||||
def test_empty_segment(self):
|
||||
segment = {"segment_type": "empty_segment", "content": []}
|
||||
result = render_universal_segment(segment, self.context_data)
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_missing_html(self):
|
||||
segment = {
|
||||
"segment_type": "no_html_segment",
|
||||
"content": [{"title": "Title Only"}]
|
||||
}
|
||||
result = render_universal_segment(segment, self.context_data)
|
||||
self.assertIn("Title Only", result)
|
||||
self.assertNotIn("<div>", result)
|
||||
|
||||
def test_missing_content(self):
|
||||
segment = {"segment_type": "html_only", "html": "<p>Only HTML</p>"}
|
||||
result = render_universal_segment(segment, self.context_data)
|
||||
self.assertIn("<p>Only HTML</p>", result)
|
||||
@@ -53,13 +53,16 @@ class DocumentViewTest(TestCase):
|
||||
|
||||
template_content = """
|
||||
- segment_type: "h1"
|
||||
content: "{{ document.organization.name }} - Risk Report"
|
||||
content:
|
||||
title: "{{ document.organization.name }} - Risk Report"
|
||||
- segment_type: "p"
|
||||
content: "Created at: {{ document.created_at|date:'Y-m-d' }}"
|
||||
content:
|
||||
descripton: |
|
||||
"Created at: {{ document.created_at|date:'Y-m-d' }}"
|
||||
- segment_type: "h2"
|
||||
content: "Top 10 Risk Identified"
|
||||
- segment_type: "table"
|
||||
content: |
|
||||
html: |
|
||||
<table>
|
||||
<tr>
|
||||
<th>Risk ID</th>
|
||||
@@ -81,11 +84,13 @@ class DocumentViewTest(TestCase):
|
||||
{% endfor %}
|
||||
</table>
|
||||
- segment_type: "image"
|
||||
content: "data:image/png;base64,{{ graph }}"
|
||||
content:
|
||||
image: "data:image/png;base64,{{ graph }}"
|
||||
- segment_type: "h2"
|
||||
content: "Risks with Controls"
|
||||
content:
|
||||
title: "Risks with Controls"
|
||||
- segment_type: "body"
|
||||
content: |
|
||||
html: |
|
||||
{% for item in risks_with_controls %}
|
||||
<div class="risk">
|
||||
<h3>Risk: {{ item.risk.name }}</h3>
|
||||
@@ -107,7 +112,8 @@ class DocumentViewTest(TestCase):
|
||||
response = self.client.get(reverse('core:document', kwargs={'document_id': self.document.id}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'document.html')
|
||||
self.assertContains(response, self.organization.name)
|
||||
self.assertContains(response, self.organization.name)
|
||||
self.assertContains(response, "Risk Report")
|
||||
|
||||
def test_index_view(self):
|
||||
response = self.client.get(reverse('core:index'))
|
||||
|
||||
@@ -6,10 +6,11 @@ from .forms import OrganizationForm
|
||||
from .models import Organization,Document, DocumentTemplate,DocumentRiskControl,Risk
|
||||
from backend.accounts.utils import send_confirmation_email, send_document_email
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.template import Template, Context
|
||||
from .utils import generate_pdf, map_weight_to_impact_likelihood, calculate_aggregate_weight, calculate_aggregate_likelihood, generate_risk_graph
|
||||
from .tables import risk_matrix_table ,get_risk_table
|
||||
from django.conf import settings
|
||||
site_domain = settings.SITE_DOMAIN
|
||||
from .processors import render_template
|
||||
|
||||
|
||||
|
||||
@@ -49,40 +50,8 @@ def thankyou(request):
|
||||
|
||||
def document(request, document_id):
|
||||
document = get_object_or_404(Document, id=document_id)
|
||||
risks = (
|
||||
DocumentRiskControl.objects
|
||||
.filter(document=document)
|
||||
.values('risk', 'risk__risk_name')
|
||||
.distinct()
|
||||
)
|
||||
|
||||
risks_with_controls = []
|
||||
|
||||
for risk_entry in risks:
|
||||
risk = {
|
||||
'id': risk_entry['risk'],
|
||||
'name': risk_entry['risk__risk_name']
|
||||
}
|
||||
|
||||
controls = (
|
||||
DocumentRiskControl.objects
|
||||
.filter(document=document, risk_id=risk['id'])
|
||||
.values('control', 'control__name', 'weight', 'likelihood')
|
||||
.distinct()
|
||||
)
|
||||
max_weight = 10*10
|
||||
total_weight = calculate_aggregate_weight(controls)
|
||||
total_likelihood = calculate_aggregate_likelihood(controls)
|
||||
impact, likelihood = map_weight_to_impact_likelihood(total_weight, total_likelihood, max_weight)
|
||||
risks_with_controls.append({
|
||||
'risk': risk,
|
||||
'controls': list(controls),
|
||||
'total_weight': total_weight,
|
||||
'impact': impact,
|
||||
'likelihood': likelihood,
|
||||
'risk_score': (round(impact) * round(likelihood))
|
||||
})
|
||||
|
||||
risks_with_controls = get_risk_table(document)
|
||||
table_risk_matrix = risk_matrix_table()
|
||||
graph_base64 = generate_risk_graph(risks_with_controls)
|
||||
|
||||
template_obj = get_object_or_404(DocumentTemplate, name="Default Template")
|
||||
@@ -95,28 +64,10 @@ def document(request, document_id):
|
||||
context = {
|
||||
'document': document,
|
||||
'risks_with_controls': risks_with_controls,
|
||||
'graph': graph_base64,
|
||||
'graph': graph_base64,
|
||||
'table_risk_matrix': table_risk_matrix,
|
||||
}
|
||||
rendered_content = ""
|
||||
for segment in template_segments:
|
||||
content = segment.get('content', '')
|
||||
segment_type = segment.get('segment_type', '')
|
||||
|
||||
django_template = Template(content)
|
||||
processed_content = django_template.render(Context(context))
|
||||
|
||||
if segment_type == "h1":
|
||||
rendered_content += f"<h1>{processed_content}</h1>\n"
|
||||
elif segment_type == "h2":
|
||||
rendered_content += f"<h2>{processed_content}</h2>\n"
|
||||
elif segment_type == "h3":
|
||||
rendered_content += f"<h3>{processed_content}</h3>\n"
|
||||
elif segment_type == "p":
|
||||
rendered_content += f"<p>{processed_content}</p>\n"
|
||||
elif segment_type == "image":
|
||||
rendered_content += f'<img src="{processed_content}" alt="Risk Graph" style="max-width:100%; height:auto;">\n'
|
||||
else:
|
||||
rendered_content += processed_content
|
||||
rendered_content = render_template(template_segments, context)
|
||||
return render(request, 'document.html', {'rendered_html': rendered_content})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user