diff --git a/framework/python/src/common/risk_profile.py b/framework/python/src/common/risk_profile.py
index f50dffdde..557ddef3c 100644
--- a/framework/python/src/common/risk_profile.py
+++ b/framework/python/src/common/risk_profile.py
@@ -20,10 +20,14 @@
from common import logger
import json
import os
+from jinja2 import Template
+from copy import deepcopy
PROFILES_PATH = 'local/risk_profiles'
LOGGER = logger.get_logger('risk_profile')
RESOURCES_DIR = 'resources/report'
+TEMPLATE_FILE = 'risk_report_template.html'
+TEMPLATE_STYLES = 'risk_report_styles.css'
# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))
@@ -43,6 +47,19 @@ class RiskProfile():
def __init__(self, profile_json=None, profile_format=None):
+ # Jinja template
+ with open(os.path.join(report_resource_dir, TEMPLATE_FILE),
+ 'r',
+ encoding='UTF-8'
+ ) as template_file:
+ self._template = Template(template_file.read())
+ with open(os.path.join(report_resource_dir,
+ TEMPLATE_STYLES),
+ 'r',
+ encoding='UTF-8'
+ ) as style_file:
+ self._template_styles = style_file.read()
+
if profile_json is None or profile_format is None:
return
@@ -55,9 +72,11 @@ def __init__(self, profile_json=None, profile_format=None):
self._device = None
self._profile_format = profile_format
+
self._validate(profile_json, profile_format)
self.update_risk(profile_format)
+
# Load a profile without modifying the created date
# but still validate the profile
def load(self, profile_json, profile_format):
@@ -295,382 +314,77 @@ def to_json(self, pretty=False):
def to_html(self, device):
self._device = device
-
- return f'''
-
-
- {self._generate_head()}
-
-
- {self._generate_header()}
- {self._generate_risk_banner()}
- {self._generate_risk_questions()}
- {self._generate_footer()}
-
-
-
- '''
-
- def _generate_head(self):
-
- return f'''
-
-
- '''
-
- content += self._generate_header()
-
- content += self._generate_table_head()
-
- return content
-
- def _generate_footer(self):
- footer = f'''
-
- '''
- return footer
-
- def _generate_css(self):
- return '''
- /* Set some global variables */
- :root {
- --header-height: .75in;
- --header-width: 8.5in;
- --header-pos-x: 0in;
- --header-pos-y: 0in;
- --page-width: 8.5in;
- }
-
- @font-face {
- font-family: 'Google Sans';
- font-style: normal;
- src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
- }
-
- @font-face {
- font-family: 'Roboto Mono';
- font-style: normal;
- src: url(https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
- }
-
- /* Define some common body formatting*/
- body {
- font-family: 'Google Sans', sans-serif;
- margin: 0px;
- padding: 0px;
- }
-
- /* Sets proper page size during print to pdf for weasyprint */
- @page {
- size: Letter;
- width: 8.5in;
- height: 11in;
- }
-
- .page {
- position: relative;
- margin: 0 20px;
- width: 8.5in;
- height: 11in;
- }
-
- /* Define the header related css elements*/
- .header {
- position: relative;
- }
-
- h1 {
- margin: 0 0 8px 0;
- font-size: 20px;
- font-weight: 400;
- }
-
- h2 {
- margin: 0px;
- font-size: 48px;
- font-weight: 700;
- }
-
- h3 {
- font-size: 24px;
- margin-bottom: 10px;
- margin-top: 15px;
- }
-
- h4 {
- font-size: 12px;
- font-weight: 500;
- color: #5F6368;
- margin-bottom: 0;
- margin-top: 0;
- }
-
- /* CSS for the footer */
- .footer {
- position: absolute;
- height: 30px;
- width: 8.5in;
- bottom: 0in;
- border-top: 1px solid #D3D3D3;
- }
-
- .footer-label {
- color: #3C4043;
- position: absolute;
- top: 5px;
- font-size: 12px;
- }
-
- @media print {
- @page {
- size: Letter;
- width: 8.5in;
- height: 11in;
- }
- }
-
- .risk-banner {
- min-height: 120px;
- padding: 5px 40px 0 40px;
- margin-top: 30px;
- }
-
- .risk-banner-limited {
- background-color: #E4F7FB;
- color: #007B83;
- }
-
- .risk-banner-high {
- background-color: #FCE8E6;
- color: #C5221F;
- }
-
- .risk-banner-title {
- text-transform: uppercase;
- font-weight: bold;
- }
-
- .risk-table {
- width: 100%;
- margin-top: 40px;
- text-align: left;
- color: #3C4043;
- font-size: 14px;
- }
-
- .risk-table-head {
- margin-bottom: 15px;
- }
-
- .risk-table-head-question {
- display: inline-block;
- margin-left: 70px;
- font-weight: bold;
- }
-
- .risk-table-head-answer {
- display: inline-block;
- margin-left: 325px;
- font-weight: bold;
- }
-
- .risk-table-row {
- margin-bottom: 8px;
- background-color: #F8F9FA;
- display: flex;
- align-items: stretch;
- overflow: hidden;
- }
-
- .risk-question-no {
- padding: 15px 20px;
- width: 10px;
- display: inline-block;
- vertical-align: top;
- position: relative;
- }
-
- .risk-question {
- padding: 15px 20px;
- display: inline-block;
- width: 350px;
- vertical-align: top;
- position: relative;
- height: 100%;
- }
-
- .risk-answer {
- background-color: #E8F0FE;
- padding: 15px 20px;
- display: inline-block;
- width: 340px;
- position: relative;
- height: 100%;
- }
-
- ul {
- margin-top: 0;
- }
-
- .risk-label{
- position: absolute;
- top: 0px;
- right: 0px;
- width: 52px;
- height: 16px;
- font-family: 'Google Sans', sans-serif;
- font-size: 8px;
- font-weight: 500;
- line-height: 16px;
- letter-spacing: 0.64px;
- text-align: center;
- font-weight: bold;
- border-radius: 3px;
- }
-
- .risk-label-high{
- background-color: #FCE8E6;
- color: #C5221F;
- }
-
- .risk-label-limited{
- width: 65px;
- background-color:#E4F7FB;
- color: #007B83;
- }
- '''
+ return pages
def to_pdf(self, device):
diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py
index 7fcac9f74..5931a2f5c 100644
--- a/framework/python/src/common/testreport.py
+++ b/framework/python/src/common/testreport.py
@@ -17,15 +17,19 @@
from weasyprint import HTML
from io import BytesIO
from common import util
-from common.statuses import TestResult, TestrunStatus
+from common.statuses import TestrunStatus
import base64
import os
from test_orc.test_case import TestCase
+from jinja2 import Template
+from collections import OrderedDict
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
RESOURCES_DIR = 'resources/report'
TESTS_FIRST_PAGE = 11
TESTS_PER_PAGE = 20
+TEST_REPORT_STYLES = 'test_report_styles.css'
+TEST_REPORT_TEMPLATE = 'test_report_template.html'
# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))
@@ -173,35 +177,62 @@ def to_pdf(self):
return pdf_bytes
def to_html(self):
- json_data = self.to_json()
- return f'''
-
-
- {self.generate_head()}
-
- {self.generate_body(json_data)}
-
-
- '''
-
- def generate_test_sections(self, json_data):
- results = json_data['tests']['results']
- sections = ''
- for result in results:
- sections += self.generate_test_section(result)
- return sections
-
- def generate_test_section(self, result):
- section_content = '
\n'
- for key, value in result.items():
- if value is not None: # Check if the value is not None
- # Replace underscores and capitalize
- formatted_key = key.replace('_', ' ').title()
- section_content += f'{formatted_key}: {value}
\n'
- section_content += '\n
\n'
- return section_content
-
- def generate_pages(self, json_data):
+
+ # Jinja template
+ with open(os.path.join(report_resource_dir, TEST_REPORT_TEMPLATE),
+ 'r',
+ encoding='UTF-8'
+ ) as template_file:
+ template = Template(template_file.read())
+ with open(os.path.join(report_resource_dir,
+ TEST_REPORT_STYLES),
+ 'r',
+ encoding='UTF-8'
+ ) as style_file:
+ styles = style_file.read()
+ with open(test_run_img_file, 'rb') as f:
+ logo = base64.b64encode(f.read()).decode('utf-8')
+ json_data=self.to_json()
+
+ # Convert the timestamp strings to datetime objects
+ start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S')
+ end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S')
+ # Calculate the duration
+ duration = end_time - start_time
+
+ successful_tests = 0
+ for test in json_data['tests']['results']:
+ if test['result'] != 'Error':
+ successful_tests += 1
+
+ steps_to_resolve = self._get_steps_to_resolve(json_data)
+
+ module_reports = self._get_module_pages()
+ pages_num = self._pages_num(json_data)
+ total_pages = pages_num + len(module_reports)
+ if len(steps_to_resolve) > 0:
+ total_pages += 1
+
+ return template.render(styles=styles,
+ logo=logo,
+ version=self._version,
+ json_data=json_data,
+ device=json_data['device'],
+ modules=self._device_modules(json_data['device']),
+ test_status=json_data['status'],
+ duration=str(duration),
+ successful_tests=successful_tests,
+ total_tests=self._total_tests,
+ test_results=json_data['tests']['results'],
+ steps_to_resolve=steps_to_resolve,
+ module_reports=module_reports,
+ pages_num=pages_num,
+ total_pages=total_pages,
+ tests_first_page=TESTS_FIRST_PAGE,
+ tests_per_page=TESTS_PER_PAGE,
+ )
+
+ def _pages_num(self, json_data):
# Calculate pages
test_count = len(json_data['tests']['results'])
@@ -209,136 +240,52 @@ def generate_pages(self, json_data):
# Multiple pages required
if test_count > TESTS_FIRST_PAGE:
# First page
- full_page = 1
+ pages = 1
- # Remaining tests
+ # Remaining testsgenerate
test_count -= TESTS_FIRST_PAGE
- full_page += (int)(test_count / TESTS_PER_PAGE)
- partial_page = 1 if test_count % TESTS_PER_PAGE > 0 else 0
+ pages += (int)(test_count / TESTS_PER_PAGE)
+ pages = pages + 1 if test_count % TESTS_PER_PAGE > 0 else pages
# 1 page required
- elif test_count == TESTS_FIRST_PAGE:
- full_page = 1
- partial_page = 0
- # Less than 1 page required
else:
- full_page = 0
- partial_page = 1
-
- num_pages = full_page + partial_page
+ pages = 1
- pages = ''
- for _ in range(num_pages):
- self._cur_page += 1
- pages += self.generate_results_page(json_data=json_data,
- page_num=self._cur_page)
return pages
- def generate_results_page(self, json_data, page_num):
- page = '
'
- page += self.generate_header(json_data, (page_num == 1))
- if page_num == 1:
- page += self.generate_summary(json_data)
- page += self.generate_results(json_data, page_num)
- page += self.generate_footer(page_num)
- page += '
'
- page += '
'
- return page
-
- def generate_module_page(self, json_data, module_report):
- self._cur_page += 1
- page = '
'
- page += self.generate_header(json_data, False)
- page += f'''
-
- {module_report}
-
'''
- page += self.generate_footer(self._cur_page)
- page += '
' # Page end
- page += '
'
- return page
-
- def generate_steps_to_resolve(self, json_data):
-
- steps_so_far = 0
+ def _device_modules(self, device):
+ sorted_modules = {}
+
+ if 'test_modules' in device:
+
+ for test_module in device['test_modules']:
+ if 'enabled' in device['test_modules'][test_module]:
+ sorted_modules[
+ util.get_module_display_name(test_module)] = device['test_modules'][
+ test_module]['enabled']
+
+ # Sort the modules by enabled first
+ sorted_modules = OrderedDict(sorted(sorted_modules.items(),
+ key=lambda x:x[1],
+ reverse=True)
+ )
+ return sorted_modules
+
+ def _get_steps_to_resolve(self, json_data):
tests_with_recommendations = []
- index = 1
# Collect all tests with recommendations
for test in json_data['tests']['results']:
if 'recommendations' in test:
tests_with_recommendations.append(test)
- # Check if test has recommendations
- if len(tests_with_recommendations) == 0:
- return ''
-
- # Start new page
- self._cur_page += 1
- page = '
'
- page += self.generate_header(json_data, False)
-
- # Add title
- page += '
Steps to Resolve
'
-
- for test in tests_with_recommendations:
-
- # Generate new page
- if steps_so_far == 4 and (
- len(tests_with_recommendations) - (index-1) > 0):
-
- # Reset steps counter
- steps_so_far = 0
-
- # Render footer
- page += self.generate_footer(self._cur_page)
- page += '' # Page end
- page += '
'
-
- # Render new header
- self._cur_page += 1
- page += '
'
- page += self.generate_header(json_data, False)
-
- # Render test recommendations
- page += f'''
-
-
-
{index}.
-
- Name
{test["name"]}
-
-
- Description
{test["description"]}
-
-
-
- Steps to resolve
- '''
-
- step_number = 1
- for recommendation in test['recommendations']:
- page += f'''
-
{
- step_number}. {recommendation}'''
- step_number += 1
-
- page += '
'
-
- index += 1
- steps_so_far += 1
-
- # Render final footer
- page += self.generate_footer(self._cur_page)
- page += '
' # Page end
- page += '
'
-
- return page
-
- def generate_module_pages(self, json_data):
- pages = ''
+ return tests_with_recommendations
+
+ def _get_module_pages(self):
content_max_size = 913
+ reports = []
+
for module_reports in self._module_reports:
# ToDo: Figure out how to make this dynamic
# Padding values from CSS
@@ -392,8 +339,7 @@ def generate_module_pages(self, json_data):
# If in the middle of a table, close the table
if data_rows_active:
page_content += ''
- page = self.generate_module_page(json_data, page_content)
- pages += page + '\n'
+ reports.append(page_content)
content_size = 0
# If in the middle of a data table, restart
# it for the rest of the rows
@@ -401,727 +347,5 @@ def generate_module_pages(self, json_data):
if data_rows_active else '')
page_content += line + '\n'
if len(page_content) > 0:
- page = self.generate_module_page(json_data, page_content)
- pages += page + '\n'
- return pages
-
- def generate_body(self, json_data):
- self._num_pages = 0
- self._cur_page = 0
- body = f'''
-
- {self.generate_pages(json_data)}
- {self.generate_steps_to_resolve(json_data)}
- {self.generate_module_pages(json_data)}
-
- '''
- # Set the max pages after all pages have been generated
- return body.replace('MAX_PAGE', str(self._cur_page))
-
- def generate_footer(self, page_num):
- footer = f'''
-
- '''
- return footer
-
- def generate_results(self, json_data, page_num):
-
- successful_tests = 0
- for test in json_data['tests']['results']:
- if test['result'] != TestResult.ERROR:
- successful_tests += 1
-
- result_list = f'''
-
-
Results List ({successful_tests}/{self._total_tests})
-
-
-
-
-
'''
- if page_num == 1:
- start = 0
- elif page_num == 2:
- start = TESTS_FIRST_PAGE
- else:
- start = (page_num - 2) * TESTS_PER_PAGE + TESTS_FIRST_PAGE
- results_on_page = TESTS_FIRST_PAGE if page_num == 1 else TESTS_PER_PAGE
- result_end = min(start + results_on_page,
- len(json_data['tests']['results']))
- for ix in range(result_end - start):
- result = json_data['tests']['results'][ix + start]
- result_list += self.generate_result(result)
- result_list += '
'
- return result_list
-
- def generate_result(self, result):
- if result['result'] == TestResult.NON_COMPLIANT:
- result_class = 'result-test-result-non-compliant'
- elif result['result'] == TestResult.COMPLIANT:
- result_class = 'result-test-result-compliant'
- elif result['result'] == TestResult.ERROR:
- result_class = 'result-test-result-error'
- elif result['result'] == TestResult.FEATURE_NOT_DETECTED:
- result_class = 'result-test-result-feature-not-detected'
- elif result['result'] == TestResult.INFORMATIONAL:
- result_class = 'result-test-result-informational'
- else:
- result_class = 'result-test-result-skipped'
-
- result_html = f'''
-
-
{result['name']}
-
{result['description']}
-
{result['result']}
-
- '''
- return result_html
-
- def generate_header(self, json_data, first_page):
- with open(test_run_img_file, 'rb') as f:
- tr_img_b64 = base64.b64encode(f.read()).decode('utf-8')
- header = ''
-
- if first_page:
- header += f'''
-