Browse Source

Dashboard generator

aguryev 3 years ago
parent
commit
5302df1636

+ 2 - 0
.gitignore

@@ -18,3 +18,5 @@ baangt_*.xlsx
 /baangt.egg-info/
 /venv3_6/
 /tests/proxies.csv
+
+baangt/reports/*.html

+ 8 - 1
baangt/base/CliAndInteractive.py

@@ -7,6 +7,8 @@ from PyQt5 import QtWidgets
 # from baangt.ui.pyqt.uimain import MainWindow
 from baangt.ui.pyqt.uimain import MainController
 from baangt.base.RuntimeStatistics import Statistic
+from baangt.reports import Reports
+
 def print_args():
     print("""
 Call: python baangtIA.py --parameters 
@@ -30,7 +32,8 @@ def args_read(l_search_parameter):
         opts, args = getopt.getopt(l_args, "", ["run=",
                                                 "globals=",
                                                 "reloadDrivers=",
-                                                "gui="
+                                                "gui=",
+                                                "reports=",
                                                 ])
     except getopt.GetoptError as err_det:
         print("Error in reading parameters:" + str(err_det))
@@ -78,6 +81,10 @@ def run():
         lDriver.downloadDriver(GC.BROWSER_FIREFOX)
         lDriver.downloadDriver(GC.BROWSER_CHROME)
         print("Latest versions of drivers for Firefox and Chrome were downloaded")
+    elif args_read("reports"):
+        r = Reports()
+        r.show_dashboard()
+
     else:
         app = QtWidgets.QApplication(sys.argv)
         controller = MainController()

+ 10 - 1
baangt/base/DataBaseORM.py

@@ -44,6 +44,15 @@ class TestrunLog(base):
 	globalVars = relationship('GlobalAttribute')
 	testcase_sequences = relationship('TestCaseSequenceLog')
 
+	@property
+	def recordCount(self):
+		return self.statusOk + self.statusPaused + self.statusFailed
+
+	@property
+	def duration(self):
+		return (self.endTime - self.startTime).seconds
+	
+
 	def __str__(self):
 		return str(uuid.UUID(bytes=self.id))
 
@@ -52,7 +61,7 @@ class TestrunLog(base):
 			'id': str(self),
 			'Name': self.testrunName,
 			'Summary': {
-				'TestRecords': sum((self.statusOk, self.statusPaused, self.statusFailed)),
+				'TestRecords': self.recordCount,
 				'Successful': self.statusOk,
 				'Paused': self.statusPaused,
 				'Error': self.statusFailed,

+ 1 - 23
baangt/base/ExportResults/ExportResults.py

@@ -112,29 +112,7 @@ class ExportResults:
             stage = value.get(GC.EXECUTION_STAGE)
 
         return stage
-
-    # -- API support --
-    def getSummary(self):
-        #
-        # returns records status as dict
-        #
-        summary = {'Testrecords': len(self.dataRecords)}
-        summary['Successful'] = len([x for x in self.dataRecords.values()
-                                     if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_SUCCESS])
-        summary['Paused'] = len([x for x in self.dataRecords.values()
-                                 if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_WAITING])
-        summary['Error'] = len([x for x in self.dataRecords.values()
-                                if x[GC.TESTCASESTATUS] == GC.TESTCASESTATUS_ERROR])
-        # logfile
-        summary['Logfile'] = logger.handlers[1].baseFilename
-        # timing
-        timing: Timing = self.testRunInstance.timing
-        summary['Starttime'], summary['Endtime'], summary['Duration'] = timing.returnTimeSegment(GC.TIMING_TESTRUN)
-        summary['Globals'] = {key: value for key, value in self.testRunInstance.globalSettings.items()}
-
-        return summary
-
-    # -- END of API support --
+        
 
     def export2CSV(self):
         """

+ 212 - 0
baangt/reports.py

@@ -0,0 +1,212 @@
+from datetime import datetime
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy import desc
+from baangt.base.DataBaseORM import engine, TestrunLog
+from jinja2 import Environment, FileSystemLoader
+import json
+import os
+import webbrowser
+
+
+class Reports:
+
+	path_to_reports = 'reports'
+
+
+	def __init__(self):
+		self.created = datetime.now()
+		
+	@property
+	def data(self):
+		#
+		# fetches TestrunLogs data
+		#
+
+		db = sessionmaker(bind=engine)()
+
+		data = {}
+
+		# get Testrun names
+		testrun_names = [item.testrunName for 
+			item in db.query(TestrunLog).order_by(TestrunLog.testrunName).group_by(TestrunLog.testrunName).all()]
+
+		for name in testrun_names:
+			# build items by testruns
+			logs = db.query(TestrunLog).order_by(desc(TestrunLog.startTime)).filter_by(testrunName=name).all()[-10:]
+			empty = max(0, 10-len(logs))
+
+			# build charts
+			charts = {}
+
+			charts['figures'] = {
+				'records': logs[-1].recordCount,
+				'successful': logs[-1].statusOk,
+				'error': logs[-1].statusFailed,
+				'paused': logs[-1].statusPaused,
+			}
+
+			# status chart
+			charts['status'] = json.dumps({
+				'type': 'doughnut',
+				'data': {
+					'datasets': [{
+						'data': [
+							logs[-1].statusOk,
+							logs[-1].statusFailed,
+							logs[-1].statusPaused,
+						],
+						'backgroundColor': [
+							'#52ff52',
+							'#ff5252',
+							'#bdbdbd',
+						],
+					}],
+					'labels': [
+						'Passed',
+						'Failed',
+						'Paused'
+					],
+				},
+				'options': {
+					'legend': {
+						'display': False,
+					},
+				},
+			})
+
+			# results chart
+			charts['results'] = json.dumps({
+			'type': 'bar',
+				'data': {
+					'datasets': [
+						{
+							'data': [x.statusOk for x in logs] + [None]*empty,
+							'backgroundColor': '#52ff52',
+							'label': 'Passed',
+						},
+						{
+							'data': [x.statusFailed for x in logs] + [None]*empty,
+							'backgroundColor': '#ff5252',
+							'label': 'Failed',
+						},
+						{
+							'data': [x.statusPaused for x in logs] + [None]*empty,
+							'backgroundColor': '#bdbdbd',
+							'label': 'Paused',
+						},
+					],
+					'labels': [x.startTime.strftime('%Y-%m-%d %H:%M') for x in logs] + [None]*empty,
+				},
+				'options': {
+					'legend': {
+						'display': False,
+					},
+					'tooltips': {
+						'mode': 'index',
+						'intersect': False,
+					},
+					'scales': {
+						'xAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+							'stacked': True,
+						}],
+						'yAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+							'stacked': True,
+						}],
+					},
+				},
+			})
+
+			# duration chart
+			charts['duration'] = json.dumps({
+			'type': 'line',
+				'data': {
+					'datasets': [{
+						'data': [str(x.duration) for x in logs] + [None]*empty,
+						'fill': False,
+						'borderColor': '#cfd8dc',
+						'borderWidth': 1, 
+						'pointBackgroundColor': 'transparent',
+						'pointBorderColor': '#0277bd',
+						'pointRadius': 5,
+						'lineTension': 0,
+						'label': 'Duration',
+					}],
+					'labels': [x.startTime.strftime('%Y-%m-%d %H:%M') for x in logs] + [None]*empty,
+				},
+				'options': {
+					'legend': {
+						'display': False,
+					},
+					'tooltips': {
+						'mode': 'index',
+						'intersect': False,
+					},
+					'layout': {
+						'padding': {
+							'top': 10,
+							'right': 10,
+						},
+					},
+					'scales': {
+						'xAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+						}],
+						'yAxes': [{
+							'gridLines': {
+								'display': False,
+								'drawBorder': False,
+							},
+							'ticks': {
+								'display': False,
+							},
+						}],
+					},
+				},
+			})
+
+
+			data['-'.join(name.split('.'))] = charts
+
+		db.close()
+
+		return data
+
+
+	def show_dashboard(self):
+		#
+		# shows html dashboard
+		#
+
+		filename = os.path.join(os.path.dirname(__file__), self.path_to_reports, self.created.strftime('dashboard-%Y%m%d-%H%M%S.html'))
+		self.generate_dashboard(filename)
+		url = f'file://{filename}'
+		webbrowser.open(url, new=2)
+
+	def generate_dashboard(self, filename):
+		file_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), self.path_to_reports, 'templates'))
+		env = Environment(loader=file_loader)
+
+		template = env.get_template('dashboard.html')
+
+		with open(filename, 'w') as f:
+			f.write(template.render(data=self.data, created=self.created.strftime('%Y-%m-%d %H:%M:%S')))

File diff suppressed because it is too large
+ 10224 - 0
baangt/reports/css/bootstrap.css


+ 89 - 0
baangt/reports/css/styles.css

@@ -0,0 +1,89 @@
+body {
+  min-width: 420px;
+}
+
+.hovered-item:hover {
+  background-color: #f5f5f5;
+}
+
+/* chips */
+.chip {
+  display: inline-block;
+  padding: 0 10px;
+  height: 30px;
+  font-size: 16px;
+  /*line-height: 50px;*/
+  border-radius: 15px;
+  background-color: #f1f1f1;
+}
+
+.closebtn {
+  padding-left: 10px;
+  color: #888;
+  font-weight: bold;
+  float: right;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+/* input groups */
+.closebtn:hover {
+  color: #000;
+}
+
+.fixed-label {
+  width: 11rem;
+}
+
+.text-break {
+  word-wrap: break-word;
+}
+
+/* buttons */
+.btn-right {
+  width: 10rem;
+}
+
+/* dialog */
+.dialog {
+  justify-content: center !important;
+}
+
+
+/* widths */
+.w-20 {
+  width: 20%;
+}
+
+.w-40 {
+  width: 40%;
+}
+
+.w-60 {
+  width: 60%;
+}
+
+.w-80 {
+  width: 80%;
+}
+
+/* loading screen */
+#loading {
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  width: 100vw;
+  height: 150vh;
+  background-color: rgba(192, 192, 192, 0.8);
+}
+
+.spinner {
+  width: 3rem;
+  height: 3rem;
+}
+
+.h-80 {
+  height: 80vh;
+}

+ 32 - 0
baangt/reports/js/charts.js

@@ -0,0 +1,32 @@
+document.addEventListener('DOMContentLoaded', () => {
+    
+    document.querySelectorAll('.hovered-item').forEach(item => {
+        console.log(item.id);
+        // get chart data
+        const request = new XMLHttpRequest();
+        request.responseType = 'json';
+        request.open('POST', `/chart/${item.id}`);
+        request.onload = () => {
+            // customize legend
+            var duration = request.response['duration'];
+            duration['options']['tooltips']['callbacks'] = {
+                label: function(item, data) {
+                    var label = data.datasets[item.datasetIndex].label || '';
+                    if (label) {
+                        label += ': ';
+                    }
+                    label += item.value;
+                    label += ' sec';
+
+                    return label;
+                }
+            };
+            //console.log(results['options']['tooltips']);
+            // draw charts 
+            var statusChart = new Chart(item.querySelector('.statusChart'), request.response['status']);
+            var resultsChart = new Chart(item.querySelector('.resultsChart'), request.response['results']);
+            var durationChart = new Chart(item.querySelector('.durationChart'), duration);
+        };
+        request.send();
+    });
+});

+ 332 - 0
baangt/reports/js/testrun.js

@@ -0,0 +1,332 @@
+
+    // get item
+    function load_item(item_type, item_id) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/${item_id}`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // create new item
+    function new_item(item_type) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/new`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // edit item
+    function edit_item(item_type, item_id) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/${item_id}/edit`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // delete item
+    function set_delete_item(type_name, type, item_id) {
+        /* set data in delete modal */
+        document.querySelector('#deleteLabel').innerHTML = `Delete ${type_name} ID #${item_id}`;
+        const btns = document.querySelector('#deleteButtons');
+        btns.setAttribute('data-type', type);
+        btns.setAttribute('data-id', item_id);
+    }
+
+    function delete_item(e, cascade) {
+        /* delete item */
+        const request = new XMLHttpRequest();
+        if (cascade) {
+            // cascade delete: POST request
+            request.open('POST', `/${e.dataset['type']}/${e.dataset['id']}/delete`);
+        } else {
+            // single item delete: DELETE request
+            request.open('DELETE', `/${e.dataset['type']}/${e.dataset['id']}/delete`);
+        }
+        
+        request.onload = () => {
+            window.location.reload(true); 
+        }
+        request.send();
+    }
+
+    // push message
+    function push_message(msg, type) {
+        // create message
+        var alrt = document.createElement('div');
+        alrt.setAttribute('class', `alert alert-${type} alert-dismissible fade show`);
+        alrt.setAttribute('role', 'alert');
+        alrt.innerHTML = msg;
+        // create close button
+        var btn = document.createElement('button');
+        btn.setAttribute('type', 'button');
+        btn.setAttribute('class', 'close');
+        btn.setAttribute('data-dismiss', 'alert');
+        btn.setAttribute('aria-label', 'Close');
+        btn.innerHTML = `<span aria-hidden="true">&times;</span>`;
+        // add to main
+        alrt.appendChild(btn);
+        document.getElementById('flash_area').appendChild(alrt);
+    }
+
+    // add DataFile
+    function add_datafile() {
+        const message = document.getElementById('upload-message');
+        const modal = document.getElementById('uploadModal');
+        const files = document.getElementById('upload-file').files;
+        message.innerHTML = "";
+        if (files.length > 0) {
+            //console.log(files[0]);
+            const request = new XMLHttpRequest();
+            request.responseType = 'json';
+            request.open('POST', '/datafile/upload');
+            // get file
+            var datafile = new FormData();
+            datafile.append('datafile', files[0]);
+
+            request.onload = () => {
+                if (request.status === 200) {
+                    // add option to datalist
+                    var datalist_option = document.createElement('option');
+                    datalist_option.setAttribute('data-id', request.response['id']);
+                    datalist_option.innerText = request.response['name'];
+                    document.getElementById('datafilesOpt').appendChild(datalist_option);
+                    // add option to multi-select
+                    var select_option = document.createElement('option');
+                    select_option.innerText = request.response['name'];
+                    select_option.setAttribute('value', request.response['id']);
+                    document.getElementById('datafiles').appendChild(select_option);
+                    // message
+                    message.innerHTML = '<span class="text-success">DataFile successfully uploaded</span>'
+                    document.getElementById('upload-label').innerText = 'Choose a file';
+                } else {
+                    // push error message
+                    for (var key in request.response) {
+                        message.innerHTML = `<span class="text-danger">${key.toUpperCase()}: ${request.response[key]}</span>`
+                    }
+                }
+            }
+            request.send(datafile);
+        } else {
+            message.innerHTML = '<span class="text-success">Please select a DataFile</span>';
+        }
+    }
+
+    function add_datafile_new() {
+        const datafiles = document.getElementById('datafiles');
+        const files = document.getElementById('datafile-select').files;
+        const f = document.querySelector('form');
+        var datafile_list = new FormData();
+        if (files.length > 0) {
+            //datafiles.files.push.apply(files[0]);
+            datafile_list.append('file', files[0]);
+            //console.log(datafile_list.keys().length);
+            //console.log(datafile_list.keys().length);
+        } else {
+            push_message('No DataFile selected', 'warning');
+        }
+    }
+
+    function upload_datafile() {
+        //console.log('upload');
+        const datafiles = document.getElementById('datafiles');
+        //document.uploadForm.submit();
+    }
+
+    function create_chip(name, text, id) {
+        var chip_area = document.getElementById(name);
+        var new_chip = document.createElement('div');
+        var onclick_action;
+        new_chip.setAttribute('class', 'chip mr-1');
+        new_chip.setAttribute('data-id', id);
+        if (id >= 0) {        
+            onclick_action = `delete_chip(this.parentElement, '${name}')`;
+        } else {
+            onclick_action = 'this.parentElement.remove()';            
+        }
+
+        new_chip.innerHTML = `
+            <small>${text}</small>
+            <span class="closebtn" onclick="${onclick_action}">&times;</span>
+            `;
+        chip_area.appendChild(new_chip);
+    }
+
+    // select multiple with chips
+    function add_chip(e, name) {
+        // check if option exists
+        const text = e.value;
+        for (var i = 0; i < e.list.childElementCount; i++) {
+            //const opt_text = e.list.children[i].text.toUpperCase();
+            if (text === e.list.children[i].text) {
+                // create chip
+                //create_chip(`chips_${name}`, e.list.children[i].text, i);
+                
+                var chip_area = document.getElementById(`chips_${name}`);
+                var new_chip = document.createElement('div');
+                new_chip.setAttribute('class', 'chip mr-1');
+                new_chip.setAttribute('data-id', e.list.children[i].dataset['id']);
+                new_chip.innerHTML = `
+                    <small>${e.list.children[i].text}</small>
+                    <span class="closebtn" onclick="delete_chip(this.parentElement, '${name}')">&times;</span>
+                    `;
+                chip_area.appendChild(new_chip);
+                
+                // remove selected option from list
+                e.value = null;
+                e.list.children[i].disabled = true;
+                break;
+            }
+        }
+    }
+
+    function delete_chip(e, name) {
+        var list = document.getElementById(`${name}Opt`);
+        for (var i = 0; i < list.childElementCount; i++) {
+            if (e.dataset['id'] === list.children[i].dataset['id']) {
+                list.children[i].disabled = false;
+                e.parentElement.removeChild(e);
+                break;
+            }
+        }
+        
+    }
+
+    function get_chips() {
+        document.querySelectorAll('datalist').forEach(list => {
+            // create multyselect field
+            const name = list.id.substring(0, list.id.length - 3);
+            const selector = document.getElementById(name);
+            const chips_area = document.getElementById(`chips_${name}`);
+            //console.log(name);
+            chips_area.querySelectorAll('div').forEach(chip => {
+                //console.log(chip.dataset['id']);
+                //value = chip.dataset['id'];
+                for (var i = 0; i < selector.length; i++) {
+                    if (chip.dataset['id'] === selector.options[i].value) {
+                        selector.options[i].selected = true;
+                    }
+                }
+            });
+        });
+
+        return false;
+    }
+
+    function filter_options(e) {
+        const text = e.value.toUpperCase();
+        for (var i = 0; i < e.list.childElementCount; i++) {
+            const opt_text = e.list.children[i].text.toUpperCase();
+            if (!opt_text.includes(text)) {
+                e.list.children[i].style.display = opt_text ? 'list-item' : 'none';
+            }
+
+
+        }
+    }
+
+    function filter_items(e) {
+        const text = e.value.toUpperCase();
+        document.querySelectorAll('.testrun-item').forEach(item => {
+            var display = false;
+            item.querySelectorAll('.filtered').forEach(property => {
+                const value = property.innerHTML.toUpperCase();
+                if (value.includes(text)) {
+                    display = true;
+                }
+            });
+
+            item.style.display = display ? '' : 'none';
+
+        });
+    }
+
+
+    function get_file(e) {
+        // get filename
+        const filename = e.files[0].name;
+        const label = e.parentElement.querySelector('label');
+        label.innerText = filename;
+    }
+
+    function set_export_id(item_id) {
+        // set item_id in modal
+        const btn = document.querySelector('#exportButton')
+        btn.setAttribute('data-id', item_id);
+        btn.style.display = '';
+        // set modal body
+        document.querySelector('#exportRequest').style.display = '';
+        document.querySelector('#exportResponse').style.display = 'none';
+    }
+
+    function export_item(e) {
+        /*
+        exports a testrun
+        */
+        // get export format
+        const exportFormat = document.querySelector('input[name="formatRadio"]:checked').value;
+        const request = new XMLHttpRequest();
+        request.open('GET', `/testrun/${exportFormat}/${e.dataset['id']}`);
+        request.onload = () => {
+            // get response
+            const response = request.responseText;
+            // set export modal body 
+            const exportResponse = document.querySelector('#exportResponse');
+            exportResponse.innerHTML = response;
+            exportResponse.style.display = '';
+            document.querySelector('#exportRequest').style.display = 'none';
+            document.querySelector('#exportButton').style.display = 'none';
+        };
+        request.send();
+    }
+
+    function set_update_item(item_id) {
+        document.querySelector('#updateLabel').innerHTML = `Update Testrun ID #${item_id}`;
+        document.querySelector('#updateForm').setAttribute('action', `/testrun/${item_id}/import`);
+    }
+
+    function add_datafile_link(limit) {
+        const active = document.getElementById('activeDataFiles');
+        // display DataFile header
+        if (active.value == "0") {
+            document.getElementById('dataFileHeader').style.display = '';
+        }
+        // display DataFile field
+        document.getElementById(`dataDiv${++active.value}`).style.display = '';
+        // check if DataFile number limit achieved
+        if (active.value == limit) {
+            const link = document.getElementById('dataFileExpander');
+            link.setAttribute('class', 'text-muted');
+            link.setAttribute('onclick', '');
+        } 
+    }
+
+    function run_item(item_id) {
+        /*
+        run the testrun via API web-service
+        */
+        const request = new XMLHttpRequest();
+        request.open('GET', `/testrun/${item_id}/run`);
+        request.onload = () => {
+            // get response
+            const response = request.responseText;
+            //document.querySelector('#loading').style.display = 'none';
+            //alert(response);
+            document.documentElement.innerHTML = response;
+
+        }
+        scroll(0, 0);
+        document.querySelector('#titleLoading').innerHTML = `Running Testrun ID #${item_id}`;
+        document.querySelector('#loading').style.display = 'block';
+        
+        request.send();
+
+    }

+ 32 - 0
baangt/reports/templates/base.html

@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <!-- Required meta tags -->
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
+
+    <!-- Local CSS -->
+    <link rel="stylesheet" href="css/styles.css">
+
+    <title>
+        {% block title %}
+        {% endblock %}
+    </title>
+  </head>
+  
+  <body>
+    <nav class="navbar navbar-expand navbar-dark bg-dark flex-md-nowrap p-1 shadow">
+        <a class="navbar-brand ml-3 py-2" href="#">
+            {% block header %}
+            {% endblock %}
+        </a>
+    </nav>
+      
+    {% block content %}
+    {% endblock %}
+
+  </body>
+</html>

+ 86 - 0
baangt/reports/templates/dashboard.html

@@ -0,0 +1,86 @@
+{% extends "base.html" %}
+
+{% block title %}
+    BAANGT Dashboard
+{% endblock %}
+
+{% block header %}
+    BAANGT Dashboard {{ created }}
+{% endblock %}
+
+{% block content %}
+
+<main>
+
+<!-- testrun list -->
+<div class="container-fluid mt-2">
+
+    <!-- header -->
+    <div class="row mx-2 mb-2 mt-3">
+        <div class="col-md-3 h3">Testrun</div>
+        <div class="col-md-3 h3">Summary</div>
+        <div class="col-md-3 h3">Results</div>
+        <div class="col-md-3 h3">Duration</div>
+    </div>
+
+    <!-- testrun list -->
+    {% for key, value in data.items() %}
+        <div class="row hovered-item border-top py-3 mx-2" id="{{ key }}">
+            <div class="col-md-3 text-break">
+                <p class="text-primary">{{ key }}</p>
+                <!--p class="text-secondary">Stage</p--> 
+            </div>
+            <div class="col-md-3">
+                <div class="row">
+                    <div class="d-flex col-3">
+                        <canvas class="statusChart" width="100" height="100"></canvas>
+                    </div>
+                    <div class="d-flex flex-column col-2 border-right px-0">
+                        <h2 class="text-center">{{ value.figures.records }}</h2>
+                        <h6 class="text-center">TESTS</h6>
+                    </div>
+                    {% if value.figures.successful > 0  %}
+                        <div class="d-flex flex-column col-2 text-success px-0">
+                            <h2 class="text-center">{{ value.figures.successful }}</h2>
+                            <h6 class="text-center">PASSED</h6>
+                        </div>
+                    {% endif %}
+                    {% if value.figures.error > 0  %}
+                        <div class="d-flex flex-column col-2 text-danger px-0">
+                            <h2 class="text-center">{{ value.figures.error }}</h2>
+                            <h6 class="text-center">FAILED</h6>
+                        </div>
+                    {% endif %}
+                    {% if value.figures.paused > 0  %}
+                        <div class="d-flex flex-column col-2 text-secondary px-0">
+                            <h2 class="text-center">{{ value.figures.paused }}</h2>
+                            <h6 class="text-center">PAUSED</h6>
+                        </div>
+                    {% endif %}
+                </div>
+            </div>
+            <div class="col-md-3">
+                <canvas class="resultsChart" height="60"></canvas>
+            </div>
+            <div class="col-md-3 text-break">
+                <canvas class="durationChart" height="60"></canvas>
+            </div>
+        </div>
+
+    {% endfor %}
+
+</div>
+</main>
+
+<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
+<script type="text/javascript">
+    {% for key, value in data.items() %}
+        var item_{{ loop.index }} = document.querySelector('#{{ key }}')
+
+        var status_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.statusChart'), {{ value.status }});
+        var results_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.resultsChart'), {{ value.results }});
+        var duration_{{ loop.index }} = new Chart(item_{{ loop.index }}.querySelector('.durationChart'), {{ value.duration }});
+    {% endfor %}
+</script>
+
+{% endblock %}

BIN
examples/YoutubePromo_singlevideo.xlsx


BIN
examples/YoutubePromo_twovideos.xlsx


+ 25 - 0
globals_20200519_134439.json

@@ -0,0 +1,25 @@
+{
+    "TC.slowExecution": "False",
+    "TC.dontCloseBrowser": "False",
+    "TC.Browser": "FF",
+    "TC.BrowserAttributes": "",
+    "TC.ParallelRuns": "",
+    "TC.Lines": "",
+    "TC.NetworkInfo": "False",
+    "TC.ExportAllFields": "False",
+    "TC.Stage": "other",
+    "TC.Release": "",
+    "TX.DEBUG": "False",
+    "TC.RestartBrowser": "False",
+    "TC.UseRotatingProxies": "False",
+    "TC.ReReadProxies": "False",
+    "CL.browserFactory": "baangt.base.BrowserFactory.BrowserFactory",
+    "CL.browserHandling": "baangt.base.BrowserHandling.BrowserHandling.BrowserDriver",
+    "CL.testCaseSequenceMaster": "baangt.TestCaseSequenceMaster.TestCaseSequenceMaster",
+    "TC.TestStepClass": "",
+    "TC.TestDataFileName": "",
+    "RootPath": "/home/aguryev/freelancer/baangt",
+    "Screenshots": "/home/aguryev/freelancer/baangt/Screenshots",
+    "1TestResults": "/home/aguryev/freelancer/baangt/1testoutput",
+    "0TestInput": "/home/aguryev/freelancer/baangt/0testdateninput"
+}

+ 1 - 0
requirements.txt

@@ -22,3 +22,4 @@ xl2dict>=0.1.5
 xlrd>=1.2.0
 XlsxWriter>=1.2.7
 numpy>=1.18.4
+jinja2>=2.11