Pormenjen naicn rendovanja podatak iz template, popunjeni ceo dokument

This commit is contained in:
2025-04-18 16:29:07 +02:00
parent fc29671331
commit 31d679d9cf
7 changed files with 753 additions and 142 deletions

View 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
View 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

View File

@@ -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>

View 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)

View File

@@ -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'))

View File

@@ -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})