summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/testmanager/webui
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-11 08:17:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-11 08:17:27 +0000
commitf215e02bf85f68d3a6106c2a1f4f7f063f819064 (patch)
tree6bb5b92c046312c4e95ac2620b10ddf482d3fa8b /src/VBox/ValidationKit/testmanager/webui
parentInitial commit. (diff)
downloadvirtualbox-f215e02bf85f68d3a6106c2a1f4f7f063f819064.tar.xz
virtualbox-f215e02bf85f68d3a6106c2a1f4f7f063f819064.zip
Adding upstream version 7.0.14-dfsg.upstream/7.0.14-dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/VBox/ValidationKit/testmanager/webui')
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/Makefile.kmk47
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/__init__.py40
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-details.html45
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html46
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template-tooltip.html20
-rw-r--r--src/VBox/ValidationKit/testmanager/webui/template.html65
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmin.py1270
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py154
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py164
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py122
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py160
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py155
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py175
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py130
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py205
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py79
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py447
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py72
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py85
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py490
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py258
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py197
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py110
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuibase.py1245
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuicontentbase.py1290
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py660
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpform.py1111
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py128
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py131
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py376
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py341
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py163
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuilogviewer.py251
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuimain.py1344
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuireport.py817
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuitestresult.py965
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py110
-rwxr-xr-xsrc/VBox/ValidationKit/testmanager/webui/wuivcshistory.py101
38 files changed, 13569 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk
new file mode 100644
index 00000000..5a9b58bd
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk
@@ -0,0 +1,47 @@
+# $Id: Makefile.kmk $
+## @file
+# VirtualBox Validation Kit.
+#
+
+#
+# Copyright (C) 2006-2023 Oracle and/or its affiliates.
+#
+# This file is part of VirtualBox base platform packages, as
+# available from https://www.virtualbox.org.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, in version 3 of the
+# License.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <https://www.gnu.org/licenses>.
+#
+# The contents of this file may alternatively be used under the terms
+# of the Common Development and Distribution License Version 1.0
+# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+# in the VirtualBox distribution, in which case the provisions of the
+# CDDL are applicable instead of those of the GPL.
+#
+# You may elect to license modified versions of this file under the
+# terms and conditions of either the GPL or the CDDL or both.
+#
+# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+#
+
+SUB_DEPTH = ../../../../..
+include $(KBUILD_PATH)/subheader.kmk
+
+
+VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py)
+VBOX_VALIDATIONKIT_PYUNITTEST_EXCLUDE += $(PATH_SUB_CURRENT)/wuihlpgraphmatplotlib.py
+
+$(evalcall def_vbox_validationkit_process_python_sources)
+$(evalcall def_vbox_validationkit_process_js_sources)
+include $(FILE_KBUILD_SUB_FOOTER)
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/__init__.py b/src/VBox/ValidationKit/testmanager/webui/__init__.py
new file mode 100644
index 00000000..d00f5436
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# $Id: __init__.py $
+
+"""
+TestBox Script - WUI Presentation.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-details.html b/src/VBox/ValidationKit/testmanager/webui/template-details.html
new file mode 100644
index 00000000..e7e16c0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-details.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/details.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="top-menu" class="tm-top-menu-wo-side">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html
new file mode 100644
index 00000000..4e1dc0c8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/graphwiz.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <script type="text/javascript" src="htdocs/js/graphwiz.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="top-menu" class="tm-top-menu-wo-side">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html
new file mode 100644
index 00000000..7aa95d71
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <title>@@PAGE_TITLE@@</title>
+</head>
+
+<body scroll="no">
+<div id="tooltip" class="tooltip-main">
+<div id="tooltip-inner" class="tooltip-inner">
+@@PAGE_BODY@@
+</div>
+</div>
+</body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/template.html b/src/VBox/ValidationKit/testmanager/webui/template.html
new file mode 100644
index 00000000..6480c20b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/template.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <meta http-equiv="content-language" content="en" />
+ <meta name="language" content="en" />
+ <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" />
+ <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" />
+ <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" />
+ <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" />
+ <script type="text/javascript" src="htdocs/js/common.js"></script>
+ <title>@@PAGE_TITLE@@</title>
+ </head>
+
+ <body>
+ <div id="wrap">
+ <div id="head-wrap">
+ <div id="logo">
+ <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg">
+ </div>
+ <div id="header">
+ <h1>@@PAGE_TITLE@@</h1>
+ </div>
+ <div id="login">
+ <p><small>
+ Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@
+ </small></p>
+ </div>
+ <div id="top-menu">
+ <ul>
+ @@TOP_MENU_ITEMS@@
+ </ul>
+ </div>
+ </div>
+
+ <div id="side-menu-wrap">
+ <div id="side-menu">
+ <div id="side-menu-body">
+ <form id="side-menu-form" @@SIDE_MENU_FORM_ATTRS@@>
+ <ul>
+ @@SIDE_MENU_ITEMS@@
+ </ul>
+ @@SIDE_FILTER_CONTROL@@
+ </form>
+ </div>
+ <!-- justify-content: space-between -->
+ <div id="side-footer">
+ <p>
+ VBox Test Manager<br/>@@TESTMANAGER_VERSION@@r@@TESTMANAGER_REVISION@@
+ </p>
+ <p>Copyright &copy; 2012-2023 Oracle Corporation</p>
+ </div>
+ </div>
+ </div>
+
+ <div id="main">
+ @@PAGE_BODY@@
+
+ @@DEBUG@@
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
new file mode 100755
index 00000000..ef5c7669
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py
@@ -0,0 +1,1270 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmin.py $
+
+"""
+Test Manager Core - WUI - Admin Main page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import cgitb;
+import sys;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager import config;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException
+
+
+class WuiAdmin(WuiDispatcherBase):
+ """
+ WUI Admin main page.
+ """
+
+ ## The name of the script.
+ ksScriptName = 'admin.py'
+
+ ## Number of days back.
+ ksParamDaysBack = 'cDaysBack';
+
+ ## @name Actions
+ ## @{
+ ksActionSystemLogList = 'SystemLogList'
+ ksActionSystemChangelogList = 'SystemChangelogList'
+ ksActionSystemDbDump = 'SystemDbDump'
+ ksActionSystemDbDumpDownload = 'SystemDbDumpDownload'
+
+ ksActionUserList = 'UserList'
+ ksActionUserAdd = 'UserAdd'
+ ksActionUserAddPost = 'UserAddPost'
+ ksActionUserEdit = 'UserEdit'
+ ksActionUserEditPost = 'UserEditPost'
+ ksActionUserDelPost = 'UserDelPost'
+ ksActionUserDetails = 'UserDetails'
+
+ ksActionTestBoxList = 'TestBoxList'
+ ksActionTestBoxListPost = 'TestBoxListPost'
+ ksActionTestBoxAdd = 'TestBoxAdd'
+ ksActionTestBoxAddPost = 'TestBoxAddPost'
+ ksActionTestBoxEdit = 'TestBoxEdit'
+ ksActionTestBoxEditPost = 'TestBoxEditPost'
+ ksActionTestBoxDetails = 'TestBoxDetails'
+ ksActionTestBoxRemovePost = 'TestBoxRemove'
+ ksActionTestBoxesRegenQueues = 'TestBoxesRegenQueues';
+
+ ksActionTestCaseList = 'TestCaseList'
+ ksActionTestCaseAdd = 'TestCaseAdd'
+ ksActionTestCaseAddPost = 'TestCaseAddPost'
+ ksActionTestCaseClone = 'TestCaseClone'
+ ksActionTestCaseDetails = 'TestCaseDetails'
+ ksActionTestCaseEdit = 'TestCaseEdit'
+ ksActionTestCaseEditPost = 'TestCaseEditPost'
+ ksActionTestCaseDoRemove = 'TestCaseDoRemove'
+
+ ksActionGlobalRsrcShowAll = 'GlobalRsrcShowAll'
+ ksActionGlobalRsrcShowAdd = 'GlobalRsrcShowAdd'
+ ksActionGlobalRsrcShowEdit = 'GlobalRsrcShowEdit'
+ ksActionGlobalRsrcAdd = 'GlobalRsrcAddPost'
+ ksActionGlobalRsrcEdit = 'GlobalRsrcEditPost'
+ ksActionGlobalRsrcDel = 'GlobalRsrcDelPost'
+
+ ksActionBuildList = 'BuildList'
+ ksActionBuildAdd = 'BuildAdd'
+ ksActionBuildAddPost = 'BuildAddPost'
+ ksActionBuildClone = 'BuildClone'
+ ksActionBuildDetails = 'BuildDetails'
+ ksActionBuildDoRemove = 'BuildDoRemove'
+ ksActionBuildEdit = 'BuildEdit'
+ ksActionBuildEditPost = 'BuildEditPost'
+
+ ksActionBuildBlacklist = 'BuildBlacklist';
+ ksActionBuildBlacklistAdd = 'BuildBlacklistAdd';
+ ksActionBuildBlacklistAddPost = 'BuildBlacklistAddPost';
+ ksActionBuildBlacklistClone = 'BuildBlacklistClone';
+ ksActionBuildBlacklistDetails = 'BuildBlacklistDetails';
+ ksActionBuildBlacklistDoRemove = 'BuildBlacklistDoRemove';
+ ksActionBuildBlacklistEdit = 'BuildBlacklistEdit';
+ ksActionBuildBlacklistEditPost = 'BuildBlacklistEditPost';
+
+ ksActionFailureCategoryList = 'FailureCategoryList';
+ ksActionFailureCategoryAdd = 'FailureCategoryAdd';
+ ksActionFailureCategoryAddPost = 'FailureCategoryAddPost';
+ ksActionFailureCategoryDetails = 'FailureCategoryDetails';
+ ksActionFailureCategoryDoRemove = 'FailureCategoryDoRemove';
+ ksActionFailureCategoryEdit = 'FailureCategoryEdit';
+ ksActionFailureCategoryEditPost = 'FailureCategoryEditPost';
+
+ ksActionFailureReasonList = 'FailureReasonList'
+ ksActionFailureReasonAdd = 'FailureReasonAdd'
+ ksActionFailureReasonAddPost = 'FailureReasonAddPost'
+ ksActionFailureReasonDetails = 'FailureReasonDetails'
+ ksActionFailureReasonDoRemove = 'FailureReasonDoRemove'
+ ksActionFailureReasonEdit = 'FailureReasonEdit'
+ ksActionFailureReasonEditPost = 'FailureReasonEditPost'
+
+ ksActionBuildSrcList = 'BuildSrcList'
+ ksActionBuildSrcAdd = 'BuildSrcAdd'
+ ksActionBuildSrcAddPost = 'BuildSrcAddPost'
+ ksActionBuildSrcClone = 'BuildSrcClone'
+ ksActionBuildSrcDetails = 'BuildSrcDetails'
+ ksActionBuildSrcEdit = 'BuildSrcEdit'
+ ksActionBuildSrcEditPost = 'BuildSrcEditPost'
+ ksActionBuildSrcDoRemove = 'BuildSrcDoRemove'
+
+ ksActionBuildCategoryList = 'BuildCategoryList'
+ ksActionBuildCategoryAdd = 'BuildCategoryAdd'
+ ksActionBuildCategoryAddPost = 'BuildCategoryAddPost'
+ ksActionBuildCategoryClone = 'BuildCategoryClone';
+ ksActionBuildCategoryDetails = 'BuildCategoryDetails';
+ ksActionBuildCategoryDoRemove = 'BuildCategoryDoRemove';
+
+ ksActionTestGroupList = 'TestGroupList'
+ ksActionTestGroupAdd = 'TestGroupAdd'
+ ksActionTestGroupAddPost = 'TestGroupAddPost'
+ ksActionTestGroupClone = 'TestGroupClone'
+ ksActionTestGroupDetails = 'TestGroupDetails'
+ ksActionTestGroupDoRemove = 'TestGroupDoRemove'
+ ksActionTestGroupEdit = 'TestGroupEdit'
+ ksActionTestGroupEditPost = 'TestGroupEditPost'
+ ksActionTestCfgRegenQueues = 'TestCfgRegenQueues'
+
+ ksActionSchedGroupList = 'SchedGroupList'
+ ksActionSchedGroupAdd = 'SchedGroupAdd';
+ ksActionSchedGroupAddPost = 'SchedGroupAddPost';
+ ksActionSchedGroupClone = 'SchedGroupClone';
+ ksActionSchedGroupDetails = 'SchedGroupDetails';
+ ksActionSchedGroupDoRemove = 'SchedGroupDel';
+ ksActionSchedGroupEdit = 'SchedGroupEdit';
+ ksActionSchedGroupEditPost = 'SchedGroupEditPost';
+ ksActionSchedQueueList = 'SchedQueueList';
+ ## @}
+
+ def __init__(self, oSrvGlue): # pylint: disable=too-many-locals,too-many-statements
+ WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName);
+ self._sTemplate = 'template.html';
+
+
+ #
+ # System actions.
+ #
+ self._dDispatch[self.ksActionSystemChangelogList] = self._actionSystemChangelogList;
+ self._dDispatch[self.ksActionSystemLogList] = self._actionSystemLogList;
+ self._dDispatch[self.ksActionSystemDbDump] = self._actionSystemDbDump;
+ self._dDispatch[self.ksActionSystemDbDumpDownload] = self._actionSystemDbDumpDownload;
+
+ #
+ # User Account actions.
+ #
+ self._dDispatch[self.ksActionUserList] = self._actionUserList;
+ self._dDispatch[self.ksActionUserAdd] = self._actionUserAdd;
+ self._dDispatch[self.ksActionUserEdit] = self._actionUserEdit;
+ self._dDispatch[self.ksActionUserAddPost] = self._actionUserAddPost;
+ self._dDispatch[self.ksActionUserEditPost] = self._actionUserEditPost;
+ self._dDispatch[self.ksActionUserDetails] = self._actionUserDetails;
+ self._dDispatch[self.ksActionUserDelPost] = self._actionUserDelPost;
+
+ #
+ # TestBox actions.
+ #
+ self._dDispatch[self.ksActionTestBoxList] = self._actionTestBoxList;
+ self._dDispatch[self.ksActionTestBoxListPost] = self._actionTestBoxListPost;
+ self._dDispatch[self.ksActionTestBoxAdd] = self._actionTestBoxAdd;
+ self._dDispatch[self.ksActionTestBoxAddPost] = self._actionTestBoxAddPost;
+ self._dDispatch[self.ksActionTestBoxDetails] = self._actionTestBoxDetails;
+ self._dDispatch[self.ksActionTestBoxEdit] = self._actionTestBoxEdit;
+ self._dDispatch[self.ksActionTestBoxEditPost] = self._actionTestBoxEditPost;
+ self._dDispatch[self.ksActionTestBoxRemovePost] = self._actionTestBoxRemovePost;
+ self._dDispatch[self.ksActionTestBoxesRegenQueues] = self._actionRegenQueuesCommon;
+
+ #
+ # Test Case actions.
+ #
+ self._dDispatch[self.ksActionTestCaseList] = self._actionTestCaseList;
+ self._dDispatch[self.ksActionTestCaseAdd] = self._actionTestCaseAdd;
+ self._dDispatch[self.ksActionTestCaseAddPost] = self._actionTestCaseAddPost;
+ self._dDispatch[self.ksActionTestCaseClone] = self._actionTestCaseClone;
+ self._dDispatch[self.ksActionTestCaseDetails] = self._actionTestCaseDetails;
+ self._dDispatch[self.ksActionTestCaseEdit] = self._actionTestCaseEdit;
+ self._dDispatch[self.ksActionTestCaseEditPost] = self._actionTestCaseEditPost;
+ self._dDispatch[self.ksActionTestCaseDoRemove] = self._actionTestCaseDoRemove;
+
+ #
+ # Global Resource actions
+ #
+ self._dDispatch[self.ksActionGlobalRsrcShowAll] = self._actionGlobalRsrcShowAll;
+ self._dDispatch[self.ksActionGlobalRsrcShowAdd] = self._actionGlobalRsrcShowAdd;
+ self._dDispatch[self.ksActionGlobalRsrcShowEdit] = self._actionGlobalRsrcShowEdit;
+ self._dDispatch[self.ksActionGlobalRsrcAdd] = self._actionGlobalRsrcAdd;
+ self._dDispatch[self.ksActionGlobalRsrcEdit] = self._actionGlobalRsrcEdit;
+ self._dDispatch[self.ksActionGlobalRsrcDel] = self._actionGlobalRsrcDel;
+
+ #
+ # Build Source actions
+ #
+ self._dDispatch[self.ksActionBuildSrcList] = self._actionBuildSrcList;
+ self._dDispatch[self.ksActionBuildSrcAdd] = self._actionBuildSrcAdd;
+ self._dDispatch[self.ksActionBuildSrcAddPost] = self._actionBuildSrcAddPost;
+ self._dDispatch[self.ksActionBuildSrcClone] = self._actionBuildSrcClone;
+ self._dDispatch[self.ksActionBuildSrcDetails] = self._actionBuildSrcDetails;
+ self._dDispatch[self.ksActionBuildSrcDoRemove] = self._actionBuildSrcDoRemove;
+ self._dDispatch[self.ksActionBuildSrcEdit] = self._actionBuildSrcEdit;
+ self._dDispatch[self.ksActionBuildSrcEditPost] = self._actionBuildSrcEditPost;
+
+ #
+ # Build actions
+ #
+ self._dDispatch[self.ksActionBuildList] = self._actionBuildList;
+ self._dDispatch[self.ksActionBuildAdd] = self._actionBuildAdd;
+ self._dDispatch[self.ksActionBuildAddPost] = self._actionBuildAddPost;
+ self._dDispatch[self.ksActionBuildClone] = self._actionBuildClone;
+ self._dDispatch[self.ksActionBuildDetails] = self._actionBuildDetails;
+ self._dDispatch[self.ksActionBuildDoRemove] = self._actionBuildDoRemove;
+ self._dDispatch[self.ksActionBuildEdit] = self._actionBuildEdit;
+ self._dDispatch[self.ksActionBuildEditPost] = self._actionBuildEditPost;
+
+ #
+ # Build Black List actions
+ #
+ self._dDispatch[self.ksActionBuildBlacklist] = self._actionBuildBlacklist;
+ self._dDispatch[self.ksActionBuildBlacklistAdd] = self._actionBuildBlacklistAdd;
+ self._dDispatch[self.ksActionBuildBlacklistAddPost] = self._actionBuildBlacklistAddPost;
+ self._dDispatch[self.ksActionBuildBlacklistClone] = self._actionBuildBlacklistClone;
+ self._dDispatch[self.ksActionBuildBlacklistDetails] = self._actionBuildBlacklistDetails;
+ self._dDispatch[self.ksActionBuildBlacklistDoRemove] = self._actionBuildBlacklistDoRemove;
+ self._dDispatch[self.ksActionBuildBlacklistEdit] = self._actionBuildBlacklistEdit;
+ self._dDispatch[self.ksActionBuildBlacklistEditPost] = self._actionBuildBlacklistEditPost;
+
+ #
+ # Failure Category actions
+ #
+ self._dDispatch[self.ksActionFailureCategoryList] = self._actionFailureCategoryList;
+ self._dDispatch[self.ksActionFailureCategoryAdd] = self._actionFailureCategoryAdd;
+ self._dDispatch[self.ksActionFailureCategoryAddPost] = self._actionFailureCategoryAddPost;
+ self._dDispatch[self.ksActionFailureCategoryDetails] = self._actionFailureCategoryDetails;
+ self._dDispatch[self.ksActionFailureCategoryDoRemove] = self._actionFailureCategoryDoRemove;
+ self._dDispatch[self.ksActionFailureCategoryEdit] = self._actionFailureCategoryEdit;
+ self._dDispatch[self.ksActionFailureCategoryEditPost] = self._actionFailureCategoryEditPost;
+
+ #
+ # Failure Reason actions
+ #
+ self._dDispatch[self.ksActionFailureReasonList] = self._actionFailureReasonList;
+ self._dDispatch[self.ksActionFailureReasonAdd] = self._actionFailureReasonAdd;
+ self._dDispatch[self.ksActionFailureReasonAddPost] = self._actionFailureReasonAddPost;
+ self._dDispatch[self.ksActionFailureReasonDetails] = self._actionFailureReasonDetails;
+ self._dDispatch[self.ksActionFailureReasonDoRemove] = self._actionFailureReasonDoRemove;
+ self._dDispatch[self.ksActionFailureReasonEdit] = self._actionFailureReasonEdit;
+ self._dDispatch[self.ksActionFailureReasonEditPost] = self._actionFailureReasonEditPost;
+
+ #
+ # Build Category actions
+ #
+ self._dDispatch[self.ksActionBuildCategoryList] = self._actionBuildCategoryList;
+ self._dDispatch[self.ksActionBuildCategoryAdd] = self._actionBuildCategoryAdd;
+ self._dDispatch[self.ksActionBuildCategoryAddPost] = self._actionBuildCategoryAddPost;
+ self._dDispatch[self.ksActionBuildCategoryClone] = self._actionBuildCategoryClone;
+ self._dDispatch[self.ksActionBuildCategoryDetails] = self._actionBuildCategoryDetails;
+ self._dDispatch[self.ksActionBuildCategoryDoRemove] = self._actionBuildCategoryDoRemove;
+
+ #
+ # Test Group actions
+ #
+ self._dDispatch[self.ksActionTestGroupList] = self._actionTestGroupList;
+ self._dDispatch[self.ksActionTestGroupAdd] = self._actionTestGroupAdd;
+ self._dDispatch[self.ksActionTestGroupAddPost] = self._actionTestGroupAddPost;
+ self._dDispatch[self.ksActionTestGroupClone] = self._actionTestGroupClone;
+ self._dDispatch[self.ksActionTestGroupDetails] = self._actionTestGroupDetails;
+ self._dDispatch[self.ksActionTestGroupEdit] = self._actionTestGroupEdit;
+ self._dDispatch[self.ksActionTestGroupEditPost] = self._actionTestGroupEditPost;
+ self._dDispatch[self.ksActionTestGroupDoRemove] = self._actionTestGroupDoRemove;
+ self._dDispatch[self.ksActionTestCfgRegenQueues] = self._actionRegenQueuesCommon;
+
+ #
+ # Scheduling Group and Queue actions
+ #
+ self._dDispatch[self.ksActionSchedGroupList] = self._actionSchedGroupList;
+ self._dDispatch[self.ksActionSchedGroupAdd] = self._actionSchedGroupAdd;
+ self._dDispatch[self.ksActionSchedGroupClone] = self._actionSchedGroupClone;
+ self._dDispatch[self.ksActionSchedGroupDetails] = self._actionSchedGroupDetails;
+ self._dDispatch[self.ksActionSchedGroupEdit] = self._actionSchedGroupEdit;
+ self._dDispatch[self.ksActionSchedGroupAddPost] = self._actionSchedGroupAddPost;
+ self._dDispatch[self.ksActionSchedGroupEditPost] = self._actionSchedGroupEditPost;
+ self._dDispatch[self.ksActionSchedGroupDoRemove] = self._actionSchedGroupDoRemove;
+ self._dDispatch[self.ksActionSchedQueueList] = self._actionSchedQueueList;
+
+
+ #
+ # Menus
+ #
+ self._aaoMenus = \
+ [
+ [
+ 'Builds', self._sActionUrlBase + self.ksActionBuildList,
+ [
+ [ 'Builds', self._sActionUrlBase + self.ksActionBuildList, False ],
+ [ 'Blacklist', self._sActionUrlBase + self.ksActionBuildBlacklist, False ],
+ [ 'Build sources', self._sActionUrlBase + self.ksActionBuildSrcList, False ],
+ [ 'Build categories', self._sActionUrlBase + self.ksActionBuildCategoryList, False ],
+ [ 'New build', self._sActionUrlBase + self.ksActionBuildAdd, True ],
+ [ 'New blacklisting', self._sActionUrlBase + self.ksActionBuildBlacklistAdd, True ],
+ [ 'New build source', self._sActionUrlBase + self.ksActionBuildSrcAdd, True ],
+ [ 'New build category', self._sActionUrlBase + self.ksActionBuildCategoryAdd, True ],
+ ]
+ ],
+ [
+ 'Failure Reasons', self._sActionUrlBase + self.ksActionFailureReasonList,
+ [
+ [ 'Failure categories', self._sActionUrlBase + self.ksActionFailureCategoryList, False ],
+ [ 'Failure reasons', self._sActionUrlBase + self.ksActionFailureReasonList, False ],
+ [ 'New failure category', self._sActionUrlBase + self.ksActionFailureCategoryAdd, True ],
+ [ 'New failure reason', self._sActionUrlBase + self.ksActionFailureReasonAdd, True ],
+ ]
+ ],
+ [
+ 'System', self._sActionUrlBase + self.ksActionSystemChangelogList,
+ [
+ [ 'Changelog', self._sActionUrlBase + self.ksActionSystemChangelogList, False ],
+ [ 'System log', self._sActionUrlBase + self.ksActionSystemLogList, False ],
+ [ 'Partial DB Dump', self._sActionUrlBase + self.ksActionSystemDbDump, False ],
+ [ 'User accounts', self._sActionUrlBase + self.ksActionUserList, False ],
+ [ 'New user', self._sActionUrlBase + self.ksActionUserAdd, True ],
+ ]
+ ],
+ [
+ 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList,
+ [
+ [ 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList, False ],
+ [ 'Scheduling groups', self._sActionUrlBase + self.ksActionSchedGroupList, False ],
+ [ 'New testbox', self._sActionUrlBase + self.ksActionTestBoxAdd, True ],
+ [ 'New scheduling group', self._sActionUrlBase + self.ksActionSchedGroupAdd, True ],
+ [ 'View scheduling queues', self._sActionUrlBase + self.ksActionSchedQueueList, False ],
+ [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestBoxesRegenQueues, True ],
+ ]
+ ],
+ [
+ 'Test Config', self._sActionUrlBase + self.ksActionTestGroupList,
+ [
+ [ 'Test cases', self._sActionUrlBase + self.ksActionTestCaseList, False ],
+ [ 'Test groups', self._sActionUrlBase + self.ksActionTestGroupList, False ],
+ [ 'Global resources', self._sActionUrlBase + self.ksActionGlobalRsrcShowAll, False ],
+ [ 'New test case', self._sActionUrlBase + self.ksActionTestCaseAdd, True ],
+ [ 'New test group', self._sActionUrlBase + self.ksActionTestGroupAdd, True ],
+ [ 'New global resource', self._sActionUrlBase + self.ksActionGlobalRsrcShowAdd, True ],
+ [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestCfgRegenQueues, True ],
+ ]
+ ],
+ [
+ '> Test Results', 'index.py?' + webutils.encodeUrlParams(self._dDbgParams), []
+ ],
+ ];
+
+
+ def _actionDefault(self):
+ """Show the default admin page."""
+ self._sAction = self.ksActionTestBoxList;
+ from testmanager.core.testbox import TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+ return self._actionGenericListing(TestBoxLogic, WuiTestBoxList);
+
+ def _actionGenericDoDelOld(self, oCoreObjectLogic, sCoreObjectIdFieldName, sRedirectAction):
+ """
+ Delete entry (using oLogicType.remove).
+
+ @param oCoreObjectLogic A *Logic class
+
+ @param sCoreObjectIdFieldName Name of HTTP POST variable that
+ contains object ID information
+
+ @param sRedirectAction An action for redirect user to
+ in case of operation success
+ """
+ iCoreDataObjectId = self.getStringParam(sCoreObjectIdFieldName) # STRING?!?!
+ self._checkForUnknownParameters()
+
+ try:
+ self._sPageTitle = None
+ self._sPageBody = None
+ self._sRedirectTo = self._sActionUrlBase + sRedirectAction
+ return oCoreObjectLogic(self._oDb).remove(self._oCurUser.uid, iCoreDataObjectId)
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageTitle = 'Unable to delete record'
+ self._sPageBody = str(oXcpt);
+ if config.g_kfDebugDbXcpt:
+ self._sPageBody += cgitb.html(sys.exc_info());
+ self._sRedirectTo = None
+
+ return False
+
+
+ #
+ # System Category.
+ #
+
+ # System wide changelog actions.
+
+ def _actionSystemChangelogList(self):
+ """ Action handler. """
+ from testmanager.core.systemchangelog import SystemChangelogLogic;
+ from testmanager.webui.wuiadminsystemchangelog import WuiAdminSystemChangelogList;
+
+ tsEffective = self.getEffectiveDateParam();
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = 1, iMax = 366, iDefault = 14);
+ self._checkForUnknownParameters();
+
+ aoEntries = SystemChangelogLogic(self._oDb).fetchForListingEx(iPage * cItemsPerPage, cItemsPerPage + 1,
+ tsEffective, cDaysBack);
+ oContent = WuiAdminSystemChangelogList(aoEntries, iPage, cItemsPerPage, tsEffective,
+ cDaysBack = cDaysBack, fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ # System Log actions.
+
+ def _actionSystemLogList(self):
+ """ Action wrapper. """
+ from testmanager.core.systemlog import SystemLogLogic;
+ from testmanager.webui.wuiadminsystemlog import WuiAdminSystemLogList;
+ return self._actionGenericListing(SystemLogLogic, WuiAdminSystemLogList)
+
+ def _actionSystemDbDump(self):
+ """ Action handler. """
+ from testmanager.webui.wuiadminsystemdbdump import WuiAdminSystemDbDumpForm;
+
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays,
+ iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays);
+ self._checkForUnknownParameters();
+
+ oContent = WuiAdminSystemDbDumpForm(cDaysBack, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+ return True;
+
+ def _actionSystemDbDumpDownload(self):
+ """ Action handler. """
+ import datetime;
+ import os;
+
+ cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays,
+ iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays);
+ self._checkForUnknownParameters();
+
+ #
+ # Generate the dump.
+ #
+ # We generate a file name that's unique to a user is smart enough to only
+ # issue one of these requests at the time. This also makes sure we won't
+ # waste too much space should this code get interrupted and rerun.
+ #
+ oFile = None;
+ oNow = datetime.datetime.utcnow();
+ sOutFile = config.g_ksTmDbDumpOutFileTmpl % (self._oCurUser.uid,);
+ sTmpFile = config.g_ksTmDbDumpTmpFileTmpl % (self._oCurUser.uid,);
+ sScript = os.path.join(config.g_ksTestManagerDir, 'db', 'partial-db-dump.py');
+ try:
+ (iExitCode, sStdOut, sStdErr) = utils.processOutputUnchecked([ sScript,
+ '--days-to-dump', str(cDaysBack),
+ '-f', sOutFile,
+ '-t', sTmpFile,
+ ]);
+ if iExitCode != 0:
+ raise Exception('iExitCode=%s\n--- stderr ---\n%s\n--- stdout ---\n%s' % (iExitCode, sStdOut, sStdErr,));
+
+ #
+ # Open and send the dump.
+ #
+ oFile = open(sOutFile, 'rb'); # pylint: disable=consider-using-with
+ cbFile = os.fstat(oFile.fileno()).st_size;
+
+ self._oSrvGlue.setHeaderField('Content-Type', 'application/zip');
+ self._oSrvGlue.setHeaderField('Content-Disposition',
+ oNow.strftime('attachment; filename="partial-db-dump-%Y-%m-%dT%H-%M-%S.zip"'));
+ self._oSrvGlue.setHeaderField('Content-Length', str(cbFile));
+
+ while True:
+ abChunk = oFile.read(262144);
+ if not abChunk:
+ break;
+ self._oSrvGlue.writeRaw(abChunk);
+
+ finally:
+ # Delete the file to save space.
+ if oFile:
+ try: oFile.close();
+ except: pass;
+ utils.noxcptDeleteFile(sOutFile);
+ utils.noxcptDeleteFile(sTmpFile);
+ return self.ksDispatchRcAllDone;
+
+
+ # User Account actions.
+
+ def _actionUserList(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccountList;
+ return self._actionGenericListing(UserAccountLogic, WuiUserAccountList)
+
+ def _actionUserAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormAdd(UserAccountData, WuiUserAccount)
+
+ def _actionUserDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormDetails(UserAccountData, UserAccountLogic, WuiUserAccount, 'uid');
+
+ def _actionUserEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormEdit(UserAccountData, WuiUserAccount, UserAccountData.ksParam_uid);
+
+ def _actionUserAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormAddPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList)
+
+ def _actionUserEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ from testmanager.webui.wuiadminuseraccount import WuiUserAccount;
+ return self._actionGenericFormEditPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList)
+
+ def _actionUserDelPost(self):
+ """ Action wrapper. """
+ from testmanager.core.useraccount import UserAccountData, UserAccountLogic;
+ return self._actionGenericDoRemove(UserAccountLogic, UserAccountData.ksParam_uid, self.ksActionUserList)
+
+
+ #
+ # TestBox & Scheduling Category.
+ #
+
+ def _actionTestBoxList(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxLogic
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+ return self._actionGenericListing(TestBoxLogic, WuiTestBoxList);
+
+ def _actionTestBoxListPost(self):
+ """Actions on a list of testboxes."""
+ from testmanager.core.testbox import TestBoxData, TestBoxLogic
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxList;
+
+ # Parameters.
+ aidTestBoxes = self.getListOfIntParams(TestBoxData.ksParam_idTestBox, iMin = 1, aiDefaults = []);
+ sListAction = self.getStringParam(self.ksParamListAction);
+ if sListAction in [asDesc[0] for asDesc in WuiTestBoxList.kasTestBoxActionDescs]:
+ idAction = None;
+ else:
+ asActionPrefixes = [ 'setgroup-', ];
+ i = 0;
+ while i < len(asActionPrefixes) and not sListAction.startswith(asActionPrefixes[i]):
+ i += 1;
+ if i >= len(asActionPrefixes):
+ raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,));
+ idAction = sListAction[len(asActionPrefixes[i]):];
+ if not idAction.isdigit():
+ raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,));
+ idAction = int(idAction);
+ sListAction = sListAction[:len(asActionPrefixes[i]) - 1];
+ self._checkForUnknownParameters();
+
+
+ # Take action.
+ if sListAction == 'none':
+ pass;
+ else:
+ oLogic = TestBoxLogic(self._oDb);
+ aoTestBoxes = []
+ for idTestBox in aidTestBoxes:
+ aoTestBoxes.append(TestBoxData().initFromDbWithId(self._oDb, idTestBox));
+
+ if sListAction in [ 'enable', 'disable' ]:
+ fEnable = sListAction == 'enable';
+ for oTestBox in aoTestBoxes:
+ if oTestBox.fEnabled != fEnable:
+ oTestBox.fEnabled = fEnable;
+ oLogic.editEntry(oTestBox, self._oCurUser.uid, fCommit = False);
+ else:
+ for oTestBox in aoTestBoxes:
+ if oTestBox.enmPendingCmd != sListAction:
+ oLogic.setCommand(oTestBox.idTestBox, oTestBox.enmPendingCmd, sListAction, self._oCurUser.uid,
+ fCommit = False);
+ self._oDb.commit();
+
+ # Re-display the list.
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = self._sActionUrlBase + self.ksActionTestBoxList;
+ return True;
+
+ def _actionTestBoxAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormAdd(TestBoxDataEx, WuiTestBox);
+
+ def _actionTestBoxAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormAddPost(TestBoxDataEx, TestBoxLogic, WuiTestBox, self.ksActionTestBoxList);
+
+ def _actionTestBoxDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormDetails(TestBoxDataEx, TestBoxLogic, WuiTestBox, 'idTestBox', 'idGenTestBox');
+
+ def _actionTestBoxEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormEdit(TestBoxDataEx, WuiTestBox, TestBoxDataEx.ksParam_idTestBox);
+
+ def _actionTestBoxEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic;
+ from testmanager.webui.wuiadmintestbox import WuiTestBox;
+ return self._actionGenericFormEditPost(TestBoxDataEx, TestBoxLogic,WuiTestBox, self.ksActionTestBoxList);
+
+ def _actionTestBoxRemovePost(self):
+ """ Action wrapper. """
+ from testmanager.core.testbox import TestBoxData, TestBoxLogic;
+ return self._actionGenericDoRemove(TestBoxLogic, TestBoxData.ksParam_idTestBox, self.ksActionTestBoxList);
+
+
+ # Scheduling Group actions
+
+ def _actionSchedGroupList(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiAdminSchedGroupList;
+ return self._actionGenericListing(SchedGroupLogic, WuiAdminSchedGroupList);
+
+ def _actionSchedGroupAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormAdd(SchedGroupDataEx, WuiSchedGroup);
+
+ def _actionSchedGroupClone(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormClone( SchedGroupDataEx, WuiSchedGroup, 'idSchedGroup');
+
+ def _actionSchedGroupDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormDetails(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, 'idSchedGroup');
+
+ def _actionSchedGroupEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormEdit(SchedGroupDataEx, WuiSchedGroup, SchedGroupDataEx.ksParam_idSchedGroup);
+
+ def _actionSchedGroupAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormAddPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList);
+
+ def _actionSchedGroupEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic;
+ from testmanager.webui.wuiadminschedgroup import WuiSchedGroup;
+ return self._actionGenericFormEditPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList);
+
+ def _actionSchedGroupDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic;
+ return self._actionGenericDoRemove(SchedGroupLogic, SchedGroupData.ksParam_idSchedGroup, self.ksActionSchedGroupList)
+
+ def _actionSchedQueueList(self):
+ """ Action wrapper. """
+ from testmanager.core.schedqueue import SchedQueueLogic;
+ from testmanager.webui.wuiadminschedqueue import WuiAdminSchedQueueList;
+ return self._actionGenericListing(SchedQueueLogic, WuiAdminSchedQueueList);
+
+ def _actionRegenQueuesCommon(self):
+ """
+ Common code for ksActionTestBoxesRegenQueues and ksActionTestCfgRegenQueues.
+
+ Too lazy to put this in some separate place right now.
+ """
+ from testmanager.core.schedgroup import SchedGroupLogic;
+ from testmanager.core.schedulerbase import SchedulerBase;
+
+ self._checkForUnknownParameters();
+ ## @todo should also be changed to a POST with a confirmation dialog preceeding it.
+
+ self._sPageTitle = 'Regenerate All Scheduling Queues';
+ if not self.isReadOnlyUser():
+ self._sPageBody = '';
+ aoGroups = SchedGroupLogic(self._oDb).getAll();
+ for oGroup in aoGroups:
+ self._sPageBody += '<h3>%s (ID %#d)</h3>' % (webutils.escapeElem(oGroup.sName), oGroup.idSchedGroup);
+ try:
+ (aoErrors, asMessages) = SchedulerBase.recreateQueue(self._oDb, self._oCurUser.uid, oGroup.idSchedGroup, 2);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageBody += '<p>SchedulerBase.recreateQueue threw an exception: %s</p>' \
+ % (webutils.escapeElem(str(oXcpt)),);
+ self._sPageBody += cgitb.html(sys.exc_info());
+ else:
+ if not aoErrors:
+ self._sPageBody += '<p>Successfully regenerated.</p>';
+ else:
+ for oError in aoErrors:
+ if oError[1] is None:
+ self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ ## @todo links.
+ #elif isinstance(oError[1], TestGroupData):
+ # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ #elif isinstance(oError[1], TestGroupCase):
+ # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),);
+ else:
+ self._sPageBody += '<p>%s. [Cannot link to %s]</p>' \
+ % (webutils.escapeElem(oError[0]), webutils.escapeElem(str(oError[1])),);
+ for sMsg in asMessages:
+ self._sPageBody += '<p>%s<p>\n' % (webutils.escapeElem(sMsg),);
+
+ # Remove leftovers from deleted scheduling groups.
+ self._sPageBody += '<h3>Cleanups</h3>\n';
+ cOrphans = SchedulerBase.cleanUpOrphanedQueues(self._oDb);
+ self._sPageBody += '<p>Removed %s orphaned (deleted) queue%s.<p>\n' % (cOrphans, '' if cOrphans == 1 else 's', );
+ else:
+ self._sPageBody = webutils.escapeElem('%s is a read only user and may not regenerate the scheduling queues!'
+ % (self._oCurUser.sUsername,));
+ return True;
+
+
+
+ #
+ # Test Config Category.
+ #
+
+ # Test Cases
+
+ def _actionTestCaseList(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCaseList;
+ return self._actionGenericListing(TestCaseLogic, WuiTestCaseList);
+
+ def _actionTestCaseAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormAdd(TestCaseDataEx, WuiTestCase);
+
+ def _actionTestCaseAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormAddPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList);
+
+ def _actionTestCaseClone(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormClone( TestCaseDataEx, WuiTestCase, 'idTestCase', 'idGenTestCase');
+
+ def _actionTestCaseDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormDetails(TestCaseDataEx, TestCaseLogic, WuiTestCase, 'idTestCase', 'idGenTestCase');
+
+ def _actionTestCaseEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormEdit(TestCaseDataEx, WuiTestCase, TestCaseDataEx.ksParam_idTestCase);
+
+ def _actionTestCaseEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic;
+ from testmanager.webui.wuiadmintestcase import WuiTestCase;
+ return self._actionGenericFormEditPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList);
+
+ def _actionTestCaseDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testcase import TestCaseData, TestCaseLogic;
+ return self._actionGenericDoRemove(TestCaseLogic, TestCaseData.ksParam_idTestCase, self.ksActionTestCaseList);
+
+ # Test Group actions
+
+ def _actionTestGroupList(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroupList;
+ return self._actionGenericListing(TestGroupLogic, WuiTestGroupList);
+ def _actionTestGroupAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormAdd(TestGroupDataEx, WuiTestGroup);
+ def _actionTestGroupAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormAddPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList);
+ def _actionTestGroupClone(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormClone(TestGroupDataEx, WuiTestGroup, 'idTestGroup');
+ def _actionTestGroupDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormDetails(TestGroupDataEx, TestGroupLogic, WuiTestGroup, 'idTestGroup');
+ def _actionTestGroupEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormEdit(TestGroupDataEx, WuiTestGroup, TestGroupDataEx.ksParam_idTestGroup);
+ def _actionTestGroupEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ from testmanager.webui.wuiadmintestgroup import WuiTestGroup;
+ return self._actionGenericFormEditPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList);
+ def _actionTestGroupDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic;
+ return self._actionGenericDoRemove(TestGroupLogic, TestGroupDataEx.ksParam_idTestGroup, self.ksActionTestGroupList)
+
+
+ # Global Resources
+
+ def _actionGlobalRsrcShowAll(self):
+ """ Action wrapper. """
+ from testmanager.core.globalresource import GlobalResourceLogic;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResourceList;
+ return self._actionGenericListing(GlobalResourceLogic, WuiGlobalResourceList);
+
+ def _actionGlobalRsrcShowAdd(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcAdd);
+
+ def _actionGlobalRsrcShowEdit(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcEdit);
+
+ def _actionGlobalRsrcAdd(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcAdd);
+
+ def _actionGlobalRsrcEdit(self):
+ """ Action wrapper. """
+ return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcEdit);
+
+ def _actionGlobalRsrcDel(self):
+ """ Action wrapper. """
+ from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic;
+ return self._actionGenericDoDelOld(GlobalResourceLogic, GlobalResourceData.ksParam_idGlobalRsrc,
+ self.ksActionGlobalRsrcShowAll);
+
+ def _actionGlobalRsrcShowAddEdit(self, sAction): # pylint: disable=invalid-name
+ """Show Global Resource creation or edit dialog"""
+ from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource;
+
+ oGlobalResourceLogic = GlobalResourceLogic(self._oDb)
+ if sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ idGlobalRsrc = self.getIntParam(GlobalResourceData.ksParam_idGlobalRsrc, iDefault = -1)
+ oData = oGlobalResourceLogic.getById(idGlobalRsrc)
+ else:
+ oData = GlobalResourceData()
+ oData.convertToParamNull()
+
+ self._checkForUnknownParameters()
+
+ oContent = WuiGlobalResource(oData)
+ (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction)
+
+ return True
+
+ def _actionGlobalRsrcAddEdit(self, sAction):
+ """Add or modify Global Resource record"""
+ from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+ from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource;
+
+ oData = GlobalResourceData()
+ oData.initFromParams(self, fStrict=True)
+
+ self._checkForUnknownParameters()
+
+ if self._oSrvGlue.getMethod() != 'POST':
+ raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),))
+
+ oGlobalResourceLogic = GlobalResourceLogic(self._oDb)
+ dErrors = oData.validateAndConvert(self._oDb);
+ if not dErrors:
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ oGlobalResourceLogic.addGlobalResource(self._oCurUser.uid, oData)
+ elif sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ idGlobalRsrc = self.getStringParam(GlobalResourceData.ksParam_idGlobalRsrc)
+ oGlobalResourceLogic.editGlobalResource(self._oCurUser.uid, idGlobalRsrc, oData)
+ else:
+ raise WuiException('Invalid parameter.')
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = self._sActionUrlBase + self.ksActionGlobalRsrcShowAll;
+ else:
+ oContent = WuiGlobalResource(oData)
+ (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction, dErrors=dErrors)
+
+ return True
+
+
+ #
+ # Build Source actions
+ #
+
+ def _actionBuildSrcList(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrcList;
+ return self._actionGenericListing(BuildSourceLogic, WuiAdminBuildSrcList);
+
+ def _actionBuildSrcAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormAdd(BuildSourceData, WuiAdminBuildSrc);
+
+ def _actionBuildSrcAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormAddPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList);
+
+ def _actionBuildSrcClone(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormClone( BuildSourceData, WuiAdminBuildSrc, 'idBuildSrc');
+
+ def _actionBuildSrcDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormDetails(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, 'idBuildSrc');
+
+ def _actionBuildSrcDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ return self._actionGenericDoRemove(BuildSourceLogic, BuildSourceData.ksParam_idBuildSrc, self.ksActionBuildSrcList);
+
+ def _actionBuildSrcEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormEdit(BuildSourceData, WuiAdminBuildSrc, BuildSourceData.ksParam_idBuildSrc);
+
+ def _actionBuildSrcEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+ from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc;
+ return self._actionGenericFormEditPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList);
+
+
+ #
+ # Build actions
+ #
+ def _actionBuildList(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuildList;
+ return self._actionGenericListing(BuildLogic, WuiAdminBuildList);
+
+ def _actionBuildAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormAdd(BuildData, WuiAdminBuild);
+
+ def _actionBuildAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormAddPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList);
+
+ def _actionBuildClone(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormClone( BuildData, WuiAdminBuild, 'idBuild');
+
+ def _actionBuildDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormDetails(BuildData, BuildLogic, WuiAdminBuild, 'idBuild');
+
+ def _actionBuildDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ return self._actionGenericDoRemove(BuildLogic, BuildData.ksParam_idBuild, self.ksActionBuildList);
+
+ def _actionBuildEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormEdit(BuildData, WuiAdminBuild, BuildData.ksParam_idBuild);
+
+ def _actionBuildEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildData, BuildLogic;
+ from testmanager.webui.wuiadminbuild import WuiAdminBuild;
+ return self._actionGenericFormEditPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList)
+
+
+ #
+ # Build Category actions
+ #
+ def _actionBuildCategoryList(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCatList;
+ return self._actionGenericListing(BuildCategoryLogic, WuiAdminBuildCatList);
+
+ def _actionBuildCategoryAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormAdd(BuildCategoryData, WuiAdminBuildCat);
+
+ def _actionBuildCategoryAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormAddPost(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat,
+ self.ksActionBuildCategoryList);
+
+ def _actionBuildCategoryClone(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormClone(BuildCategoryData, WuiAdminBuildCat, 'idBuildCategory');
+
+ def _actionBuildCategoryDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat;
+ return self._actionGenericFormDetails(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat, 'idBuildCategory');
+
+ def _actionBuildCategoryDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.build import BuildCategoryData, BuildCategoryLogic;
+ return self._actionGenericDoRemove(BuildCategoryLogic, BuildCategoryData.ksParam_idBuildCategory,
+ self.ksActionBuildCategoryList)
+
+
+ #
+ # Build Black List actions
+ #
+ def _actionBuildBlacklist(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminListOfBlacklistItems;
+ return self._actionGenericListing(BuildBlacklistLogic, WuiAdminListOfBlacklistItems);
+
+ def _actionBuildBlacklistAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormAdd(BuildBlacklistData, WuiAdminBuildBlacklist);
+
+ def _actionBuildBlacklistAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormAddPost(BuildBlacklistData, BuildBlacklistLogic,
+ WuiAdminBuildBlacklist, self.ksActionBuildBlacklist);
+
+ def _actionBuildBlacklistClone(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormClone(BuildBlacklistData, WuiAdminBuildBlacklist, 'idBlacklisting');
+
+ def _actionBuildBlacklistDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormDetails(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist, 'idBlacklisting');
+
+ def _actionBuildBlacklistDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ return self._actionGenericDoRemove(BuildBlacklistLogic, BuildBlacklistData.ksParam_idBlacklisting,
+ self.ksActionBuildBlacklist);
+
+ def _actionBuildBlacklistEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormEdit(BuildBlacklistData, WuiAdminBuildBlacklist, BuildBlacklistData.ksParam_idBlacklisting);
+
+ def _actionBuildBlacklistEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic;
+ from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist;
+ return self._actionGenericFormEditPost(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist,
+ self.ksActionBuildBlacklist)
+
+
+ #
+ # Failure Category actions
+ #
+ def _actionFailureCategoryList(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategoryList;
+ return self._actionGenericListing(FailureCategoryLogic, WuiFailureCategoryList);
+
+ def _actionFailureCategoryAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormAdd(FailureCategoryData, WuiFailureCategory);
+
+ def _actionFailureCategoryAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormAddPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory,
+ self.ksActionFailureCategoryList)
+
+ def _actionFailureCategoryDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormDetails(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory);
+
+
+ def _actionFailureCategoryDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ return self._actionGenericDoRemove(FailureCategoryLogic, FailureCategoryData.ksParam_idFailureCategory,
+ self.ksActionFailureCategoryList);
+
+ def _actionFailureCategoryEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormEdit(FailureCategoryData, WuiFailureCategory,
+ FailureCategoryData.ksParam_idFailureCategory);
+
+ def _actionFailureCategoryEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic;
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory;
+ return self._actionGenericFormEditPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory,
+ self.ksActionFailureCategoryList);
+
+ #
+ # Failure Reason actions
+ #
+ def _actionFailureReasonList(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList;
+ return self._actionGenericListing(FailureReasonLogic, WuiAdminFailureReasonList)
+
+ def _actionFailureReasonAdd(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormAdd(FailureReasonData, WuiAdminFailureReason);
+
+ def _actionFailureReasonAddPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormAddPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason,
+ self.ksActionFailureReasonList);
+
+ def _actionFailureReasonDetails(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormDetails(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason);
+
+ def _actionFailureReasonDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ return self._actionGenericDoRemove(FailureReasonLogic, FailureReasonData.ksParam_idFailureReason,
+ self.ksActionFailureReasonList);
+
+ def _actionFailureReasonEdit(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormEdit(FailureReasonData, WuiAdminFailureReason);
+
+
+ def _actionFailureReasonEditPost(self):
+ """ Action wrapper. """
+ from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+ from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason;
+ return self._actionGenericFormEditPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason,
+ self.ksActionFailureReasonList)
+
+
+ #
+ # Overrides.
+ #
+
+ def _generatePage(self):
+ """Override parent handler in order to change page titte"""
+ if self._sPageTitle is not None:
+ self._sPageTitle = 'Test Manager Admin - ' + self._sPageTitle
+
+ return WuiDispatcherBase._generatePage(self)
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py
new file mode 100755
index 00000000..17944ec3
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuild.py $
+
+"""
+Test Manager WUI - Builds.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiBuildLogLink, \
+ WuiSvnLinkWithTooltip;
+from testmanager.core.build import BuildData, BuildCategoryLogic;
+from testmanager.core.buildblacklist import BuildBlacklistData;
+from testmanager.core.db import isDbTimestampInfinity;
+
+
+class WuiAdminBuild(WuiFormContentBase):
+ """
+ WUI Build HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Build'
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Build - #%s' % (oData.idBuild,);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build - #%s' % (oData.idBuild,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'Build', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (BuildData.ksParam_idBuild, oData.idBuild, 'Build ID')
+ oForm.addTimestampRO(BuildData.ksParam_tsCreated, oData.tsCreated, 'Created')
+ oForm.addTimestampRO(BuildData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (BuildData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build category',
+ BuildCategoryLogic(self._oDisp.getDb()).fetchForCombo());
+
+ oForm.addInt (BuildData.ksParam_iRevision, oData.iRevision, 'Revision')
+ oForm.addText (BuildData.ksParam_sVersion, oData.sVersion, 'Version')
+ oForm.addWideText (BuildData.ksParam_sLogUrl, oData.sLogUrl, 'Log URL')
+ oForm.addWideText (BuildData.ksParam_sBinaries, oData.sBinaries, 'Binaries')
+ oForm.addCheckBox (BuildData.ksParam_fBinariesDeleted, oData.fBinariesDeleted, 'Binaries deleted')
+
+ oForm.addSubmit()
+ return True;
+
+
+class WuiAdminBuildList(WuiListContentBase):
+ """
+ WUI Admin Build List Content Generator.
+ """
+
+ ksResultsSortByOs_Darwin = ''
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Builds', sId = 'builds', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Product', 'Branch', 'Version',
+ 'Type', 'OS(es)', 'Author', 'Added',
+ 'Files', 'Action' ];
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ '', 'align="center"'];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry];
+
+ aoActions = [];
+ if oEntry.sLogUrl is not None:
+ aoActions.append(WuiBuildLogLink(oEntry.sLogUrl, 'Build Log'));
+
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistAdd,
+ BuildBlacklistData.ksParam_sProduct: oEntry.oCat.sProduct,
+ BuildBlacklistData.ksParam_sBranch: oEntry.oCat.sBranch,
+ BuildBlacklistData.ksParam_asTypes: oEntry.oCat.sType,
+ BuildBlacklistData.ksParam_asOsArches: oEntry.oCat.asOsArches,
+ BuildBlacklistData.ksParam_iFirstRevision: oEntry.iRevision,
+ BuildBlacklistData.ksParam_iLastRevision: oEntry.iRevision }
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Blacklist', WuiAdmin.ksScriptName, dParams),
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuild,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildClone,
+ BuildData.ksParam_idBuild: oEntry.idBuild,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildEdit,
+ BuildData.ksParam_idBuild: oEntry.idBuild }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDoRemove,
+ BuildData.ksParam_idBuild: oEntry.idBuild },
+ sConfirm = 'Are you sure you want to remove build #%d?' % (oEntry.idBuild,) ),
+ ];
+
+ return [ oEntry.idBuild,
+ oEntry.oCat.sProduct,
+ oEntry.oCat.sBranch,
+ WuiSvnLinkWithTooltip(oEntry.iRevision, oEntry.oCat.sRepository,
+ sName = '%s r%s' % (oEntry.sVersion, oEntry.iRevision,)),
+ oEntry.oCat.sType,
+ ' '.join(oEntry.oCat.asOsArches),
+ 'batch' if oEntry.uidAuthor is None else oEntry.uidAuthor,
+ self.formatTsShort(oEntry.tsCreated),
+ oEntry.sBinaries if not oEntry.fBinariesDeleted else '<Deleted>',
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py
new file mode 100755
index 00000000..d265259b
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildblacklist.py $
+
+"""
+Test Manager WUI - Build Blacklist.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink
+from testmanager.core.buildblacklist import BuildBlacklistData
+from testmanager.core.failurereason import FailureReasonLogic
+from testmanager.core.db import TMDatabaseConnection
+from testmanager.core import coreconsts
+
+
+class WuiAdminBuildBlacklist(WuiFormContentBase):
+ """
+ WUI Build Black List Form.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Build Blacklist Entry'
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Build Blacklist Entry'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Black';
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildBlacklist', oDisp, sTitle);
+
+ #
+ # Additional data.
+ #
+ self.asTypes = coreconsts.g_kasBuildTypesAll
+ self.asOsArches = coreconsts.g_kasOsDotCpusAll
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo()
+ if not aoFailureReasons:
+ from testmanager.webui.wuiadmin import WuiAdmin
+ raise WuiException('Please <a href="%s?%s=%s">add</a> some Failure Reasons first.'
+ % (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureReasonAdd));
+
+ asTypes = self.getListOfItems(self.asTypes, oData.asTypes)
+ asOsArches = self.getListOfItems(self.asOsArches, oData.asOsArches)
+
+ oForm.addIntRO (BuildBlacklistData.ksParam_idBlacklisting, oData.idBlacklisting, 'Blacklist item ID')
+ oForm.addTimestampRO(BuildBlacklistData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildBlacklistData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildBlacklistData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (BuildBlacklistData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason',
+ aoFailureReasons)
+
+ oForm.addText (BuildBlacklistData.ksParam_sProduct, oData.sProduct, 'Product')
+ oForm.addText (BuildBlacklistData.ksParam_sBranch, oData.sBranch, 'Branch')
+ oForm.addListOfTypes(BuildBlacklistData.ksParam_asTypes, asTypes, 'Build types')
+ oForm.addListOfOsArches(BuildBlacklistData.ksParam_asOsArches, asOsArches, 'Target architectures')
+ oForm.addInt (BuildBlacklistData.ksParam_iFirstRevision, oData.iFirstRevision, 'First revision')
+ oForm.addInt (BuildBlacklistData.ksParam_iLastRevision, oData.iLastRevision, 'Last revision (incl)')
+
+ oForm.addSubmit();
+
+ return True;
+
+
+class WuiAdminListOfBlacklistItems(WuiListContentBase):
+ """
+ WUI Admin Build Blacklist Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Build Blacklist', sId = 'buildsBlacklist',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Failure Reason',
+ 'Product', 'Branch', 'Type',
+ 'OS(es)', 'First Revision', 'Last Revision',
+ 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"' ]
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ sShortFailReason = FailureReasonLogic(TMDatabaseConnection()).getById(oEntry.idFailureReason).sShort
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDetails,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistEdit,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }),
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistClone,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting,
+ WuiAdmin.ksParamEffectiveDate: oEntry.tsEffective, }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDoRemove,
+ BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting },
+ sConfirm = 'Are you sure you want to remove black list entry #%d?' % (oEntry.idBlacklisting,)),
+ ];
+
+ return [ oEntry.idBlacklisting,
+ sShortFailReason,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ oEntry.asTypes,
+ oEntry.asOsArches,
+ oEntry.iFirstRevision,
+ oEntry.iLastRevision,
+ aoActions
+ ];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py
new file mode 100755
index 00000000..87d76fba
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildcategory.py $
+
+"""
+Test Manager WUI - Build categories.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiFormContentBase, WuiRawHtml, WuiTmLink;
+from testmanager.core.build import BuildCategoryData
+from testmanager.core import coreconsts;
+
+
+class WuiAdminBuildCatList(WuiListContentBase):
+ """
+ WUI Build Category List Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Build Categories', sId = 'buildcategories',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ([ 'ID', 'Product', 'Repository', 'Branch', 'Build Type', 'OS/Architectures', 'Actions' ]);
+ self._asColumnAttribs = (['align="right"', '', '', '', '', 'align="center"' ]);
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDetails,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryClone,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ WuiTmLink('Try Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDoRemove,
+ BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }),
+ ];
+
+ sHtml = '<ul class="tmshowall">\n';
+ for sOsArch in oEntry.asOsArches:
+ sHtml += ' <li class="tmshowall">%s</li>\n' % (webutils.escapeElem(sOsArch),);
+ sHtml += '</ul>\n'
+
+ return [ oEntry.idBuildCategory,
+ oEntry.sRepository,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ oEntry.sType,
+ WuiRawHtml(sHtml),
+ aoActions,
+ ];
+
+
+class WuiAdminBuildCat(WuiFormContentBase):
+ """
+ WUI Build Category Form Content Generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Create Build Category';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ assert False, 'not possible'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Category- %s' % (oData.idBuildCategory,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildCategory', oDisp, sTitle, fEditable = False);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( BuildCategoryData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build Category ID')
+ oForm.addText( BuildCategoryData.ksParam_sRepository, oData.sRepository, 'VCS repository name');
+ oForm.addText( BuildCategoryData.ksParam_sProduct, oData.sProduct, 'Product name')
+ oForm.addText( BuildCategoryData.ksParam_sBranch, oData.sBranch, 'Branch name')
+ oForm.addText( BuildCategoryData.ksParam_sType, oData.sType, 'Build type')
+
+ aoOsArches = [[sOsArch, sOsArch in oData.asOsArches, sOsArch] for sOsArch in coreconsts.g_kasOsDotCpusAll];
+ oForm.addListOfOsArches(BuildCategoryData.ksParam_asOsArches, aoOsArches, 'Target architectures');
+
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit();
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py
new file mode 100755
index 00000000..52d05f07
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminbuildsource.py $
+
+"""
+Test Manager WUI - Build Sources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core import coreconsts;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.buildsource import BuildSourceData;
+
+
+class WuiAdminBuildSrc(WuiFormContentBase):
+ """
+ WUI Build Sources HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, BuildSourceData);
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Build Source';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,);
+ WuiFormContentBase.__init__(self, oData, sMode, 'BuildSrc', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (BuildSourceData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build Source item ID')
+ oForm.addTimestampRO(BuildSourceData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(BuildSourceData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (BuildSourceData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText (BuildSourceData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText (BuildSourceData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addText (BuildSourceData.ksParam_sProduct, oData.sProduct, 'Product')
+ oForm.addText (BuildSourceData.ksParam_sBranch, oData.sBranch, 'Branch')
+ asTypes = self.getListOfItems(coreconsts.g_kasBuildTypesAll, oData.asTypes);
+ oForm.addListOfTypes(BuildSourceData.ksParam_asTypes, asTypes, 'Build types')
+ asOsArches = self.getListOfItems(coreconsts.g_kasOsDotCpusAll, oData.asOsArches);
+ oForm.addListOfOsArches(BuildSourceData.ksParam_asOsArches, asOsArches, 'Target architectures')
+ oForm.addInt (BuildSourceData.ksParam_iFirstRevision, oData.iFirstRevision, 'Starting from revision')
+ oForm.addInt (BuildSourceData.ksParam_iLastRevision, oData.iLastRevision, 'Ending by revision')
+ oForm.addLong (BuildSourceData.ksParam_cSecMaxAge,
+ utils.formatIntervalSeconds2(oData.cSecMaxAge) if oData.cSecMaxAge not in [-1, '', None] else '',
+ 'Max age in seconds');
+ oForm.addSubmit();
+ return True;
+
+class WuiAdminBuildSrcList(WuiListContentBase):
+ """
+ WUI Build Source content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered Build Sources', sId = 'build sources',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['ID', 'Name', 'Description', 'Product',
+ 'Branch', 'Build Types', 'OS/ARCH', 'First Revision', 'Last Revision', 'Max Age',
+ 'Actions' ];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="left"', 'align="left"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"' ];
+
+ def _getSubList(self, aList):
+ """
+ Convert pythonic list into HTML list
+ """
+ if aList not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in aList:
+ sHtml += ' <li class="tmshowall">%s</a></li>\n' % (webutils.escapeElem(sTmp),);
+ sHtml += ' </ul>\n';
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">Any</li></ul>\n';
+
+ return WuiRawHtml(sHtml);
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcClone,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }),
+ ];
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcEdit,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDoRemove,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc },
+ sConfirm = 'Are you sure you want to remove build source #%d?' % (oEntry.idBuildSrc,) )
+ ];
+
+ return [ oEntry.idBuildSrc,
+ oEntry.sName,
+ oEntry.sDescription,
+ oEntry.sProduct,
+ oEntry.sBranch,
+ self._getSubList(oEntry.asTypes),
+ self._getSubList(oEntry.asOsArches),
+ oEntry.iFirstRevision,
+ oEntry.iLastRevision,
+ utils.formatIntervalSeconds2(oEntry.cSecMaxAge) if oEntry.cSecMaxAge is not None else None,
+ aoActions,
+ ]
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py
new file mode 100755
index 00000000..d02931de
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminfailurecategory.py $
+
+"""
+Test Manager WUI - Failure Categories Web content generator.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiListContentBase, WuiTmLink;
+from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList;
+from testmanager.core.failurecategory import FailureCategoryData;
+from testmanager.core.failurereason import FailureReasonLogic;
+
+
+class WuiFailureReasonCategoryLink(WuiTmLink):
+ """ Link to a failure category. """
+ def __init__(self, idFailureCategory, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails,
+ FailureCategoryData.ksParam_idFailureCategory: idFailureCategory, },
+ fBracketed = fBracketed,
+ sTitle = sTitle);
+ self.idFailureCategory = idFailureCategory;
+
+
+
+class WuiFailureCategory(WuiFormContentBase):
+ """
+ WUI Failure Category HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ sTitle = 'Failure Category';
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add ' + sTitle;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit ' + sTitle;
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+
+ WuiFormContentBase.__init__(self, oData, sMode, 'FailureCategory', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ oForm.addIntRO (FailureCategoryData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category ID')
+ oForm.addTimestampRO(FailureCategoryData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(FailureCategoryData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (FailureCategoryData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText (FailureCategoryData.ksParam_sShort, oData.sShort, 'Short Description')
+ oForm.addText (FailureCategoryData.ksParam_sFull, oData.sFull, 'Full Description')
+
+ oForm.addSubmit()
+
+ return True;
+
+ def _generatePostFormContent(self, oData):
+ """
+ Adds a table with the category members below the form.
+ """
+ if oData.idFailureCategory is not None and oData.idFailureCategory >= 0:
+ oLogic = FailureReasonLogic(self._oDisp.getDb());
+ tsNow = self._oDisp.getNow();
+ cMax = 4096;
+ aoEntries = oLogic.fetchForListingInCategory(0, cMax, tsNow, oData.idFailureCategory)
+ if aoEntries:
+ oList = WuiAdminFailureReasonList(aoEntries, 0, cMax, tsNow, fnDPrint = None, oDisp = self._oDisp);
+ return [ [ 'Members', oList.show(fShowNavigation = False)[1]], ];
+ return [];
+
+
+
+class WuiFailureCategoryList(WuiListContentBase):
+ """
+ WUI Admin Failure Category Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Failure Categories', sId = 'failureCategories',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Short Description', 'Full Description', 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryEdit,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDoRemove,
+ FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory },
+ sConfirm = 'Do you really want to remove failure cateogry #%d?' % (oEntry.idFailureCategory,)),
+ ]
+
+ return [ oEntry.idFailureCategory,
+ oEntry.sShort,
+ oEntry.sFull,
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py
new file mode 100755
index 00000000..5fa76227
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminfailurereason.py $
+
+"""
+Test Manager WUI - Failure Reasons Web content generator.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink;
+from testmanager.core.failurereason import FailureReasonData;
+from testmanager.core.failurecategory import FailureCategoryLogic;
+from testmanager.core.db import TMDatabaseConnection;
+
+
+
+class WuiFailureReasonDetailsLink(WuiTmLink):
+ """ Short link to a failure reason. """
+ def __init__(self, idFailureReason, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: idFailureReason, },
+ fBracketed = fBracketed);
+ self.idFailureReason = idFailureReason;
+
+
+
+class WuiFailureReasonAddLink(WuiTmLink):
+ """ Link for adding a failure reason. """
+ def __init__(self, sName = WuiContentBase.ksShortAddLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiAdmin.ksScriptName,
+ dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonAdd, },
+ fBracketed = fBracketed);
+
+
+
+class WuiAdminFailureReason(WuiFormContentBase):
+ """
+ WUI Failure Reason HTML content generator.
+ """
+
+ def __init__(self, oFailureReasonData, sMode, oDisp):
+ """
+ Prepare & initialize parent
+ """
+
+ sTitle = 'Failure Reason';
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add' + sTitle;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit' + sTitle;
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+
+ WuiFormContentBase.__init__(self, oFailureReasonData, sMode, 'FailureReason', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ """
+ Construct an HTML form
+ """
+
+ aoFailureCategories = FailureCategoryLogic(TMDatabaseConnection()).getFailureCategoriesForCombo()
+ if not aoFailureCategories:
+ from testmanager.webui.wuiadmin import WuiAdmin
+ sExceptionMsg = 'Please <a href="%s?%s=%s">add</a> Failure Category first.' % \
+ (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureCategoryAdd)
+
+ raise WuiException(sExceptionMsg)
+
+ oForm.addIntRO (FailureReasonData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason ID')
+ oForm.addTimestampRO (FailureReasonData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO (FailureReasonData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (FailureReasonData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+
+ oForm.addComboBox (FailureReasonData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category',
+ aoFailureCategories)
+
+ oForm.addText (FailureReasonData.ksParam_sShort, oData.sShort, 'Short Description')
+ oForm.addText (FailureReasonData.ksParam_sFull, oData.sFull, 'Full Description')
+ oForm.addInt (FailureReasonData.ksParam_iTicket, oData.iTicket, 'Ticket Number')
+ oForm.addMultilineText(FailureReasonData.ksParam_asUrls, oData.asUrls, 'Other URLs to reports '
+ 'or discussions of the '
+ 'observed symptoms')
+ oForm.addSubmit()
+
+ return True
+
+
+class WuiAdminFailureReasonList(WuiListContentBase):
+ """
+ WUI Admin Failure Reasons Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Failure Reasons', sId = 'failureReasons',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Category', 'Short Description',
+ 'Full Description', 'Ticket', 'External References', 'Actions' ]
+
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"',
+ 'align="center"',' align="center"', 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ from testmanager.webui.wuiadminfailurecategory import WuiFailureReasonCategoryLink;
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonEdit,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDoRemove,
+ FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason },
+ sConfirm = 'Are you sure you want to remove failure reason #%d?' % (oEntry.idFailureReason,)),
+ ];
+
+ return [ oEntry.idFailureReason,
+ WuiFailureReasonCategoryLink(oEntry.idFailureCategory, sName = oEntry.oCategory.sShort, fBracketed = False),
+ oEntry.sShort,
+ oEntry.sFull,
+ oEntry.iTicket,
+ oEntry.asUrls,
+ aoActions,
+ ]
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py
new file mode 100755
index 00000000..b748ea2f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminglobalrsrc.py $
+
+"""
+Test Manager WUI - Global resources.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuibase import WuiException
+from testmanager.webui.wuicontentbase import WuiContentBase
+from testmanager.webui.wuihlpform import WuiHlpForm
+from testmanager.core.globalresource import GlobalResourceData
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink
+
+
+class WuiGlobalResource(WuiContentBase):
+ """
+ WUI global resources content generator.
+ """
+
+ def __init__(self, oData, fnDPrint = None):
+ """
+ Do necessary initializations
+ """
+ WuiContentBase.__init__(self, fnDPrint)
+ self._oData = oData
+
+ def showAddModifyPage(self, sAction, dErrors = None):
+ """
+ Render add global resource HTML form.
+ """
+ from testmanager.webui.wuiadmin import WuiAdmin
+
+ sFormActionUrl = '%s?%s=%s' % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, sAction)
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ sTitle = 'Add Global Resource'
+ elif sAction == WuiAdmin.ksActionGlobalRsrcEdit:
+ sTitle = 'Modify Global Resource'
+ sFormActionUrl += '&%s=%s' % (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc)
+ else:
+ raise WuiException('Invalid paraemter "%s"' % (sAction,))
+
+ oForm = WuiHlpForm('globalresourceform',
+ sFormActionUrl,
+ dErrors if dErrors is not None else {})
+
+ if sAction == WuiAdmin.ksActionGlobalRsrcAdd:
+ oForm.addIntRO (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc, 'Global Resource ID')
+ oForm.addTimestampRO(GlobalResourceData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(GlobalResourceData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (GlobalResourceData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID')
+ oForm.addText (GlobalResourceData.ksParam_sName, self._oData.sName, 'Name')
+ oForm.addText (GlobalResourceData.ksParam_sDescription, self._oData.sDescription, 'Description')
+ oForm.addCheckBox (GlobalResourceData.ksParam_fEnabled, self._oData.fEnabled, 'Enabled')
+
+ oForm.addSubmit('Submit')
+
+ return (sTitle, oForm.finalize())
+
+
+class WuiGlobalResourceList(WuiListContentBase):
+ """
+ WUI Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Global Resources', sId = 'globalResources',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = ['ID', 'Name', 'Description', 'Enabled', 'Actions' ]
+ self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"']
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry]
+
+ aoActions = [ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcShowEdit,
+ GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc }),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcDel,
+ GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc },
+ sConfirm = 'Are you sure you want to remove global resource #%d?' % (oEntry.idGlobalRsrc,)),
+ ];
+
+ return [ oEntry.idGlobalRsrc,
+ oEntry.sName,
+ oEntry.sDescription,
+ oEntry.fEnabled,
+ aoActions, ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py
new file mode 100755
index 00000000..e7a43717
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminschedgroup.py $
+
+"""
+Test Manager WUI - Scheduling groups.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.schedgroup import SchedGroupData, SchedGroupDataEx;
+from testmanager.core.testgroup import TestGroupData, TestGroupLogic;
+from testmanager.core.testbox import TestBoxLogic;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink;
+
+
+class WuiSchedGroup(WuiFormContentBase):
+ """
+ WUI Scheduling Groups HTML content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, SchedGroupData);
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Scheduling Group';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Scheduling Group'
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Scheduling Group';
+ WuiFormContentBase.__init__(self, oData, sMode, 'SchedGroup', oDisp, sTitle);
+
+ # Read additional bits form the DB, unless we're in
+ if sMode != WuiFormContentBase.ksMode_Show:
+ self._aoAllRelevantTestGroups = TestGroupLogic(oDisp.getDb()).getAll();
+ self._aoAllRelevantTestBoxes = TestBoxLogic(oDisp.getDb()).getAll();
+ else:
+ self._aoAllRelevantTestGroups = [oMember.oTestGroup for oMember in oData.aoMembers];
+ self._aoAllRelevantTestBoxes = [oMember.oTestBox for oMember in oData.aoTestBoxes];
+
+ def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool
+ """
+ Construct an HTML form
+ """
+
+ oForm.addIntRO( SchedGroupData.ksParam_idSchedGroup, oData.idSchedGroup, 'ID')
+ oForm.addTimestampRO(SchedGroupData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(SchedGroupData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO( SchedGroupData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addText( SchedGroupData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText( SchedGroupData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addCheckBox( SchedGroupData.ksParam_fEnabled, oData.fEnabled, 'Enabled')
+
+ oForm.addComboBox( SchedGroupData.ksParam_enmScheduler, oData.enmScheduler, 'Scheduler type',
+ SchedGroupData.kasSchedulerDesc)
+
+ aoBuildSrcIds = BuildSourceLogic(self._oDisp.getDb()).fetchForCombo();
+ oForm.addComboBox( SchedGroupData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build source', aoBuildSrcIds);
+ oForm.addComboBox( SchedGroupData.ksParam_idBuildSrcTestSuite,
+ oData.idBuildSrcTestSuite, 'Test suite', aoBuildSrcIds);
+
+ oForm.addListOfSchedGroupMembers(SchedGroupDataEx.ksParam_aoMembers,
+ oData.aoMembers, self._aoAllRelevantTestGroups, 'Test groups',
+ oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addListOfSchedGroupBoxes(SchedGroupDataEx.ksParam_aoTestBoxes,
+ oData.aoTestBoxes, self._aoAllRelevantTestBoxes, 'Test boxes',
+ oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addMultilineText(SchedGroupData.ksParam_sComment, oData.sComment, 'Comment');
+ oForm.addSubmit()
+
+ return True;
+
+class WuiAdminSchedGroupList(WuiListContentBase):
+ """
+ Content generator for the schedule group listing.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered Scheduling Groups', sId = 'schedgroups',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._asColumnHeaders = [
+ 'ID', 'Name', 'Enabled', 'Scheduler Type',
+ 'Build Source', 'Validation Kit Source', 'Test Groups', 'TestBoxes', 'Note', 'Actions',
+ ];
+
+ self._asColumnAttribs = [
+ 'align="right"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', '', '', 'align="center"', 'align="center"',
+ ];
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+ from testmanager.webui.wuiadmin import WuiAdmin
+ oEntry = self._aoEntries[iEntry] # type: SchedGroupDataEx
+
+ oBuildSrc = None;
+ if oEntry.idBuildSrc is not None:
+ oBuildSrc = WuiTmLink(oEntry.oBuildSrc.sName if oEntry.oBuildSrc else str(oEntry.idBuildSrc),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc, });
+
+ oValidationKitSrc = None;
+ if oEntry.idBuildSrcTestSuite is not None:
+ oValidationKitSrc = WuiTmLink(oEntry.oBuildSrcValidationKit.sName if oEntry.oBuildSrcValidationKit
+ else str(oEntry.idBuildSrcTestSuite),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails,
+ BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrcTestSuite, });
+
+ # Test groups
+ aoMembers = [];
+ for oMember in oEntry.aoMembers:
+ aoMembers.append(WuiTmLink(oMember.oTestGroup.sName, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oMember.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, },
+ sTitle = '#%s' % (oMember.idTestGroup,) if oMember.oTestGroup.sDescription is None
+ else '#%s - %s' % (oMember.idTestGroup, oMember.oTestGroup.sDescription,) ));
+
+ # Test boxes.
+ aoTestBoxes = [];
+ for oRelation in oEntry.aoTestBoxes:
+ oTestBox = oRelation.oTestBox;
+ if oTestBox:
+ aoTestBoxes.append(WuiTestBoxDetailsLink(oTestBox, fBracketed = True, tsNow = self._tsEffectiveDate));
+ else:
+ aoTestBoxes.append(WuiRawHtml('#%s' % (oRelation.idTestBox,)));
+
+ # Actions
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDetails,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ),];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup } ));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupClone,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ));
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDoRemove,
+ SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup },
+ sConfirm = 'Are you sure you want to remove scheduling group #%d?'
+ % (oEntry.idSchedGroup,)));
+
+ return [
+ oEntry.idSchedGroup,
+ oEntry.sName,
+ oEntry.fEnabled,
+ oEntry.enmScheduler,
+ oBuildSrc,
+ oValidationKitSrc,
+ aoMembers,
+ aoTestBoxes,
+ self._formatCommentCell(oEntry.sComment),
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py
new file mode 100755
index 00000000..88a3475a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# "$Id: wuiadminschedqueue.py $"
+
+"""
+Test Manager WUI - Admin - Scheduling Queue.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports
+from testmanager.webui.wuicontentbase import WuiListContentBase
+
+
+class WuiAdminSchedQueueList(WuiListContentBase):
+ """
+ WUI Scheduling Queue Content Generator.
+ """
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ tsEffective = None; # Not relevant, no history on the scheduling queue.
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'Scheduling Queue',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns,
+ fTimeNavigation = False);
+ self._asColumnHeaders = [
+ 'Last Run', 'Scheduling Group', 'Test Group', 'Test Case', 'Config State', 'Item ID'
+ ];
+ self._asColumnAttribs = [
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"'
+ ];
+ self._iPrevPerSchedGroupRowNumber = 0;
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry
+ sState = 'up-to-date' if oEntry.fUpToDate else 'outdated';
+ return [ oEntry.tsLastScheduled, oEntry.sSchedGroup, oEntry.sTestGroup, oEntry.sTestCase, sState, oEntry.idItem ];
+
+ def _formatListEntryHtml(self, iEntry):
+ sHtml = WuiListContentBase._formatListEntryHtml(self, iEntry);
+
+ # Insert separator row?
+ if iEntry < len(self._aoEntries):
+ oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry
+ if oEntry.iPerSchedGroupRowNumber != self._iPrevPerSchedGroupRowNumber:
+ if iEntry > 0 and iEntry + 1 < min(len(self._aoEntries), self._cItemsPerPage):
+ sHtml += '<tr class="tmseparator"><td colspan=%s> </td></tr>\n' % (len(self._asColumnHeaders),);
+ self._iPrevPerSchedGroupRowNumber = oEntry.iPerSchedGroupRowNumber;
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
new file mode 100755
index 00000000..94057524
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py
@@ -0,0 +1,447 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemchangelog.py $
+
+"""
+Test Manager WUI - Admin - System changelog.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+from common import webutils;
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiHtmlKeeper, WuiAdminLink, \
+ WuiMainLink, WuiElementText, WuiHtmlBase;
+
+from testmanager.core.base import AttributeChangeEntryPre;
+from testmanager.core.buildblacklist import BuildBlacklistLogic, BuildBlacklistData;
+from testmanager.core.build import BuildLogic, BuildData;
+from testmanager.core.buildsource import BuildSourceLogic, BuildSourceData;
+from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData;
+from testmanager.core.failurecategory import FailureCategoryLogic, FailureCategoryData;
+from testmanager.core.failurereason import FailureReasonLogic, FailureReasonData;
+from testmanager.core.systemlog import SystemLogData;
+from testmanager.core.systemchangelog import SystemChangelogLogic;
+from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData;
+from testmanager.core.testbox import TestBoxLogic, TestBoxData;
+from testmanager.core.testcase import TestCaseLogic, TestCaseData;
+from testmanager.core.testgroup import TestGroupLogic, TestGroupData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.useraccount import UserAccountLogic, UserAccountData;
+
+
+class WuiAdminSystemChangelogList(WuiListContentBase):
+ """
+ WUI System Changelog Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, cDaysBack, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Changelog',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = [ 'When', 'User', 'Event', 'Details' ];
+ self._asColumnAttribs = [ 'align="center"', 'align="center"', '', '' ];
+ self._oBuildBlacklistLogic = BuildBlacklistLogic(oDisp.getDb());
+ self._oBuildLogic = BuildLogic(oDisp.getDb());
+ self._oBuildSourceLogic = BuildSourceLogic(oDisp.getDb());
+ self._oFailureCategoryLogic = FailureCategoryLogic(oDisp.getDb());
+ self._oFailureReasonLogic = FailureReasonLogic(oDisp.getDb());
+ self._oGlobalResourceLogic = GlobalResourceLogic(oDisp.getDb());
+ self._oSchedGroupLogic = SchedGroupLogic(oDisp.getDb());
+ self._oTestBoxLogic = TestBoxLogic(oDisp.getDb());
+ self._oTestCaseLogic = TestCaseLogic(oDisp.getDb());
+ self._oTestGroupLogic = TestGroupLogic(oDisp.getDb());
+ self._oUserAccountLogic = UserAccountLogic(oDisp.getDb());
+ self._sPrevDate = '';
+ _ = cDaysBack;
+
+ # oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ def _createBlacklistingDetailsLink(self, idBlacklisting, tsEffective):
+ """ Creates a link to the build source details. """
+ oBlacklisting = self._oBuildBlacklistLogic.cachedLookup(idBlacklisting);
+ if oBlacklisting is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink('Blacklisting #%u' % (oBlacklisting.idBlacklisting,),
+ WuiAdmin.ksActionBuildBlacklistDetails, tsEffective,
+ { BuildBlacklistData.ksParam_idBlacklisting: oBlacklisting.idBlacklisting },
+ fBracketed = False);
+ return WuiElementText('[blacklisting #%u not found]' % (idBlacklisting,));
+
+ def _createBuildDetailsLink(self, idBuild, tsEffective):
+ """ Creates a link to the build details. """
+ oBuild = self._oBuildLogic.cachedLookup(idBuild);
+ if oBuild is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink('%s %sr%u' % ( oBuild.oCat.sProduct, oBuild.sVersion, oBuild.iRevision),
+ WuiAdmin.ksActionBuildDetails, tsEffective,
+ { BuildData.ksParam_idBuild: oBuild.idBuild },
+ fBracketed = False,
+ sTitle = 'build #%u for %s, type %s'
+ % (oBuild.idBuild, ' & '.join(oBuild.oCat.asOsArches), oBuild.oCat.sType));
+ return WuiElementText('[build #%u not found]' % (idBuild,));
+
+ def _createBuildSourceDetailsLink(self, idBuildSrc, tsEffective):
+ """ Creates a link to the build source details. """
+ oBuildSource = self._oBuildSourceLogic.cachedLookup(idBuildSrc);
+ if oBuildSource is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oBuildSource.sName, WuiAdmin.ksActionBuildSrcDetails, tsEffective,
+ { BuildSourceData.ksParam_idBuildSrc: oBuildSource.idBuildSrc },
+ fBracketed = False,
+ sTitle = 'Build source #%u' % (oBuildSource.idBuildSrc,));
+ return WuiElementText('[build source #%u not found]' % (idBuildSrc,));
+
+ def _createFailureCategoryDetailsLink(self, idFailureCategory, tsEffective):
+ """ Creates a link to the failure category details. """
+ oFailureCategory = self._oFailureCategoryLogic.cachedLookup(idFailureCategory);
+ if oFailureCategory is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oFailureCategory.sShort, WuiAdmin.ksActionFailureCategoryDetails, tsEffective,
+ { FailureCategoryData.ksParam_idFailureCategory: oFailureCategory.idFailureCategory },
+ fBracketed = False,
+ sTitle = 'Failure category #%u' % (oFailureCategory.idFailureCategory,));
+ return WuiElementText('[failure category #%u not found]' % (idFailureCategory,));
+
+ def _createFailureReasonDetailsLink(self, idFailureReason, tsEffective):
+ """ Creates a link to the failure reason details. """
+ oFailureReason = self._oFailureReasonLogic.cachedLookup(idFailureReason);
+ if oFailureReason is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oFailureReason.sShort, WuiAdmin.ksActionFailureReasonDetails, tsEffective,
+ { FailureReasonData.ksParam_idFailureReason: oFailureReason.idFailureReason },
+ fBracketed = False,
+ sTitle = 'Failure reason #%u, category %s'
+ % (oFailureReason.idFailureReason, oFailureReason.oCategory.sShort));
+ return WuiElementText('[failure reason #%u not found]' % (idFailureReason,));
+
+ def _createGlobalResourceDetailsLink(self, idGlobalRsrc, tsEffective):
+ """ Creates a link to the global resource details. """
+ oGlobalResource = self._oGlobalResourceLogic.cachedLookup(idGlobalRsrc);
+ if oGlobalResource is not None:
+ return WuiAdminLink(oGlobalResource.sName, '@todo', tsEffective,
+ { GlobalResourceData.ksParam_idGlobalRsrc: oGlobalResource.idGlobalRsrc },
+ fBracketed = False,
+ sTitle = 'Global resource #%u' % (oGlobalResource.idGlobalRsrc,));
+ return WuiElementText('[global resource #%u not found]' % (idGlobalRsrc,));
+
+ def _createSchedGroupDetailsLink(self, idSchedGroup, tsEffective):
+ """ Creates a link to the scheduling group details. """
+ oSchedGroup = self._oSchedGroupLogic.cachedLookup(idSchedGroup);
+ if oSchedGroup is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oSchedGroup.sName, WuiAdmin.ksActionSchedGroupDetails, tsEffective,
+ { SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup },
+ fBracketed = False,
+ sTitle = 'Scheduling group #%u' % (oSchedGroup.idSchedGroup,));
+ return WuiElementText('[scheduling group #%u not found]' % (idSchedGroup,));
+
+ def _createTestBoxDetailsLink(self, idTestBox, tsEffective):
+ """ Creates a link to the testbox details. """
+ oTestBox = self._oTestBoxLogic.cachedLookup(idTestBox);
+ if oTestBox is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestBox.sName, WuiAdmin.ksActionTestBoxDetails, tsEffective,
+ { TestBoxData.ksParam_idTestBox: oTestBox.idTestBox },
+ fBracketed = False, sTitle = 'Testbox #%u' % (oTestBox.idTestBox,));
+ return WuiElementText('[testbox #%u not found]' % (idTestBox,));
+
+ def _createTestCaseDetailsLink(self, idTestCase, tsEffective):
+ """ Creates a link to the test case details. """
+ oTestCase = self._oTestCaseLogic.cachedLookup(idTestCase);
+ if oTestCase is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestCase.sName, WuiAdmin.ksActionTestCaseDetails, tsEffective,
+ { TestCaseData.ksParam_idTestCase: oTestCase.idTestCase },
+ fBracketed = False, sTitle = 'Test case #%u' % (oTestCase.idTestCase,));
+ return WuiElementText('[test case #%u not found]' % (idTestCase,));
+
+ def _createTestGroupDetailsLink(self, idTestGroup, tsEffective):
+ """ Creates a link to the test group details. """
+ oTestGroup = self._oTestGroupLogic.cachedLookup(idTestGroup);
+ if oTestGroup is not None:
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ return WuiAdminLink(oTestGroup.sName, WuiAdmin.ksActionTestGroupDetails, tsEffective,
+ { TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup },
+ fBracketed = False, sTitle = 'Test group #%u' % (oTestGroup.idTestGroup,));
+ return WuiElementText('[test group #%u not found]' % (idTestGroup,));
+
+ def _createTestSetResultsDetailsLink(self, idTestSet, tsEffective):
+ """ Creates a link to the test set results. """
+ _ = tsEffective;
+ from testmanager.webui.wuimain import WuiMain;
+ return WuiMainLink('test set #%u' % idTestSet, WuiMain.ksActionTestSetDetails,
+ { TestSetData.ksParam_idTestSet: idTestSet }, fBracketed = False);
+
+ def _createTestSetDetailsLinkByResult(self, idTestResult, tsEffective):
+ """ Creates a link to the test set results. """
+ _ = tsEffective;
+ from testmanager.webui.wuimain import WuiMain;
+ return WuiMainLink('test result #%u' % idTestResult, WuiMain.ksActionTestSetDetailsFromResult,
+ { TestSetData.ksParam_idTestResult: idTestResult }, fBracketed = False);
+
+ def _createUserAccountDetailsLink(self, uid, tsEffective):
+ """ Creates a link to the user account details. """
+ oUser = self._oUserAccountLogic.cachedLookup(uid);
+ if oUser is not None:
+ return WuiAdminLink(oUser.sUsername, '@todo', tsEffective, { UserAccountData.ksParam_uid: oUser.uid },
+ fBracketed = False, sTitle = '%s (#%u)' % (oUser.sFullName, oUser.uid));
+ return WuiElementText('[user #%u not found]' % (uid,));
+
+ def _formatDescGeneric(self, sDesc, oEntry):
+ """
+ Generically format system log the description.
+ """
+ oRet = WuiHtmlKeeper();
+ asWords = sDesc.split();
+ for sWord in asWords:
+ offEqual = sWord.find('=');
+ if offEqual > 0:
+ sKey = sWord[:offEqual];
+ try: idValue = int(sWord[offEqual+1:].rstrip('.,'));
+ except: pass;
+ else:
+ if sKey == 'idTestSet':
+ oRet.append(self._createTestSetResultsDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+ if sKey == 'idTestBox':
+ oRet.append(self._createTestBoxDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+ if sKey == 'idSchedGroup':
+ oRet.append(self._createSchedGroupDetailsLink(idValue, oEntry.tsEffective));
+ continue;
+
+ oRet.append(WuiElementText(sWord));
+ return oRet;
+
+ def _formatListEntryHtml(self, iEntry): # pylint: disable=too-many-statements
+ """
+ Overridden parent method.
+ """
+ oEntry = self._aoEntries[iEntry];
+ sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
+ sHtml = u'';
+
+ #
+ # Format the timestamp.
+ #
+ sDate = self.formatTsShort(oEntry.tsEffective);
+ if sDate[:10] != self._sPrevDate:
+ self._sPrevDate = sDate[:10];
+ sHtml += ' <tr class="%s tmdaterow" align="left"><td colspan="7">%s</td></tr>\n' % (sRowClass, sDate[:10],);
+ sDate = sDate[11:]
+
+ #
+ # System log events.
+ # pylint: disable=redefined-variable-type
+ #
+ aoChanges = None;
+ if oEntry.sEvent == SystemLogData.ksEvent_CmdNacked:
+ sEvent = 'Command not acknowleged';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown:
+ sEvent = 'Unknown testbox';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_TestSetAbandoned:
+ sEvent = 'Abandoned ' if oEntry.sDesc.startswith('idTestSet') else 'Abandoned test set';
+ oDetails = self._formatDescGeneric(oEntry.sDesc, oEntry);
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown:
+ sEvent = 'Unknown user account';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_XmlResultMalformed:
+ sEvent = 'Malformed XML result';
+ oDetails = oEntry.sDesc;
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_SchedQueueRecreate:
+ sEvent = 'Recreating scheduling queue';
+ asWords = oEntry.sDesc.split();
+ if len(asWords) > 3 and asWords[0] == 'User' and asWords[1][0] == '#':
+ try: idAuthor = int(asWords[1][1:]);
+ except: pass;
+ else:
+ oEntry.oAuthor = self._oUserAccountLogic.cachedLookup(idAuthor);
+ if oEntry.oAuthor is not None:
+ i = 2;
+ if asWords[i] == 'recreated': i += 1;
+ oEntry.sDesc = ' '.join(asWords[i:]);
+ oDetails = self._formatDescGeneric(oEntry.sDesc.replace('sched queue #', 'for scheduling group idSchedGroup='),
+ oEntry);
+ #
+ # System changelog events.
+ #
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Blacklisting:
+ sEvent = 'Modified blacklisting';
+ oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Build:
+ sEvent = 'Modified build';
+ oDetails = self._createBuildDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_BuildSource:
+ sEvent = 'Modified build source';
+ oDetails = self._createBuildSourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_GlobalRsrc:
+ sEvent = 'Modified global resource';
+ oDetails = self._createGlobalResourceDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureCategory:
+ sEvent = 'Modified failure category';
+ oDetails = self._createFailureCategoryDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oFailureCategoryLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureReason:
+ sEvent = 'Modified failure reason';
+ oDetails = self._createFailureReasonDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oFailureReasonLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_SchedGroup:
+ sEvent = 'Modified scheduling group';
+ oDetails = self._createSchedGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestBox:
+ sEvent = 'Modified testbox';
+ oDetails = self._createTestBoxDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oTestBoxLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestCase:
+ sEvent = 'Modified test case';
+ oDetails = self._createTestCaseDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+ (aoChanges, _) = self._oTestCaseLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestGroup:
+ sEvent = 'Modified test group';
+ oDetails = self._createTestGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestResult:
+ sEvent = 'Modified test failure reason';
+ oDetails = self._createTestSetDetailsLinkByResult(oEntry.idWhat, oEntry.tsEffective);
+
+ elif oEntry.sEvent == SystemChangelogLogic.ksWhat_User:
+ sEvent = 'Modified user account';
+ oDetails = self._createUserAccountDetailsLink(oEntry.idWhat, oEntry.tsEffective);
+
+ else:
+ sEvent = '%s(%s)' % (oEntry.sEvent, oEntry.idWhat,);
+ oDetails = '!Unknown event!' + (oEntry.sDesc if oEntry.sDesc else '');
+
+ #
+ # Do the formatting.
+ #
+
+ if aoChanges:
+ oChangeEntry = aoChanges[0];
+ cAttribsChanged = len(oChangeEntry.aoChanges) + 1;
+ if oChangeEntry.oOldRaw is None and sEvent.startswith('Modified '):
+ sEvent = 'Created ' + sEvent[9:];
+
+ else:
+ oChangeEntry = None;
+ cAttribsChanged = -1;
+
+ sHtml += u' <tr class="%s">\n' \
+ u' <td rowspan="%d" align="center" >%s</td>\n' \
+ u' <td rowspan="%d" align="center" >%s</td>\n' \
+ u' <td colspan="5" class="%s%s">%s %s</td>\n' \
+ u' </tr>\n' \
+ % ( sRowClass,
+ 1 + cAttribsChanged + 1, sDate,
+ 1 + cAttribsChanged + 1, webutils.escapeElem(oEntry.oAuthor.sUsername if oEntry.oAuthor is not None else ''),
+ sRowClass, ' tmsyschlogevent' if oChangeEntry is not None else '', webutils.escapeElem(sEvent),
+ oDetails.toHtml() if isinstance(oDetails, WuiHtmlBase) else oDetails,
+ );
+
+ if oChangeEntry is not None:
+ sHtml += u' <tr class="%s tmsyschlogspacerrowabove">\n' \
+ u' <td xrowspan="%d" style="border-right: 0px; border-bottom: 0px;"></td>\n' \
+ u' <td colspan="3" style="border-right: 0px;"></td>\n' \
+ u' <td rowspan="%d" class="%s tmsyschlogspacer"></td>\n' \
+ u' </tr>\n' \
+ % (sRowClass, cAttribsChanged + 1, cAttribsChanged + 1, sRowClass);
+ for j, oChange in enumerate(oChangeEntry.aoChanges):
+ fLastRow = j + 1 == len(oChangeEntry.aoChanges);
+ sHtml += u' <tr class="%s%s tmsyschlogattr%s">\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even', ' tmsyschlogattrfinal' if fLastRow else '',);
+ if j == 0:
+ sHtml += u' <td class="%s tmsyschlogspacer" rowspan="%d"></td>\n' % (sRowClass, cAttribsChanged - 1,);
+
+ if isinstance(oChange, AttributeChangeEntryPre):
+ sHtml += u' <td class="%s%s">%s</td>\n' \
+ u' <td><div class="tdpre"><pre>%s</pre></div></td>\n' \
+ u' <td class="%s%s"><div class="tdpre"><pre>%s</pre></div></td>\n' \
+ % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
+ webutils.escapeElem(oChange.sNewText), );
+ else:
+ sHtml += u' <td class="%s%s">%s</td>\n' \
+ u' <td>%s</td>\n' \
+ u' <td class="%s%s">%s</td>\n' \
+ % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '',
+ webutils.escapeElem(oChange.sNewText), );
+ sHtml += u' </tr>\n';
+
+ if oChangeEntry is not None:
+ sHtml += u' <tr class="%s tmsyschlogspacerrowbelow "><td colspan="5"></td></tr>\n\n' % (sRowClass,);
+ return sHtml;
+
+
+ def _generateTableHeaders(self):
+ """
+ Overridden parent method.
+ """
+
+ sHtml = u'<thead class="tmheader">\n' \
+ u' <tr>\n' \
+ u' <th rowspan="2">When</th>\n' \
+ u' <th rowspan="2">Who</th>\n' \
+ u' <th colspan="5">Event</th>\n' \
+ u' </tr>\n' \
+ u' <tr>\n' \
+ u' <th style="border-right: 0px;"></th>\n' \
+ u' <th>Attribute</th>\n' \
+ u' <th>Old</th>\n' \
+ u' <th style="border-right: 0px;">New</th>\n' \
+ u' <th></th>\n' \
+ u' </tr>\n' \
+ u'</thead>\n';
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py
new file mode 100755
index 00000000..b0e6f99c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemdbdump.py $
+
+"""
+Test Manager WUI - System DB - Partial Dumping
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.core.base import ModelDataBase;
+from testmanager.webui.wuicontentbase import WuiFormContentBase;
+
+
+class WuiAdminSystemDbDumpForm(WuiFormContentBase):
+ """
+ WUI Partial DB Dump HTML content generator.
+ """
+
+ def __init__(self, cDaysBack, oDisp):
+ WuiFormContentBase.__init__(self, ModelDataBase(),
+ WuiFormContentBase.ksMode_Edit, 'DbDump', oDisp, 'Partial DB Dump',
+ sSubmitAction = oDisp.ksActionSystemDbDumpDownload);
+ self._cDaysBack = cDaysBack;
+
+ def _generateTopRowFormActions(self, oData):
+ _ = oData;
+ return [];
+
+ def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool
+ """
+ Construct an HTML form
+ """
+ _ = oData;
+
+ oForm.addInt(self._oDisp.ksParamDaysBack, self._cDaysBack, 'How many days back to dump');
+ oForm.addSubmit('Produce & Download');
+
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py
new file mode 100755
index 00000000..6b3b7a45
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminsystemlog.py $
+
+"""
+Test Manager WUI - Admin - System Log.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink;
+from testmanager.core.testbox import TestBoxData;
+from testmanager.core.systemlog import SystemLogData;
+from testmanager.core.useraccount import UserAccountData;
+
+
+class WuiAdminSystemLogList(WuiListContentBase):
+ """
+ WUI System Log Content Generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Log',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['Date', 'Event', 'Message', 'Action'];
+ self._asColumnAttribs = ['', '', '', 'align="center"'];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ oAction = None
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown \
+ and oEntry.sLogText.find('addr=') >= 0 \
+ and oEntry.sLogText.find('uuid=') >= 0:
+ sUuid = (oEntry.sLogText[(oEntry.sLogText.find('uuid=') + 5):])[:36];
+ sAddr = (oEntry.sLogText[(oEntry.sLogText.find('addr=') + 5):]).split(' ')[0];
+ oAction = WuiTmLink('Add TestBox', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxAdd,
+ TestBoxData.ksParam_uuidSystem: sUuid,
+ TestBoxData.ksParam_ip: sAddr });
+
+ elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown:
+ sUserName = oEntry.sLogText[oEntry.sLogText.find('(') + 1:
+ oEntry.sLogText.find(')')]
+ oAction = WuiTmLink('Add User', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserAdd,
+ UserAccountData.ksParam_sLoginName: sUserName });
+
+ return [oEntry.tsCreated, oEntry.sEvent, oEntry.sLogText, oAction];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py
new file mode 100755
index 00000000..a06e670c
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py
@@ -0,0 +1,490 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestbox.py $
+
+"""
+Test Manager WUI - TestBox.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import socket;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentWithActionBase, WuiFormContentBase, WuiLinkBase, \
+ WuiSvnLink, WuiTmLink, WuiSpanText, WuiRawHtml;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData;
+from testmanager.core.testbox import TestBoxData, TestBoxDataEx, TestBoxLogic;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.db import isDbTimestampInfinity;
+
+
+
+class WuiTestBoxDetailsLinkById(WuiTmLink):
+ """ Test box details link by ID. """
+
+ def __init__(self, idTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None, sTitle = None):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ dParams = {
+ WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: idTestBox,
+ };
+ if tsNow is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ??
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed, sTitle = sTitle);
+ self.idTestBox = idTestBox;
+
+
+class WuiTestBoxDetailsLink(WuiTestBoxDetailsLinkById):
+ """ Test box details link by TestBoxData instance. """
+
+ def __init__(self, oTestBox, sName = None, fBracketed = False, tsNow = None): # (TestBoxData, str, bool, Any) -> None
+ WuiTestBoxDetailsLinkById.__init__(self, oTestBox.idTestBox,
+ sName if sName else oTestBox.sName,
+ fBracketed = fBracketed,
+ tsNow = tsNow,
+ sTitle = self.formatTitleText(oTestBox));
+ self.oTestBox = oTestBox;
+
+ @staticmethod
+ def formatTitleText(oTestBox): # (TestBoxData) -> str
+ """
+ Formats the title text for a TestBoxData object.
+ """
+
+ # Note! Somewhat similar code is found in testresults.py
+
+ #
+ # Collect field/value tuples.
+ #
+ aasTestBoxTitle = [
+ (u'Identifier:', '#%u' % (oTestBox.idTestBox,),),
+ (u'Name:', oTestBox.sName,),
+ ];
+ if oTestBox.sCpuVendor:
+ aasTestBoxTitle.append((u'CPU\u00a0vendor:', oTestBox.sCpuVendor, ));
+ if oTestBox.sCpuName:
+ aasTestBoxTitle.append((u'CPU\u00a0name:', u'\u00a0'.join(oTestBox.sCpuName.split()),));
+ if oTestBox.cCpus:
+ aasTestBoxTitle.append((u'CPU\u00a0threads:', u'%s' % ( oTestBox.cCpus, ),));
+
+ asFeatures = [];
+ if oTestBox.fCpuHwVirt is True:
+ if oTestBox.sCpuVendor is None:
+ asFeatures.append(u'HW\u2011Virt');
+ elif oTestBox.sCpuVendor in ['AuthenticAMD',]:
+ asFeatures.append(u'HW\u2011Virt(AMD\u2011V)');
+ else:
+ asFeatures.append(u'HW\u2011Virt(VT\u2011x)');
+ if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ aasTestBoxTitle.append((u'CPU\u00a0features:', u',\u00a0'.join(asFeatures),));
+
+ if oTestBox.cMbMemory:
+ aasTestBoxTitle.append((u'System\u00a0RAM:', u'%s MiB' % ( oTestBox.cMbMemory, ),));
+ if oTestBox.sOs:
+ aasTestBoxTitle.append((u'OS:', oTestBox.sOs, ));
+ if oTestBox.sCpuArch:
+ aasTestBoxTitle.append((u'OS\u00a0arch:', oTestBox.sCpuArch,));
+ if oTestBox.sOsVersion:
+ aasTestBoxTitle.append((u'OS\u00a0version:', u'\u00a0'.join(oTestBox.sOsVersion.split()),));
+ if oTestBox.ip:
+ aasTestBoxTitle.append((u'IP\u00a0address:', u'%s' % ( oTestBox.ip, ),));
+
+ #
+ # Do a guestimation of the max field name width and pad short
+ # names when constructing the title text lines.
+ #
+ cchMaxWidth = 0;
+ for sEntry, _ in aasTestBoxTitle:
+ cchMaxWidth = max(WuiTestBoxDetailsLink.estimateStringWidth(sEntry), cchMaxWidth);
+ asTestBoxTitle = [];
+ for sEntry, sValue in aasTestBoxTitle:
+ asTestBoxTitle.append(u'%s%s\t\t%s'
+ % (sEntry, WuiTestBoxDetailsLink.getStringWidthPadding(sEntry, cchMaxWidth), sValue));
+
+ return u'\n'.join(asTestBoxTitle);
+
+
+class WuiTestBoxDetailsLinkShort(WuiTestBoxDetailsLink):
+ """ Test box details link by TestBoxData instance, but with ksShortDetailsLink as default name. """
+
+ def __init__(self, oTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False,
+ tsNow = None): # (TestBoxData, str, bool, Any) -> None
+ WuiTestBoxDetailsLink.__init__(self, oTestBox, sName = sName, fBracketed = fBracketed, tsNow = tsNow);
+
+
+class WuiTestBox(WuiFormContentBase):
+ """
+ WUI TestBox Form Content Generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Create TextBox';
+ if oData.uuidSystem is not None and len(oData.uuidSystem) > 10:
+ sTitle += ' - ' + oData.uuidSystem;
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit TestBox - %s (#%s)' % (oData.sName, oData.idTestBox);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'TestBox - %s (#%s)' % (oData.sName, oData.idTestBox);
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestBox', oDisp, sTitle);
+
+ # Try enter sName as hostname (no domain) when creating the testbox.
+ if sMode == WuiFormContentBase.ksMode_Add \
+ and self._oData.sName in [None, ''] \
+ and self._oData.ip not in [None, '']:
+ try:
+ (self._oData.sName, _, _) = socket.gethostbyaddr(self._oData.ip);
+ except:
+ pass;
+ offDot = self._oData.sName.find('.');
+ if offDot > 0:
+ self._oData.sName = self._oData.sName[:offDot];
+
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( TestBoxData.ksParam_idTestBox, oData.idTestBox, 'TestBox ID');
+ oForm.addIntRO( TestBoxData.ksParam_idGenTestBox, oData.idGenTestBox, 'TestBox generation ID');
+ oForm.addTimestampRO(TestBoxData.ksParam_tsEffective, oData.tsEffective, 'Last changed');
+ oForm.addTimestampRO(TestBoxData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)');
+ oForm.addIntRO( TestBoxData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+
+ oForm.addText( TestBoxData.ksParam_ip, oData.ip, 'TestBox IP Address'); ## make read only??
+ oForm.addUuid( TestBoxData.ksParam_uuidSystem, oData.uuidSystem, 'TestBox System/Firmware UUID');
+ oForm.addText( TestBoxData.ksParam_sName, oData.sName, 'TestBox Name');
+ oForm.addText( TestBoxData.ksParam_sDescription, oData.sDescription, 'TestBox Description');
+ oForm.addCheckBox( TestBoxData.ksParam_fEnabled, oData.fEnabled, 'Enabled');
+ oForm.addComboBox( TestBoxData.ksParam_enmLomKind, oData.enmLomKind, 'Lights-out-management',
+ TestBoxData.kaoLomKindDescs);
+ oForm.addText( TestBoxData.ksParam_ipLom, oData.ipLom, 'Lights-out-management IP Address');
+ oForm.addInt( TestBoxData.ksParam_pctScaleTimeout, oData.pctScaleTimeout, 'Timeout scale factor (%)');
+
+ oForm.addListOfSchedGroupsForTestBox(TestBoxDataEx.ksParam_aoInSchedGroups,
+ oData.aoInSchedGroups,
+ SchedGroupLogic(TMDatabaseConnection()).fetchOrderedByName(),
+ 'Scheduling Group', oData.idTestBox);
+ # Command, comment and submit button.
+ if self._sMode == WuiFormContentBase.ksMode_Edit:
+ oForm.addComboBox(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command',
+ TestBoxData.kaoTestBoxCmdDescs);
+ else:
+ oForm.addComboBoxRO(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command',
+ TestBoxData.kaoTestBoxCmdDescs);
+ oForm.addMultilineText(TestBoxData.ksParam_sComment, oData.sComment, 'Comment');
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Create TestBox' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change TestBox');
+
+ return True;
+
+
+ def _generatePostFormContent(self, oData):
+ from testmanager.webui.wuihlpform import WuiHlpForm;
+
+ oForm = WuiHlpForm('testbox-machine-settable', '', fReadOnly = True);
+ oForm.addTextRO( TestBoxData.ksParam_sOs, oData.sOs, 'TestBox OS');
+ oForm.addTextRO( TestBoxData.ksParam_sOsVersion, oData.sOsVersion, 'TestBox OS version');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuArch, oData.sCpuArch, 'TestBox OS kernel architecture');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuVendor, oData.sCpuVendor, 'TestBox CPU vendor');
+ oForm.addTextRO( TestBoxData.ksParam_sCpuName, oData.sCpuName, 'TestBox CPU name');
+ if oData.lCpuRevision:
+ oForm.addTextRO( TestBoxData.ksParam_lCpuRevision, '%#x' % (oData.lCpuRevision,), 'TestBox CPU revision',
+ sPostHtml = ' (family=%#x model=%#x stepping=%#x)'
+ % (oData.getCpuFamily(), oData.getCpuModel(), oData.getCpuStepping(),),
+ sSubClass = 'long');
+ else:
+ oForm.addLongRO( TestBoxData.ksParam_lCpuRevision, oData.lCpuRevision, 'TestBox CPU revision');
+ oForm.addIntRO( TestBoxData.ksParam_cCpus, oData.cCpus, 'Number of CPUs, cores and threads');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuHwVirt, oData.fCpuHwVirt, 'VT-x or AMD-V supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuNestedPaging, oData.fCpuNestedPaging, 'Nested paging supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fCpu64BitGuest, oData.fCpu64BitGuest, '64-bit guest supported');
+ oForm.addCheckBoxRO( TestBoxData.ksParam_fChipsetIoMmu, oData.fChipsetIoMmu, 'I/O MMU supported');
+ oForm.addMultilineTextRO(TestBoxData.ksParam_sReport, oData.sReport, 'Hardware/software report');
+ oForm.addLongRO( TestBoxData.ksParam_cMbMemory, oData.cMbMemory, 'Installed RAM size (MB)');
+ oForm.addLongRO( TestBoxData.ksParam_cMbScratch, oData.cMbScratch, 'Available scratch space (MB)');
+ oForm.addIntRO( TestBoxData.ksParam_iTestBoxScriptRev, oData.iTestBoxScriptRev,
+ 'TestBox Script SVN revision');
+ sHexVer = oData.formatPythonVersion();
+ oForm.addIntRO( TestBoxData.ksParam_iPythonHexVersion, oData.iPythonHexVersion,
+ 'Python version (hex)', sPostHtml = webutils.escapeElem(sHexVer));
+ return [('Machine Only Settables', oForm.finalize()),];
+
+
+
+class WuiTestBoxList(WuiListContentWithActionBase):
+ """
+ WUI TestBox List Content Generator.
+ """
+
+ ## Descriptors for the combo box.
+ kasTestBoxActionDescs = \
+ [ \
+ [ 'none', 'Select an action...', '' ],
+ [ 'enable', 'Enable', '' ],
+ [ 'disable', 'Disable', '' ],
+ TestBoxData.kaoTestBoxCmdDescs[1],
+ TestBoxData.kaoTestBoxCmdDescs[2],
+ TestBoxData.kaoTestBoxCmdDescs[3],
+ TestBoxData.kaoTestBoxCmdDescs[4],
+ TestBoxData.kaoTestBoxCmdDescs[5],
+ ];
+
+ ## Boxes which doesn't report in for more than 15 min are considered dead.
+ kcSecMaxStatusDeltaAlive = 15*60
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ # type: (list[TestBoxDataForListing], int, int, datetime.datetime, ignore, WuiAdmin) -> None
+ WuiListContentWithActionBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'TestBoxes', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders.extend([ 'Name', 'LOM', 'Status', 'Cmd',
+ 'Note', 'Script', 'Python', 'Group',
+ 'OS', 'CPU', 'Features', 'CPUs', 'RAM', 'Scratch',
+ 'Actions' ]);
+ self._asColumnAttribs.extend([ 'align="center"', 'align="center"', 'align="center"', 'align="center"'
+ 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ '', '', '', 'align="left"', 'align="right"', 'align="right"', 'align="right"',
+ 'align="center"' ]);
+ self._aaiColumnSorting.extend([
+ (TestBoxLogic.kiSortColumn_sName,),
+ None, # LOM
+ (-TestBoxLogic.kiSortColumn_fEnabled, TestBoxLogic.kiSortColumn_enmState, -TestBoxLogic.kiSortColumn_tsUpdated,),
+ (TestBoxLogic.kiSortColumn_enmPendingCmd,),
+ None, # Note
+ (TestBoxLogic.kiSortColumn_iTestBoxScriptRev,),
+ (TestBoxLogic.kiSortColumn_iPythonHexVersion,),
+ None, # Group
+ (TestBoxLogic.kiSortColumn_sOs, TestBoxLogic.kiSortColumn_sOsVersion, TestBoxLogic.kiSortColumn_sCpuArch,),
+ (TestBoxLogic.kiSortColumn_sCpuVendor, TestBoxLogic.kiSortColumn_lCpuRevision,),
+ (TestBoxLogic.kiSortColumn_fCpuNestedPaging,),
+ (TestBoxLogic.kiSortColumn_cCpus,),
+ (TestBoxLogic.kiSortColumn_cMbMemory,),
+ (TestBoxLogic.kiSortColumn_cMbScratch,),
+ None, # Actions
+ ]);
+ assert len(self._aaiColumnSorting) == len(self._asColumnHeaders);
+ self._aoActions = list(self.kasTestBoxActionDescs);
+ self._sAction = oDisp.ksActionTestBoxListPost;
+ self._sCheckboxName = TestBoxData.ksParam_idTestBox;
+
+ def show(self, fShowNavigation = True):
+ """ Adds some stats at the bottom of the page """
+ (sTitle, sBody) = super(WuiTestBoxList, self).show(fShowNavigation);
+
+ # Count boxes in interesting states.
+ if self._aoEntries:
+ cActive = 0;
+ cDead = 0;
+ for oTestBox in self._aoEntries:
+ if oTestBox.oStatus is not None:
+ oDelta = oTestBox.tsCurrent - oTestBox.oStatus.tsUpdated;
+ if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive:
+ if oTestBox.fEnabled:
+ cActive += 1;
+ else:
+ cDead += 1;
+ else:
+ cDead += 1;
+ sBody += '<div id="testboxsummary"><p>\n' \
+ '%s testboxes of which %s are active and %s dead' \
+ '</p></div>\n' \
+ % (len(self._aoEntries), cActive, cDead,)
+ return (sTitle, sBody);
+
+ def _formatListEntry(self, iEntry): # pylint: disable=too-many-locals
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+
+ # Lights outs managment.
+ if oEntry.enmLomKind == TestBoxData.ksLomKind_ILOM:
+ aoLom = [ WuiLinkBase('ILOM', 'https://%s/' % (oEntry.ipLom,), fBracketed = False), ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_ELOM:
+ aoLom = [ WuiLinkBase('ELOM', 'http://%s/' % (oEntry.ipLom,), fBracketed = False), ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_AppleXserveLom:
+ aoLom = [ 'Apple LOM' ];
+ elif oEntry.enmLomKind == TestBoxData.ksLomKind_None:
+ aoLom = [ 'none' ];
+ else:
+ aoLom = [ 'Unexpected enmLomKind value "%s"' % (oEntry.enmLomKind,) ];
+ if oEntry.ipLom is not None:
+ if oEntry.enmLomKind in [ TestBoxData.ksLomKind_ILOM, TestBoxData.ksLomKind_ELOM ]:
+ aoLom += [ WuiLinkBase('(ssh)', 'ssh://%s' % (oEntry.ipLom,), fBracketed = False) ];
+ aoLom += [ WuiRawHtml('<br>'), '%s' % (oEntry.ipLom,) ];
+
+ # State and Last seen.
+ if oEntry.oStatus is None:
+ oSeen = WuiSpanText('tmspan-offline', 'Never');
+ oState = '';
+ else:
+ oDelta = oEntry.tsCurrent - oEntry.oStatus.tsUpdated;
+ if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive:
+ oSeen = WuiSpanText('tmspan-online', u'%s\u00a0s\u00a0ago' % (oDelta.days * 24 * 3600 + oDelta.seconds,));
+ else:
+ oSeen = WuiSpanText('tmspan-offline', u'%s' % (self.formatTsShort(oEntry.oStatus.tsUpdated),));
+
+ if oEntry.oStatus.idTestSet is None:
+ oState = str(oEntry.oStatus.enmState);
+ else:
+ from testmanager.webui.wuimain import WuiMain;
+ oState = WuiTmLink(oEntry.oStatus.enmState, WuiMain.ksScriptName, # pylint: disable=redefined-variable-type
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.oStatus.idTestSet, },
+ sTitle = '#%u' % (oEntry.oStatus.idTestSet,),
+ fBracketed = False);
+ # Comment
+ oComment = self._formatCommentCell(oEntry.sComment);
+
+ # Group links.
+ aoGroups = [];
+ for oInGroup in oEntry.aoInSchedGroups:
+ oSchedGroup = oInGroup.oSchedGroup;
+ aoGroups.append(WuiTmLink(oSchedGroup.sName, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit,
+ SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup, },
+ sTitle = '#%u' % (oSchedGroup.idSchedGroup,),
+ fBracketed = len(oEntry.aoInSchedGroups) > 1));
+
+ # Reformat the OS version to take less space.
+ aoOs = [ 'N/A' ];
+ if oEntry.sOs is not None and oEntry.sOsVersion is not None and oEntry.sCpuArch:
+ sOsVersion = oEntry.sOsVersion;
+ if sOsVersion[0] not in [ 'v', 'V', 'r', 'R'] \
+ and sOsVersion[0].isdigit() \
+ and sOsVersion.find('.') in range(4) \
+ and oEntry.sOs in [ 'linux', 'solaris', 'darwin', ]:
+ sOsVersion = 'v' + sOsVersion;
+
+ sVer1 = sOsVersion;
+ sVer2 = None;
+ if oEntry.sOs in ('linux', 'darwin'):
+ iSep = sOsVersion.find(' / ');
+ if iSep > 0:
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = sOsVersion[iSep + 3:].strip();
+ sVer2 = sVer2.replace('Red Hat Enterprise Linux Server', 'RHEL');
+ sVer2 = sVer2.replace('Oracle Linux Server', 'OL');
+ elif oEntry.sOs == 'solaris':
+ iSep = sOsVersion.find(' (');
+ if iSep > 0 and sOsVersion[-1] == ')':
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = sOsVersion[iSep + 2:-1].strip();
+ elif oEntry.sOs == 'win':
+ iSep = sOsVersion.find('build');
+ if iSep > 0:
+ sVer1 = sOsVersion[:iSep].strip();
+ sVer2 = 'B' + sOsVersion[iSep + 1:].strip();
+ aoOs = [
+ WuiSpanText('tmspan-osarch', u'%s.%s' % (oEntry.sOs, oEntry.sCpuArch,)),
+ WuiSpanText('tmspan-osver1', sVer1.replace('-', u'\u2011'),),
+ ];
+ if sVer2 is not None:
+ aoOs += [ WuiRawHtml('<br>'), WuiSpanText('tmspan-osver2', sVer2.replace('-', u'\u2011')), ];
+
+ # Format the CPU revision.
+ oCpu = None;
+ if oEntry.lCpuRevision is not None and oEntry.sCpuVendor is not None and oEntry.sCpuName is not None:
+ oCpu = [
+ u'%s (fam:%xh\u00a0m:%xh\u00a0s:%xh)'
+ % (oEntry.sCpuVendor, oEntry.getCpuFamily(), oEntry.getCpuModel(), oEntry.getCpuStepping(),),
+ WuiRawHtml('<br>'),
+ oEntry.sCpuName,
+ ];
+ else:
+ oCpu = [];
+ if oEntry.sCpuVendor is not None:
+ oCpu.append(oEntry.sCpuVendor);
+ if oEntry.lCpuRevision is not None:
+ oCpu.append('%#x' % (oEntry.lCpuRevision,));
+ if oEntry.sCpuName is not None:
+ oCpu.append(oEntry.sCpuName);
+
+ # Stuff cpu vendor and cpu/box features into one field.
+ asFeatures = []
+ if oEntry.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt');
+ if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ sFeatures = u' '.join(asFeatures) if asFeatures else u'';
+
+ # Collection applicable actions.
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ),
+ ]
+
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions += [
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxEdit,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox, } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxRemovePost,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
+ sConfirm = 'Are you sure that you want to remove %s (%s)?' % (oEntry.sName, oEntry.ip) ),
+ ]
+
+ if oEntry.sOs not in [ 'win', 'os2', ] and oEntry.ip is not None:
+ aoActions.append(WuiLinkBase('ssh', 'ssh://vbox@%s' % (oEntry.ip,),));
+
+ return [ self._getCheckBoxColumn(iEntry, oEntry.idTestBox),
+ [ WuiSpanText('tmspan-name', oEntry.sName), WuiRawHtml('<br>'), '%s' % (oEntry.ip,),],
+ aoLom,
+ [
+ '' if oEntry.fEnabled else 'disabled / ',
+ oState,
+ WuiRawHtml('<br>'),
+ oSeen,
+ ],
+ oEntry.enmPendingCmd,
+ oComment,
+ WuiSvnLink(oEntry.iTestBoxScriptRev),
+ oEntry.formatPythonVersion(),
+ aoGroups,
+ aoOs,
+ oCpu,
+ sFeatures,
+ oEntry.cCpus if oEntry.cCpus is not None else 'N/A',
+ utils.formatNumberNbsp(oEntry.cMbMemory) + u'\u00a0MB' if oEntry.cMbMemory is not None else 'N/A',
+ utils.formatNumberNbsp(oEntry.cMbScratch) + u'\u00a0MB' if oEntry.cMbScratch is not None else 'N/A',
+ aoActions,
+ ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py
new file mode 100755
index 00000000..19fa86c9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py
@@ -0,0 +1,258 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestcase.py $
+
+"""
+Test Manager WUI - Test Cases.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.testcase import TestCaseDataEx, TestCaseData, TestCaseDependencyLogic;
+from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic;
+
+
+
+class WuiTestCaseDetailsLink(WuiTmLink):
+ """ Test case details link by ID. """
+
+ def __init__(self, idTestCase, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ dParams = {
+ WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: idTestCase,
+ };
+ if tsNow is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ??
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed);
+ self.idTestCase = idTestCase;
+
+
+class WuiTestCaseList(WuiListContentBase):
+ """
+ WUI test case list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Cases',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = \
+ [
+ 'Name', 'Active', 'Timeout', 'Base Command / Variations', 'Validation Kit Files',
+ 'Test Case Prereqs', 'Global Rsrces', 'Note', 'Actions'
+ ];
+ self._asColumnAttribs = \
+ [
+ '', '', 'align="center"', '', '',
+ 'valign="top"', 'valign="top"', 'align="center"', 'align="center"'
+ ];
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry];
+ from testmanager.webui.wuiadmin import WuiAdmin;
+
+ aoRet = \
+ [
+ oEntry.sName.replace('-', u'\u2011'),
+ 'Enabled' if oEntry.fEnabled else 'Disabled',
+ utils.formatIntervalSeconds(oEntry.cSecTimeout),
+ ];
+
+ # Base command and variations.
+ fNoGang = True;
+ fNoSubName = True;
+ fAllDefaultTimeouts = True;
+ for oVar in oEntry.aoTestCaseArgs:
+ if fNoSubName and oVar.sSubName is not None and oVar.sSubName.strip():
+ fNoSubName = False;
+ if oVar.cGangMembers > 1:
+ fNoGang = False;
+ if oVar.cSecTimeout is not None:
+ fAllDefaultTimeouts = False;
+
+ sHtml = ' <table class="tminnertbl" width=100%>\n' \
+ ' <tr>\n' \
+ ' ';
+ if not fNoSubName:
+ sHtml += '<th class="tmtcasubname">Sub-name</th>';
+ if not fNoGang:
+ sHtml += '<th class="tmtcagangsize">Gang Size</th>';
+ if not fAllDefaultTimeouts:
+ sHtml += '<th class="tmtcatimeout">Timeout</th>';
+ sHtml += '<th>Additional Arguments</b></th>\n' \
+ ' </tr>\n'
+ for oTmp in oEntry.aoTestCaseArgs:
+ sHtml += '<tr>';
+ if not fNoSubName:
+ sHtml += '<td>%s</td>' % (webutils.escapeElem(oTmp.sSubName) if oTmp.sSubName is not None else '');
+ if not fNoGang:
+ sHtml += '<td>%d</td>' % (oTmp.cGangMembers,)
+ if not fAllDefaultTimeouts:
+ sHtml += '<td>%s</td>' \
+ % (utils.formatIntervalSeconds(oTmp.cSecTimeout) if oTmp.cSecTimeout is not None else 'Default',)
+ sHtml += u'<td>%s</td></tr>' \
+ % ( webutils.escapeElem(oTmp.sArgs.replace('-', u'\u2011')) if oTmp.sArgs else u'\u2011',);
+ sHtml += '</tr>\n';
+ sHtml += ' </table>'
+
+ aoRet.append([oEntry.sBaseCmd.replace('-', u'\u2011'), WuiRawHtml(sHtml)]);
+
+ # Next.
+ aoRet += [ oEntry.sValidationKitZips if oEntry.sValidationKitZips is not None else '', ];
+
+ # Show dependency on other testcases
+ if oEntry.aoDepTestCases not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in oEntry.aoDepTestCases:
+ sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \
+ % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase, sTmp.idTestCase,
+ sTmp.sName)
+ sHtml += ' </ul>\n'
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n'
+ aoRet.append(WuiRawHtml(sHtml));
+
+ # Show dependency on global resources
+ if oEntry.aoDepGlobalResources not in (None, []):
+ sHtml = ' <ul class="tmshowall">\n'
+ for sTmp in oEntry.aoDepGlobalResources:
+ sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \
+ % (WuiAdmin.ksScriptName,
+ WuiAdmin.ksParamAction, WuiAdmin.ksActionGlobalRsrcShowEdit,
+ GlobalResourceData.ksParam_idGlobalRsrc, sTmp.idGlobalRsrc,
+ sTmp.sName)
+ sHtml += ' </ul>\n'
+ else:
+ sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n'
+ aoRet.append(WuiRawHtml(sHtml));
+
+ # Comment (note).
+ aoRet.append(self._formatCommentCell(oEntry.sComment));
+
+ # Show actions that can be taken.
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase }), ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase }));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseClone,
+ TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase }));
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDoRemove,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase },
+ sConfirm = 'Are you sure you want to remove test case #%d?' % (oEntry.idTestCase,)));
+ aoRet.append(aoActions);
+
+ return aoRet;
+
+
+class WuiTestCase(WuiFormContentBase):
+ """
+ WUI user account content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, TestCaseDataEx);
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'New Test Case';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Edit Test Case - %s (#%s)' % (oData.sName, oData.idTestCase);
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Case - %s (#%s)' % (oData.sName, oData.idTestCase);
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestCase', oDisp, sTitle);
+
+ # Read additional bits form the DB.
+ oDepLogic = TestCaseDependencyLogic(oDisp.getDb());
+ self._aoAllTestCases = oDepLogic.getApplicableDepTestCaseData(-1 if oData.idTestCase is None else oData.idTestCase);
+ self._aoAllGlobalRsrcs = GlobalResourceLogic(oDisp.getDb()).getAll();
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (TestCaseData.ksParam_idTestCase, oData.idTestCase, 'Test Case ID')
+ oForm.addTimestampRO(TestCaseData.ksParam_tsEffective, oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO(TestCaseData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (TestCaseData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID')
+ oForm.addIntRO (TestCaseData.ksParam_idGenTestCase, oData.idGenTestCase, 'Test Case generation ID')
+ oForm.addText (TestCaseData.ksParam_sName, oData.sName, 'Name')
+ oForm.addText (TestCaseData.ksParam_sDescription, oData.sDescription, 'Description')
+ oForm.addCheckBox (TestCaseData.ksParam_fEnabled, oData.fEnabled, 'Enabled')
+ oForm.addLong (TestCaseData.ksParam_cSecTimeout,
+ utils.formatIntervalSeconds2(oData.cSecTimeout), 'Default timeout')
+ oForm.addWideText (TestCaseData.ksParam_sTestBoxReqExpr, oData.sTestBoxReqExpr, 'TestBox requirements (python)');
+ oForm.addWideText (TestCaseData.ksParam_sBuildReqExpr, oData.sBuildReqExpr, 'Build requirement (python)');
+ oForm.addWideText (TestCaseData.ksParam_sBaseCmd, oData.sBaseCmd, 'Base command')
+ oForm.addText (TestCaseData.ksParam_sValidationKitZips, oData.sValidationKitZips, 'Test suite files')
+
+ oForm.addListOfTestCaseArgs(TestCaseDataEx.ksParam_aoTestCaseArgs, oData.aoTestCaseArgs, 'Argument variations')
+
+ aoTestCaseDeps = [];
+ for oTestCase in self._aoAllTestCases:
+ if oTestCase.idTestCase == oData.idTestCase:
+ continue;
+ fSelected = False;
+ for oDep in oData.aoDepTestCases:
+ if oDep.idTestCase == oTestCase.idTestCase:
+ fSelected = True;
+ break;
+ aoTestCaseDeps.append([oTestCase.idTestCase, fSelected, oTestCase.sName]);
+ oForm.addListOfTestCases(TestCaseDataEx.ksParam_aoDepTestCases, aoTestCaseDeps, 'Depends on test cases')
+
+ aoGlobalResrcDeps = [];
+ for oGlobalRsrc in self._aoAllGlobalRsrcs:
+ fSelected = False;
+ for oDep in oData.aoDepGlobalResources:
+ if oDep.idGlobalRsrc == oGlobalRsrc.idGlobalRsrc:
+ fSelected = True;
+ break;
+ aoGlobalResrcDeps.append([oGlobalRsrc.idGlobalRsrc, fSelected, oGlobalRsrc.sName]);
+ oForm.addListOfResources(TestCaseDataEx.ksParam_aoDepGlobalResources, aoGlobalResrcDeps, 'Depends on resources')
+
+ oForm.addMultilineText(TestCaseDataEx.ksParam_sComment, oData.sComment, 'Comment');
+
+ oForm.addSubmit();
+
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py
new file mode 100755
index 00000000..8954afe9
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadmintestgroup.py $
+
+"""
+Test Manager WUI - Test Groups.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml;
+from testmanager.core.db import isDbTimestampInfinity;
+from testmanager.core.testgroup import TestGroupData, TestGroupDataEx;
+from testmanager.core.testcase import TestCaseData, TestCaseLogic;
+
+
+class WuiTestGroup(WuiFormContentBase):
+ """
+ WUI test group content generator.
+ """
+
+ def __init__(self, oData, sMode, oDisp):
+ assert isinstance(oData, TestGroupDataEx);
+
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Test Group';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Test Group';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Group';
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestGroup', oDisp, sTitle);
+
+ #
+ # Fetch additional data.
+ #
+ if sMode in [WuiFormContentBase.ksMode_Add, WuiFormContentBase.ksMode_Edit]:
+ self.aoAllTestCases = TestCaseLogic(oDisp.getDb()).fetchForListing(0, 0x7fff, None);
+ else:
+ self.aoAllTestCases = [oMember.oTestCase for oMember in oData.aoMembers];
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO (TestGroupData.ksParam_idTestGroup, self._oData.idTestGroup, 'Test Group ID')
+ oForm.addTimestampRO (TestGroupData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed')
+ oForm.addTimestampRO (TestGroupData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)')
+ oForm.addIntRO (TestGroupData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID')
+ oForm.addText (TestGroupData.ksParam_sName, self._oData.sName, 'Name')
+ oForm.addText (TestGroupData.ksParam_sDescription, self._oData.sDescription, 'Description')
+
+ oForm.addListOfTestGroupMembers(TestGroupDataEx.ksParam_aoMembers,
+ oData.aoMembers, self.aoAllTestCases, 'Test Case List',
+ fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show);
+
+ oForm.addMultilineText (TestGroupData.ksParam_sComment, self._oData.sComment, 'Comment');
+ oForm.addSubmit();
+ return True;
+
+
+class WuiTestGroupList(WuiListContentBase):
+ """
+ WUI test group list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ assert not aoEntries or isinstance(aoEntries[0], TestGroupDataEx)
+
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Groups',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = [ 'ID', 'Name', 'Description', 'Test Cases', 'Note', 'Actions' ];
+ self._asColumnAttribs = [ 'align="right"', '', '', '', 'align="center"', 'align="center"' ];
+
+
+ def _formatListEntry(self, iEntry):
+ oEntry = self._aoEntries[iEntry];
+ from testmanager.webui.wuiadmin import WuiAdmin;
+
+ #
+ # Test case list.
+ #
+ sHtml = '';
+ if oEntry.aoMembers:
+ for oMember in oEntry.aoMembers:
+ sHtml += '<dl>\n' \
+ ' <dd><strong>%s</strong> (priority: %d) %s %s</dd>\n' \
+ % ( webutils.escapeElem(oMember.oTestCase.sName),
+ oMember.iSchedPriority,
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idGenTestCase: oMember.oTestCase.idGenTestCase, } ).toHtml(),
+ WuiTmLink('Edit', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit,
+ TestCaseData.ksParam_idTestCase: oMember.oTestCase.idTestCase, } ).toHtml()
+ if isDbTimestampInfinity(oMember.oTestCase.tsExpire)
+ and self._oDisp is not None
+ and not self._oDisp.isReadOnlyUser() else '',
+ );
+
+ sHtml += ' <dt>\n';
+
+ fNoGang = True;
+ for oVar in oMember.oTestCase.aoTestCaseArgs:
+ if oVar.cGangMembers > 1:
+ fNoGang = False
+ break;
+
+ sHtml += ' <table class="tminnertbl" width="100%">\n'
+ if fNoGang:
+ sHtml += ' <tr><th>Timeout</th><th>Arguments</th></tr>\n';
+ else:
+ sHtml += ' <tr><th>Gang Size</th><th>Timeout</th><th style="text-align:left;">Arguments</th></tr>\n';
+
+ cArgsIncluded = 0;
+ for oVar in oMember.oTestCase.aoTestCaseArgs:
+ if oMember.aidTestCaseArgs is None or oVar.idTestCaseArgs in oMember.aidTestCaseArgs:
+ cArgsIncluded += 1;
+ if fNoGang:
+ sHtml += ' <tr>';
+ else:
+ sHtml += ' <tr><td>%s</td>' % (oVar.cGangMembers,);
+ sHtml += '<td>%s</td><td>%s</td></tr>\n' \
+ % ( utils.formatIntervalSeconds(oMember.oTestCase.cSecTimeout if oVar.cSecTimeout is None
+ else oVar.cSecTimeout),
+ webutils.escapeElem(oVar.sArgs), );
+ if cArgsIncluded == 0:
+ sHtml += ' <tr><td colspan="%u">No arguments selected.</td></tr>\n' % ( 2 if fNoGang else 3, );
+ sHtml += ' </table>\n' \
+ ' </dl>\n';
+ oTestCases = WuiRawHtml(sHtml);
+
+ #
+ # Actions.
+ #
+ aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }) ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+
+ if isDbTimestampInfinity(oEntry.tsExpire):
+ aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupEdit,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup }));
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }));
+ aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDoRemove,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup },
+ sConfirm = 'Do you really want to remove test group #%d?' % (oEntry.idTestGroup,)));
+ else:
+ aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone,
+ TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup,
+ WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }));
+
+
+
+ return [ oEntry.idTestGroup,
+ oEntry.sName,
+ oEntry.sDescription if oEntry.sDescription is not None else '',
+ oTestCases,
+ self._formatCommentCell(oEntry.sComment, cMaxLines = max(3, len(oEntry.aoMembers) * 2)),
+ aoActions ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py
new file mode 100755
index 00000000..30d78cd4
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# $Id: wuiadminuseraccount.py $
+
+"""
+Test Manager WUI - User accounts.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink;
+from testmanager.core.useraccount import UserAccountData
+
+
+class WuiUserAccount(WuiFormContentBase):
+ """
+ WUI user account content generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add User Account';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify User Account';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'User Account';
+ WuiFormContentBase.__init__(self, oData, sMode, 'User', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+ oForm.addIntRO( UserAccountData.ksParam_uid, oData.uid, 'User ID');
+ oForm.addTimestampRO(UserAccountData.ksParam_tsEffective, oData.tsEffective, 'Effective Date');
+ oForm.addTimestampRO(UserAccountData.ksParam_tsExpire, oData.tsExpire, 'Effective Date');
+ oForm.addIntRO( UserAccountData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+ oForm.addText( UserAccountData.ksParam_sLoginName, oData.sLoginName, 'Login name')
+ oForm.addText( UserAccountData.ksParam_sUsername, oData.sUsername, 'User name')
+ oForm.addText( UserAccountData.ksParam_sFullName, oData.sFullName, 'Full name')
+ oForm.addText( UserAccountData.ksParam_sEmail, oData.sEmail, 'E-mail')
+ oForm.addCheckBox( UserAccountData.ksParam_fReadOnly, oData.fReadOnly, 'Only read access')
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Add User' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change User');
+ return True;
+
+
+class WuiUserAccountList(WuiListContentBase):
+ """
+ WUI user account list content generator.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Registered User Accounts', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp,
+ aiSelectedSortColumns = aiSelectedSortColumns);
+ self._asColumnHeaders = ['User ID', 'Name', 'E-mail', 'Full Name', 'Login Name', 'Access', 'Actions'];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', ];
+
+ def _formatListEntry(self, iEntry):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ oEntry = self._aoEntries[iEntry];
+ aoActions = [
+ WuiTmLink('Details', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDetails,
+ UserAccountData.ksParam_uid: oEntry.uid } ),
+ ];
+ if self._oDisp is None or not self._oDisp.isReadOnlyUser():
+ aoActions += [
+ WuiTmLink('Modify', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserEdit,
+ UserAccountData.ksParam_uid: oEntry.uid } ),
+ WuiTmLink('Remove', WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDelPost,
+ UserAccountData.ksParam_uid: oEntry.uid },
+ sConfirm = 'Are you sure you want to remove user #%d?' % (oEntry.uid,)),
+ ];
+
+ return [ oEntry.uid, oEntry.sUsername, oEntry.sEmail, oEntry.sFullName, oEntry.sLoginName,
+ 'read only' if oEntry.fReadOnly else 'full',
+ aoActions, ];
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuibase.py b/src/VBox/ValidationKit/testmanager/webui/wuibase.py
new file mode 100755
index 00000000..feccf20a
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuibase.py
@@ -0,0 +1,1245 @@
+# -*- coding: utf-8 -*-
+# $Id: wuibase.py $
+
+"""
+Test Manager Web-UI - Base Classes.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import os;
+import sys;
+import string;
+
+# Validation Kit imports.
+from common import webutils, utils;
+from testmanager import config;
+from testmanager.core.base import ModelDataBase, ModelLogicBase, TMExceptionBase;
+from testmanager.core.db import TMDatabaseConnection;
+from testmanager.core.systemlog import SystemLogLogic, SystemLogData;
+from testmanager.core.useraccount import UserAccountLogic
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+ long = int; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiException(TMExceptionBase):
+ """
+ For exceptions raised by Web UI code.
+ """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class WuiDispatcherBase(object):
+ """
+ Base class for the Web User Interface (WUI) dispatchers.
+
+ The dispatcher class defines the basics of the page (like base template,
+ menu items, action). It is also responsible for parsing requests and
+ dispatching them to action (POST) or/and content generators (GET+POST).
+ The content returned by the generator is merged into the template and sent
+ back to the webserver glue.
+ """
+
+ ## @todo possible that this should all go into presentation.
+
+ ## The action parameter.
+ ksParamAction = 'Action';
+ ## The name of the default action.
+ ksActionDefault = 'default';
+
+ ## The name of the current page number parameter used when displaying lists.
+ ksParamPageNo = 'PageNo';
+ ## The name of the page length (list items) parameter when displaying lists.
+ ksParamItemsPerPage = 'ItemsPerPage';
+
+ ## The name of the effective date (timestamp) parameter.
+ ksParamEffectiveDate = 'EffectiveDate';
+
+ ## The name of the redirect-to (test manager relative url) parameter.
+ ksParamRedirectTo = 'RedirectTo';
+
+ ## The name of the list-action parameter (WuiListContentWithActionBase).
+ ksParamListAction = 'ListAction';
+
+ ## One or more columns to sort by.
+ ksParamSortColumns = 'SortBy';
+
+ ## The name of the change log enabled/disabled parameter.
+ ksParamChangeLogEnabled = 'ChangeLogEnabled';
+ ## The name of the parmaeter indicating the change log page number.
+ ksParamChangeLogPageNo = 'ChangeLogPageNo';
+ ## The name of the parameter indicate number of change log entries per page.
+ ksParamChangeLogEntriesPerPage = 'ChangeLogEntriesPerPage';
+ ## The change log related parameters.
+ kasChangeLogParams = (ksParamChangeLogEnabled, ksParamChangeLogPageNo, ksParamChangeLogEntriesPerPage,);
+
+ ## @name Dispatcher debugging parameters.
+ ## {@
+ ksParamDbgSqlTrace = 'DbgSqlTrace';
+ ksParamDbgSqlExplain = 'DbgSqlExplain';
+ ## List of all debugging parameters.
+ kasDbgParams = (ksParamDbgSqlTrace, ksParamDbgSqlExplain,);
+ ## @}
+
+ ## Special action return code for skipping _generatePage. Useful for
+ # download pages and the like that messes with the HTTP header and more.
+ ksDispatchRcAllDone = 'Done - Page has been rendered already';
+
+
+ def __init__(self, oSrvGlue, sScriptName):
+ self._oSrvGlue = oSrvGlue;
+ self._oDb = TMDatabaseConnection(self.dprint if config.g_kfWebUiSqlDebug else None, oSrvGlue = oSrvGlue);
+ self._tsNow = None; # Set by getEffectiveDateParam.
+ self._asCheckedParams = [];
+ self._dParams = None; # Set by dispatchRequest.
+ self._sAction = None; # Set by dispatchRequest.
+ self._dDispatch = { self.ksActionDefault: self._actionDefault, };
+
+ # Template bits.
+ self._sTemplate = 'template-default.html';
+ self._sPageTitle = '$$TODO$$'; # The page title.
+ self._aaoMenus = []; # List of [sName, sLink, [ [sSideName, sLink], .. ] tuples.
+ self._sPageFilter = ''; # The filter controls (optional).
+ self._sPageBody = '$$TODO$$'; # The body text.
+ self._dSideMenuFormAttrs = {}; # key/value with attributes for the side menu <form> tag.
+ self._sRedirectTo = None;
+ self._sDebug = '';
+
+ # Debugger bits.
+ self._fDbgSqlTrace = False;
+ self._fDbgSqlExplain = False;
+ self._dDbgParams = {};
+ for sKey, sValue in oSrvGlue.getParameters().items():
+ if sKey in self.kasDbgParams:
+ self._dDbgParams[sKey] = sValue;
+ if self._dDbgParams:
+ from testmanager.webui.wuicontentbase import WuiTmLink;
+ WuiTmLink.kdDbgParams = self._dDbgParams;
+
+ # Determine currently logged in user credentials
+ self._oCurUser = UserAccountLogic(self._oDb).tryFetchAccountByLoginName(oSrvGlue.getLoginName());
+
+ # Calc a couple of URL base strings for this dispatcher.
+ self._sUrlBase = sScriptName + '?';
+ if self._dDbgParams:
+ self._sUrlBase += webutils.encodeUrlParams(self._dDbgParams) + '&';
+ self._sActionUrlBase = self._sUrlBase + self.ksParamAction + '=';
+
+
+ def _redirectPage(self):
+ """
+ Redirects the page to the URL given in self._sRedirectTo.
+ """
+ assert self._sRedirectTo;
+ assert self._sPageBody is None;
+ assert self._sPageTitle is None;
+
+ self._oSrvGlue.setRedirect(self._sRedirectTo);
+ return True;
+
+ def _isMenuMatch(self, sMenuUrl, sActionParam):
+ """ Overridable menu matcher. """
+ return sMenuUrl is not None and sMenuUrl.find(sActionParam) > 0;
+
+ def _isSideMenuMatch(self, sSideMenuUrl, sActionParam):
+ """ Overridable side menu matcher. """
+ return sSideMenuUrl is not None and sSideMenuUrl.find(sActionParam) > 0;
+
+ def _generateMenus(self):
+ """
+ Generates the two menus, returning them as (sTopMenuItems, sSideMenuItems).
+ """
+ fReadOnly = self.isReadOnlyUser();
+
+ #
+ # We use the action to locate the side menu.
+ #
+ aasSideMenu = None;
+ for cchAction in range(len(self._sAction), 1, -1):
+ sActionParam = '%s=%s' % (self.ksParamAction, self._sAction[:cchAction]);
+ for aoItem in self._aaoMenus:
+ if self._isMenuMatch(aoItem[1], sActionParam):
+ aasSideMenu = aoItem[2];
+ break;
+ for asSubItem in aoItem[2]:
+ if self._isMenuMatch(asSubItem[1], sActionParam):
+ aasSideMenu = aoItem[2];
+ break;
+ if aasSideMenu is not None:
+ break;
+
+ #
+ # Top menu first.
+ #
+ sTopMenuItems = '';
+ for aoItem in self._aaoMenus:
+ if aasSideMenu is aoItem[2]:
+ sTopMenuItems += '<li class="current_page_item">';
+ else:
+ sTopMenuItems += '<li>';
+ sTopMenuItems += '<a href="' + webutils.escapeAttr(aoItem[1]) + '">' \
+ + webutils.escapeElem(aoItem[0]) + '</a></li>\n';
+
+ #
+ # Side menu (if found).
+ #
+ sActionParam = '%s=%s' % (self.ksParamAction, self._sAction);
+ sSideMenuItems = '';
+ if aasSideMenu is not None:
+ for asSubItem in aasSideMenu:
+ if asSubItem[1] is not None:
+ if not asSubItem[2] or not fReadOnly:
+ if self._isSideMenuMatch(asSubItem[1], sActionParam):
+ sSideMenuItems += '<li class="current_page_item">';
+ else:
+ sSideMenuItems += '<li>';
+ sSideMenuItems += '<a href="' + webutils.escapeAttr(asSubItem[1]) + '">' \
+ + webutils.escapeElem(asSubItem[0]) + '</a></li>\n';
+ else:
+ sSideMenuItems += '<li class="subheader_item">' + webutils.escapeElem(asSubItem[0]) + '</li>';
+ return (sTopMenuItems, sSideMenuItems);
+
+ def _generatePage(self):
+ """
+ Generates the page using _sTemplate, _sPageTitle, _aaoMenus, and _sPageBody.
+ """
+ assert self._sRedirectTo is None;
+
+ #
+ # Build the replacement string dictionary.
+ #
+
+ # Provide basic auth log out for browsers that supports it.
+ sUserAgent = self._oSrvGlue.getUserAgent();
+ if sUserAgent.startswith('Mozilla/') and sUserAgent.find('Firefox') > 0:
+ # Log in as the logout user in the same realm, the browser forgets
+ # the old login and the job is done. (see apache sample conf)
+ sLogOut = ' (<a href="%s://logout:logout@%s%slogout.py">logout</a>)' \
+ % (self._oSrvGlue.getUrlScheme(), self._oSrvGlue.getUrlNetLoc(), self._oSrvGlue.getUrlBasePath());
+ elif sUserAgent.startswith('Mozilla/') and sUserAgent.find('Safari') > 0:
+ # For a 401, causing the browser to forget the old login. Works
+ # with safari as well as the two above. Since safari consider the
+ # above method a phishing attempt and displays a warning to that
+ # effect, which when taken seriously aborts the logout, this method
+ # is preferable, even if it throws logon boxes in the user's face
+ # till he/she/it hits escape, because it always works.
+ sLogOut = ' (<a href="logout2.py">logout</a>)'
+ elif (sUserAgent.startswith('Mozilla/') and sUserAgent.find('MSIE') > 0) \
+ or (sUserAgent.startswith('Mozilla/') and sUserAgent.find('Chrome') > 0):
+ ## There doesn't seem to be any way to make IE really log out
+ # without using a cookie and systematically 401 accesses based on
+ # some logout state associated with it. Not sure how secure that
+ # can be made and we really want to avoid cookies. So, perhaps,
+ # just avoid IE for now. :-)
+ ## Chrome/21.0 doesn't want to log out either.
+ sLogOut = ''
+ else:
+ sLogOut = ''
+
+ # Prep Menus.
+ (sTopMenuItems, sSideMenuItems) = self._generateMenus();
+
+ # The dictionary (max variable length is 28 chars (see further down)).
+ dReplacements = {
+ '@@PAGE_TITLE@@': self._sPageTitle,
+ '@@LOG_OUT@@': sLogOut,
+ '@@TESTMANAGER_VERSION@@': config.g_ksVersion,
+ '@@TESTMANAGER_REVISION@@': config.g_ksRevision,
+ '@@BASE_URL@@': self._oSrvGlue.getBaseUrl(),
+ '@@TOP_MENU_ITEMS@@': sTopMenuItems,
+ '@@SIDE_MENU_ITEMS@@': sSideMenuItems,
+ '@@SIDE_FILTER_CONTROL@@': self._sPageFilter,
+ '@@SIDE_MENU_FORM_ATTRS@@': '',
+ '@@PAGE_BODY@@': self._sPageBody,
+ '@@DEBUG@@': '',
+ };
+
+ # Side menu form attributes.
+ if self._dSideMenuFormAttrs:
+ dReplacements['@@SIDE_MENU_FORM_ATTRS@@'] = ' '.join(['%s="%s"' % (sKey, webutils.escapeAttr(sValue))
+ for sKey, sValue in self._dSideMenuFormAttrs.items()]);
+
+ # Special current user handling.
+ if self._oCurUser is not None:
+ dReplacements['@@USER_NAME@@'] = self._oCurUser.sUsername;
+ else:
+ dReplacements['@@USER_NAME@@'] = 'unauthorized user "' + self._oSrvGlue.getLoginName() + '"';
+
+ # Prep debug section.
+ if self._sDebug == '':
+ if config.g_kfWebUiSqlTrace or self._fDbgSqlTrace or self._fDbgSqlExplain:
+ self._sDebug = '<h3>Processed in %s ns.</h3>\n%s\n' \
+ % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,),
+ self._oDb.debugHtmlReport(self._oSrvGlue.tsStart));
+ elif config.g_kfWebUiProcessedIn:
+ self._sDebug = '<h3>Processed in %s ns.</h3>\n' \
+ % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,), );
+ if config.g_kfWebUiDebugPanel:
+ self._sDebug += self._debugRenderPanel();
+ if self._sDebug != '':
+ dReplacements['@@DEBUG@@'] = u'<div id="debug"><br><br><hr/>' \
+ + (utils.toUnicode(self._sDebug, errors='ignore') if isinstance(self._sDebug, str)
+ else self._sDebug) \
+ + u'</div>\n';
+
+ #
+ # Load the template.
+ #
+ with open(os.path.join(self._oSrvGlue.pathTmWebUI(), self._sTemplate)) as oFile: # pylint: disable=unspecified-encoding
+ sTmpl = oFile.read();
+
+ #
+ # Process the template, outputting each part we process.
+ #
+ offStart = 0;
+ offCur = 0;
+ while offCur < len(sTmpl):
+ # Look for a replacement variable.
+ offAtAt = sTmpl.find('@@', offCur);
+ if offAtAt < 0:
+ break;
+ offCur = offAtAt + 2;
+ if sTmpl[offCur] not in string.ascii_uppercase:
+ continue;
+ offEnd = sTmpl.find('@@', offCur, offCur+28);
+ if offEnd <= 0:
+ continue;
+ offCur = offEnd;
+ sReplacement = sTmpl[offAtAt:offEnd+2];
+ if sReplacement in dReplacements:
+ # Got a match! Write out the previous chunk followed by the replacement text.
+ if offStart < offAtAt:
+ self._oSrvGlue.write(sTmpl[offStart:offAtAt]);
+ self._oSrvGlue.write(dReplacements[sReplacement]);
+ # Advance past the replacement point in the template.
+ offCur += 2;
+ offStart = offCur;
+ else:
+ assert False, 'Unknown replacement "%s" at offset %s in %s' % (sReplacement, offAtAt, self._sTemplate );
+
+ # The final chunk.
+ if offStart < len(sTmpl):
+ self._oSrvGlue.write(sTmpl[offStart:]);
+
+ return True;
+
+ #
+ # Interface for WuiContentBase classes.
+ #
+
+ def getUrlNoParams(self):
+ """
+ Returns the base URL without any parameters (no trailing '?' or &).
+ """
+ return self._sUrlBase[:self._sUrlBase.rindex('?')];
+
+ def getUrlBase(self):
+ """
+ Returns the base URL, ending with '?' or '&'.
+ This may already include some debug parameters.
+ """
+ return self._sUrlBase;
+
+ def getParameters(self):
+ """
+ Returns a (shallow) copy of the request parameter dictionary.
+ """
+ return self._dParams.copy();
+
+ def getDb(self):
+ """
+ Returns the database connection.
+ """
+ return self._oDb;
+
+ def getNow(self):
+ """
+ Returns the effective date.
+ """
+ return self._tsNow;
+
+
+ #
+ # Parameter handling.
+ #
+
+ def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
+ """
+ Gets a string parameter.
+ Raises exception if not found and sDefault is None.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+ sValue = self._dParams[sName];
+ if isinstance(sValue, list):
+ raise WuiException('%s parameter "%s" is given multiple times: "%s"' % (self._sAction, sName, sValue));
+ sValue = sValue.strip();
+ elif sDefault is None and fAllowNull is not True:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,));
+ else:
+ sValue = sDefault;
+
+ if asValidValues is not None and sValue not in asValidValues:
+ raise WuiException('%s parameter %s value "%s" not in %s '
+ % (self._sAction, sName, sValue, asValidValues));
+ return sValue;
+
+ def getBoolParam(self, sName, fDefault = None):
+ """
+ Gets a boolean parameter.
+ Raises exception if not found and fDefault is None, or if not a valid boolean.
+ """
+ sValue = self.getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'],
+ '0' if fDefault is None else str(fDefault));
+ # HACK: Checkboxes doesn't return a value when unchecked, so we always
+ # provide a default when dealing with boolean parameters.
+ return sValue in ('True', 'true', '1',);
+
+ def getIntParam(self, sName, iMin = None, iMax = None, iDefault = None):
+ """
+ Gets a integer parameter.
+ Raises exception if not found and iDefault is None, if not a valid int,
+ or if outside the range defined by iMin and iMax.
+ """
+ if iDefault is not None and sName not in self._dParams:
+ return iDefault;
+
+ sValue = self.getStringParam(sName, None, None if iDefault is None else str(iDefault));
+ try:
+ iValue = int(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, iValue, iMin, iMax));
+ return iValue;
+
+ def getLongParam(self, sName, lMin = None, lMax = None, lDefault = None):
+ """
+ Gets a long integer parameter.
+ Raises exception if not found and lDefault is None, if not a valid long,
+ or if outside the range defined by lMin and lMax.
+ """
+ if lDefault is not None and sName not in self._dParams:
+ return lDefault;
+
+ sValue = self.getStringParam(sName, None, None if lDefault is None else str(lDefault));
+ try:
+ lValue = long(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (lMin is not None and lValue < lMin) \
+ or (lMax is not None and lValue > lMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, lValue, lMin, lMax));
+ return lValue;
+
+ def getTsParam(self, sName, tsDefault = None, fRequired = True):
+ """
+ Gets a timestamp parameter.
+ Raises exception if not found and fRequired is True.
+ """
+ if fRequired is False and sName not in self._dParams:
+ return tsDefault;
+
+ sValue = self.getStringParam(sName, None, None if tsDefault is None else str(tsDefault));
+ (sValue, sError) = ModelDataBase.validateTs(sValue);
+ if sError is not None:
+ raise WuiException('%s parameter %s value "%s": %s'
+ % (self._sAction, sName, sValue, sError));
+ return sValue;
+
+ def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
+ """
+ Gets parameter list.
+ Raises exception if not found and aiDefaults is None, or if any of the
+ values are not valid integers or outside the range defined by iMin and iMax.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if isinstance(self._dParams[sName], list):
+ asValues = self._dParams[sName];
+ else:
+ asValues = [self._dParams[sName],];
+ aiValues = [];
+ for sValue in asValues:
+ try:
+ iValue = int(sValue);
+ except:
+ raise WuiException('%s parameter %s value "%s" cannot be convert to an integer'
+ % (self._sAction, sName, sValue));
+
+ if (iMin is not None and iValue < iMin) \
+ or (iMax is not None and iValue > iMax):
+ raise WuiException('%s parameter %s value %d is out of range [%s..%s]'
+ % (self._sAction, sName, iValue, iMin, iMax));
+ aiValues.append(iValue);
+ else:
+ aiValues = aiDefaults;
+
+ return aiValues;
+
+ def getListOfStrParams(self, sName, asDefaults = None):
+ """
+ Gets parameter list.
+ Raises exception if not found and asDefaults is None.
+ """
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ if isinstance(self._dParams[sName], list):
+ asValues = [str(s).strip() for s in self._dParams[sName]];
+ else:
+ asValues = [str(self._dParams[sName]).strip(), ];
+ elif asDefaults is None:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,));
+ else:
+ asValues = asDefaults;
+
+ return asValues;
+
+ def getListOfTestCasesParam(self, sName, asDefaults = None): # too many local vars - pylint: disable=too-many-locals
+ """Get list of test cases and their parameters"""
+ if sName in self._dParams:
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName)
+
+ aoListOfTestCases = []
+
+ aiSelectedTestCaseIds = self.getListOfIntParams('%s[asCheckedTestCases]' % sName, aiDefaults=[])
+ aiAllTestCases = self.getListOfIntParams('%s[asAllTestCases]' % sName, aiDefaults=[])
+
+ for idTestCase in aiAllTestCases:
+ aiCheckedTestCaseArgs = \
+ self.getListOfIntParams(
+ '%s[%d][asCheckedTestCaseArgs]' % (sName, idTestCase),
+ aiDefaults=[])
+
+ aiAllTestCaseArgs = \
+ self.getListOfIntParams(
+ '%s[%d][asAllTestCaseArgs]' % (sName, idTestCase),
+ aiDefaults=[])
+
+ oListEntryTestCaseArgs = []
+ for idTestCaseArgs in aiAllTestCaseArgs:
+ fArgsChecked = idTestCaseArgs in aiCheckedTestCaseArgs;
+
+ # Dry run
+ sPrefix = '%s[%d][%d]' % (sName, idTestCase, idTestCaseArgs,);
+ self.getIntParam(sPrefix + '[idTestCaseArgs]', iDefault = -1,)
+
+ sArgs = self.getStringParam(sPrefix + '[sArgs]', sDefault = '')
+ cSecTimeout = self.getStringParam(sPrefix + '[cSecTimeout]', sDefault = '')
+ cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '')
+ cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '')
+
+ oListEntryTestCaseArgs.append((fArgsChecked, idTestCaseArgs, sArgs, cSecTimeout, cGangMembers))
+
+ sTestCaseName = self.getStringParam('%s[%d][sName]' % (sName, idTestCase), sDefault='')
+
+ oListEntryTestCase = (
+ idTestCase,
+ idTestCase in aiSelectedTestCaseIds,
+ sTestCaseName,
+ oListEntryTestCaseArgs
+ );
+
+ aoListOfTestCases.append(oListEntryTestCase)
+
+ if not aoListOfTestCases:
+ if asDefaults is None:
+ raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName))
+ aoListOfTestCases = asDefaults
+
+ return aoListOfTestCases
+
+ def getEffectiveDateParam(self, sParamName = None):
+ """
+ Gets the effective date parameter.
+
+ Returns a timestamp suitable for database and url parameters.
+ Returns None if not found or empty.
+
+ The first call with sParamName set to None will set the internal _tsNow
+ value upon successfull return.
+ """
+
+ sName = sParamName if sParamName is not None else WuiDispatcherBase.ksParamEffectiveDate
+
+ if sName not in self._dParams:
+ return None;
+
+ if sName not in self._asCheckedParams:
+ self._asCheckedParams.append(sName);
+
+ sValue = self._dParams[sName];
+ if isinstance(sValue, list):
+ raise WuiException('%s parameter "%s" is given multiple times: %s' % (self._sAction, sName, sValue));
+ sValue = sValue.strip();
+ if sValue == '':
+ return None;
+
+ #
+ # Timestamp, just validate it and return.
+ #
+ if sValue[0] not in ['-', '+']:
+ (sValue, sError) = ModelDataBase.validateTs(sValue);
+ if sError is not None:
+ raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError));
+ if sParamName is None and self._tsNow is None:
+ self._tsNow = sValue;
+ return sValue;
+
+ #
+ # Relative timestamp. Validate and convert it to a fixed timestamp.
+ #
+ chSign = sValue[0];
+ (sValue, sError) = ModelDataBase.validateTs(sValue[1:], fRelative = True);
+ if sError is not None:
+ raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError));
+ if sValue[-6] in ['-', '+']:
+ raise WuiException('%s parameter "%s" ("%s") is a relative timestamp but incorrectly includes a time zone.'
+ % (self._sAction, sName, sValue));
+ offTime = 11;
+ if sValue[offTime - 1] != ' ':
+ raise WuiException('%s parameter "%s" ("%s") incorrect format.' % (self._sAction, sName, sValue));
+ sInterval = 'P' + sValue[:(offTime - 1)] + 'T' + sValue[offTime:];
+
+ self._oDb.execute('SELECT CURRENT_TIMESTAMP ' + chSign + ' \'' + sInterval + '\'::INTERVAL');
+ oDate = self._oDb.fetchOne()[0];
+
+ sValue = str(oDate);
+ if sParamName is None and self._tsNow is None:
+ self._tsNow = sValue;
+ return sValue;
+
+ def getRedirectToParameter(self, sDefault = None):
+ """
+ Gets the special redirect to parameter if it exists, will Return default
+ if not, with None being a valid default.
+
+ Makes sure the it doesn't got offsite.
+ Raises exception if invalid.
+ """
+ if sDefault is not None or self.ksParamRedirectTo in self._dParams:
+ sValue = self.getStringParam(self.ksParamRedirectTo, sDefault = sDefault);
+ cch = sValue.find("?");
+ if cch < 0:
+ cch = sValue.find("#");
+ if cch < 0:
+ cch = len(sValue);
+ for ch in (':', '/', '\\', '..'):
+ if sValue.find(ch, 0, cch) >= 0:
+ raise WuiException('Invalid character (%c) in redirect-to url: %s' % (ch, sValue,));
+ else:
+ sValue = None;
+ return sValue;
+
+
+ def _checkForUnknownParameters(self):
+ """
+ Check if we've handled all parameters, raises exception if anything
+ unknown was found.
+ """
+
+ if len(self._asCheckedParams) != len(self._dParams):
+ sUnknownParams = '';
+ for sKey in self._dParams:
+ if sKey not in self._asCheckedParams:
+ sUnknownParams += ' ' + sKey + '=' + str(self._dParams[sKey]);
+ raise WuiException('Unknown parameters: ' + sUnknownParams);
+
+ return True;
+
+ def _assertPostRequest(self):
+ """
+ Makes sure that the request we're dispatching is a POST request.
+ Raises an exception of not.
+ """
+ if self._oSrvGlue.getMethod() != 'POST':
+ raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),))
+ return True;
+
+ #
+ # Client browser type.
+ #
+
+ ## @name Browser types.
+ ## @{
+ ksBrowserFamily_Unknown = 0;
+ ksBrowserFamily_Gecko = 1;
+ ksBrowserFamily_Webkit = 2;
+ ksBrowserFamily_Trident = 3;
+ ## @}
+
+ ## @name Browser types.
+ ## @{
+ ksBrowserType_FamilyMask = 0xff;
+ ksBrowserType_Unknown = 0;
+ ksBrowserType_Firefox = (1 << 8) | ksBrowserFamily_Gecko;
+ ksBrowserType_Chrome = (2 << 8) | ksBrowserFamily_Webkit;
+ ksBrowserType_Safari = (3 << 8) | ksBrowserFamily_Webkit;
+ ksBrowserType_IE = (4 << 8) | ksBrowserFamily_Trident;
+ ## @}
+
+ def getBrowserType(self):
+ """
+ Gets the browser type.
+ The browser family can be extracted from this using ksBrowserType_FamilyMask.
+ """
+ sAgent = self._oSrvGlue.getUserAgent();
+ if sAgent.find('AppleWebKit/') > 0:
+ if sAgent.find('Chrome/') > 0:
+ return self.ksBrowserType_Chrome;
+ if sAgent.find('Safari/') > 0:
+ return self.ksBrowserType_Safari;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Webkit;
+ if sAgent.find('Gecko/') > 0:
+ if sAgent.find('Firefox/') > 0:
+ return self.ksBrowserType_Firefox;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Gecko;
+ return self.ksBrowserType_Unknown | self.ksBrowserFamily_Unknown;
+
+ def isBrowserGecko(self, sMinVersion = None):
+ """ Returns true if it's a gecko based browser. """
+ if (self.getBrowserType() & self.ksBrowserType_FamilyMask) != self.ksBrowserFamily_Gecko:
+ return False;
+ if sMinVersion is not None:
+ sAgent = self._oSrvGlue.getUserAgent();
+ sVersion = sAgent[sAgent.find('Gecko/')+6:].split()[0];
+ if sVersion < sMinVersion:
+ return False;
+ return True;
+
+ #
+ # User related stuff.
+ #
+
+ def isReadOnlyUser(self):
+ """ Returns true if the logged in user is read-only or if no user is logged in. """
+ return self._oCurUser is None or self._oCurUser.fReadOnly;
+
+
+ #
+ # Debugging
+ #
+
+ def _debugProcessDispatch(self):
+ """
+ Processes any debugging parameters in the request and adds them to
+ _asCheckedParams so they won't cause trouble in the action handler.
+ """
+
+ self._fDbgSqlTrace = self.getBoolParam(self.ksParamDbgSqlTrace, False);
+ self._fDbgSqlExplain = self.getBoolParam(self.ksParamDbgSqlExplain, False);
+
+ if self._fDbgSqlExplain:
+ self._oDb.debugEnableExplain();
+
+ return True;
+
+ def _debugRenderPanel(self):
+ """
+ Renders a simple form for controlling WUI debugging.
+
+ Returns the HTML for it.
+ """
+
+ sHtml = '<div id="debug-panel">\n' \
+ ' <form id="debug-panel-form" method="get" action="#">\n';
+
+ for sKey, oValue in self._dParams.items():
+ if sKey not in self.kasDbgParams:
+ if hasattr(oValue, 'startswith'):
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oValue),);
+ else:
+ for oSubValue in oValue:
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oSubValue),);
+
+ for aoCheckBox in (
+ [self.ksParamDbgSqlTrace, self._fDbgSqlTrace, 'SQL trace'],
+ [self.ksParamDbgSqlExplain, self._fDbgSqlExplain, 'SQL explain'], ):
+ sHtml += ' <input type="checkbox" name="%s" value="1"%s />%s\n' \
+ % (aoCheckBox[0], ' checked' if aoCheckBox[1] else '', aoCheckBox[2]);
+
+ sHtml += ' <button type="submit">Apply</button>\n';
+ sHtml += ' </form>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+ def _debugGetParameters(self):
+ """
+ Gets a dictionary with the debug parameters.
+
+ For use when links are constructed from scratch instead of self._dParams.
+ """
+ return self._dDbgParams;
+
+ #
+ # Dispatching
+ #
+
+ def _actionDefault(self):
+ """The default action handler, always overridden. """
+ raise WuiException('The child class shall override WuiBase.actionDefault().')
+
+ def _actionGenericListing(self, oLogicType, oListContentType):
+ """
+ Generic listing action.
+
+ oLogicType implements fetchForListing.
+ oListContentType is a child of WuiListContentBase.
+ """
+ tsEffective = self.getEffectiveDateParam();
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns,
+ iMin = -getattr(oLogicType, 'kcMaxSortColumns', 0) + 1,
+ iMax = getattr(oLogicType, 'kcMaxSortColumns', 0), aiDefaults = []);
+ aiSortColumns = [];
+ for iSortColumn in aiSortColumnsDup:
+ if iSortColumn not in aiSortColumns:
+ aiSortColumns.append(iSortColumn);
+ self._checkForUnknownParameters();
+
+ ## @todo fetchForListing could be made more useful if it returned a tuple
+ # that includes the total number of entries, thus making paging more user
+ # friendly (known number of pages). So, the return should be:
+ # (aoEntries, cAvailableEntries)
+ #
+ # In addition, we could add a new parameter to include deleted entries,
+ # making it easier to find old deleted testboxes/testcases/whatever and
+ # clone them back to life. The temporal navigation is pretty usless here.
+ #
+ aoEntries = oLogicType(self._oDb).fetchForListing(iPage * cItemsPerPage, cItemsPerPage + 1, tsEffective, aiSortColumns);
+ oContent = oListContentType(aoEntries, iPage, cItemsPerPage, tsEffective,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self, aiSelectedSortColumns = aiSortColumns);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionGenericFormAdd(self, oDataType, oFormType, sRedirectTo = None):
+ """
+ Generic add something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oData = oDataType().initFromParams(oDisp = self, fStrict = False);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+
+ oForm = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oForm.showForm();
+ return True
+
+ def _actionGenericFormDetails(self, oDataType, oLogicType, oFormType, sIdAttr = None, sGenIdAttr = None): # pylint: disable=too-many-locals
+ """
+ Generic handler for showing a details form/page.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType may implement fetchForChangeLog.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter (not idGen!).
+ """
+ # Input.
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ if sIdAttr is None:
+ sIdAttr = oDataType.ksIdAttr;
+ if sGenIdAttr is None:
+ sGenIdAttr = getattr(oDataType, 'ksGenIdAttr', None);
+
+ # Parameters.
+ idGenObject = -1;
+ if sGenIdAttr is not None:
+ idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1);
+ if idGenObject != -1:
+ idObject = tsNow = None;
+ else:
+ idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1);
+ tsNow = self.getEffectiveDateParam();
+ fChangeLog = self.getBoolParam(WuiDispatcherBase.ksParamChangeLogEnabled, True);
+ iChangeLogPageNo = self.getIntParam(WuiDispatcherBase.ksParamChangeLogPageNo, 0, 9999, 0);
+ cChangeLogEntriesPerPage = self.getIntParam(WuiDispatcherBase.ksParamChangeLogEntriesPerPage, 2, 9999, 4);
+ self._checkForUnknownParameters();
+
+ # Fetch item and display it.
+ if idGenObject == -1:
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow);
+ else:
+ oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject);
+
+ oContent = oFormType(oData, oFormType.ksMode_Show, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+
+ # Add change log if supported.
+ if fChangeLog and hasattr(oLogicType, 'fetchForChangeLog'):
+ (aoEntries, fMore) = oLogicType(self._oDb).fetchForChangeLog(getattr(oData, sIdAttr),
+ iChangeLogPageNo * cChangeLogEntriesPerPage,
+ cChangeLogEntriesPerPage ,
+ tsNow);
+ self._sPageBody += oContent.showChangeLog(aoEntries, fMore, iChangeLogPageNo, cChangeLogEntriesPerPage, tsNow);
+ return True
+
+ def _actionGenericDoRemove(self, oLogicType, sParamId, sRedirAction):
+ """
+ Delete entry (using oLogicType.removeEntry).
+
+ oLogicType is a class that implements addEntry.
+
+ sParamId is the name (ksParam_...) of the HTTP variable hold the ID of
+ the database entry to delete.
+
+ sRedirAction is what action to redirect to on success.
+ """
+ import cgitb;
+
+ idEntry = self.getIntParam(sParamId, iMin = 1, iMax = 0x7ffffffe)
+ fCascade = self.getBoolParam('fCascadeDelete', False);
+ sRedirectTo = self.getRedirectToParameter(self._sActionUrlBase + sRedirAction);
+ self._checkForUnknownParameters()
+
+ try:
+ if self.isReadOnlyUser():
+ raise Exception('"%s" is a read only user!' % (self._oCurUser.sUsername,));
+ self._sPageTitle = None
+ self._sPageBody = None
+ self._sRedirectTo = sRedirectTo;
+ return oLogicType(self._oDb).removeEntry(self._oCurUser.uid, idEntry, fCascade = fCascade, fCommit = True);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ self._sPageTitle = 'Unable to delete entry';
+ self._sPageBody = str(oXcpt);
+ if config.g_kfDebugDbXcpt:
+ self._sPageBody += cgitb.html(sys.exc_info());
+ self._sRedirectTo = None;
+ return False;
+
+ def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName = None, sRedirectTo = None):
+ """
+ Generic edit something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter (not idGen!).
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ if sIdParamName is None:
+ sIdParamName = getattr(oDataType, 'ksParam_' + oDataType.ksIdAttr);
+ assert len(sIdParamName) > 1;
+
+ tsNow = self.getEffectiveDateParam();
+ idObject = self.getIntParam(sIdParamName, 0, 0x7ffffffe);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow = tsNow);
+
+ oContent = oFormType(oData, oFormType.ksMode_Edit, oDisp = self);
+ oContent.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm();
+ return True
+
+ def _actionGenericFormEditL(self, oCoreObjectLogic, sCoreObjectIdFieldName, oWuiObjectLogic):
+ """
+ Generic modify something form display request handler.
+
+ @param oCoreObjectLogic A *Logic class
+
+ @param sCoreObjectIdFieldName Name of HTTP POST variable that
+ contains object ID information
+
+ @param oWuiObjectLogic Web interface renderer class
+ """
+
+ iCoreDataObjectId = self.getIntParam(sCoreObjectIdFieldName, 0, 0x7ffffffe, -1)
+ self._checkForUnknownParameters();
+
+ ## @todo r=bird: This will return a None object if the object wasn't found... Crash bang in the content generator
+ # code (that's not logic code btw.).
+ oData = oCoreObjectLogic(self._oDb).getById(iCoreDataObjectId)
+
+ # Instantiate and render the MODIFY dialog form
+ oContent = oWuiObjectLogic(oData, oWuiObjectLogic.ksMode_Edit, oDisp=self)
+
+ (self._sPageTitle, self._sPageBody) = oContent.showForm()
+
+ return True
+
+ def _actionGenericFormClone(self, oDataType, oFormType, sIdAttr, sGenIdAttr = None):
+ """
+ Generic clone something form display request handler.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ sIdParamName is the name of the ID parameter.
+ sGenIdParamName is the name of the generation ID parameter, None if not applicable.
+ """
+ # Input.
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ # Parameters.
+ idGenObject = -1;
+ if sGenIdAttr is not None:
+ idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1);
+ if idGenObject != -1:
+ idObject = tsNow = None;
+ else:
+ idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1);
+ tsNow = self.getEffectiveDateParam();
+ self._checkForUnknownParameters();
+
+ # Fetch data and clear identifying attributes not relevant to the clone.
+ if idGenObject != -1:
+ oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject);
+ else:
+ oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow);
+
+ setattr(oData, sIdAttr, None);
+ if sGenIdAttr is not None:
+ setattr(oData, sGenIdAttr, None);
+ oData.tsEffective = None;
+ oData.tsExpire = None;
+
+ # Display form.
+ oContent = oFormType(oData, oFormType.ksMode_Add, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.showForm()
+ return True
+
+
+ def _actionGenericFormPost(self, sMode, fnLogicAction, oDataType, oFormType, sRedirectTo, fStrict = True):
+ """
+ Generic POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oFormType is a WuiFormContentBase child class.
+ fnLogicAction is a method taking a oDataType instance and uidAuthor as arguments.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ #
+ # Read and validate parameters.
+ #
+ oData = oDataType().initFromParams(oDisp = self, fStrict = fStrict);
+ sRedirectTo = self.getRedirectToParameter(sRedirectTo);
+ self._checkForUnknownParameters();
+ self._assertPostRequest();
+ if sMode == WuiFormContentBase.ksMode_Add and getattr(oData, 'kfIdAttrIsForForeign', False):
+ enmValidateFor = oData.ksValidateFor_AddForeignId;
+ elif sMode == WuiFormContentBase.ksMode_Add:
+ enmValidateFor = oData.ksValidateFor_Add;
+ else:
+ enmValidateFor = oData.ksValidateFor_Edit;
+ dErrors = oData.validateAndConvert(self._oDb, enmValidateFor);
+
+ # Check that the user can do this.
+ sErrorMsg = None;
+ assert self._oCurUser is not None;
+ if self.isReadOnlyUser():
+ sErrorMsg = 'User %s is not allowed to modify anything!' % (self._oCurUser.sUsername,)
+
+ if not dErrors and not sErrorMsg:
+ oData.convertFromParamNull();
+
+ #
+ # Try do the job.
+ #
+ try:
+ fnLogicAction(oData, self._oCurUser.uid, fCommit = True);
+ except Exception as oXcpt:
+ self._oDb.rollback();
+ oForm = oFormType(oData, sMode, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ sErrorMsg = str(oXcpt) if not config.g_kfDebugDbXcpt else '\n'.join(utils.getXcptInfo(4));
+ (self._sPageTitle, self._sPageBody) = oForm.showForm(sErrorMsg = sErrorMsg);
+ else:
+ #
+ # Worked, redirect to the specified page.
+ #
+ self._sPageTitle = None;
+ self._sPageBody = None;
+ self._sRedirectTo = sRedirectTo;
+ else:
+ oForm = oFormType(oData, sMode, oDisp = self);
+ oForm.setRedirectTo(sRedirectTo);
+ (self._sPageTitle, self._sPageBody) = oForm.showForm(dErrors = dErrors, sErrorMsg = sErrorMsg);
+ return True;
+
+ def _actionGenericFormAddPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict=True):
+ """
+ Generic add entry POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType is a class that implements addEntry.
+ oFormType is a WuiFormContentBase child class.
+ sRedirAction is what action to redirect to on success.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oLogic = oLogicType(self._oDb);
+ return self._actionGenericFormPost(WuiFormContentBase.ksMode_Add, oLogic.addEntry, oDataType, oFormType,
+ '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}), fStrict=fStrict)
+
+ def _actionGenericFormEditPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict = True):
+ """
+ Generic edit POST request handling from a WuiFormContentBase child.
+
+ oDataType is a ModelDataBase child class.
+ oLogicType is a class that implements addEntry.
+ oFormType is a WuiFormContentBase child class.
+ sRedirAction is what action to redirect to on success.
+ """
+ assert issubclass(oDataType, ModelDataBase);
+ assert issubclass(oLogicType, ModelLogicBase);
+ from testmanager.webui.wuicontentbase import WuiFormContentBase;
+ assert issubclass(oFormType, WuiFormContentBase);
+
+ oLogic = oLogicType(self._oDb);
+ return self._actionGenericFormPost(WuiFormContentBase.ksMode_Edit, oLogic.editEntry, oDataType, oFormType,
+ '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}),
+ fStrict = fStrict);
+
+ def _unauthorizedUser(self):
+ """
+ Displays the unauthorized user message (corresponding record is not
+ present in DB).
+ """
+
+ sLoginName = self._oSrvGlue.getLoginName();
+
+ # Report to system log
+ oSystemLogLogic = SystemLogLogic(self._oDb);
+ oSystemLogLogic.addEntry(SystemLogData.ksEvent_UserAccountUnknown,
+ 'Unknown user (%s) attempts to access from %s'
+ % (sLoginName, self._oSrvGlue.getClientAddr()),
+ 24, fCommit = True)
+
+ # Display message.
+ self._sPageTitle = 'User not authorized'
+ self._sPageBody = """
+ <p>Access denied for user <b>%s</b>.
+ Please contact an admin user to set up your access.</p>
+ """ % (sLoginName,)
+ return True;
+
+ def dispatchRequest(self):
+ """
+ Dispatches a request.
+ """
+
+ #
+ # Get the parameters and checks for duplicates.
+ #
+ try:
+ dParams = self._oSrvGlue.getParameters();
+ except Exception as oXcpt:
+ raise WuiException('Error retriving parameters: %s' % (oXcpt,));
+
+ for sKey in dParams.keys():
+
+ # Take care about strings which may contain unicode characters: convert percent-encoded symbols back to unicode.
+ for idxItem, _ in enumerate(dParams[sKey]):
+ dParams[sKey][idxItem] = utils.toUnicode(dParams[sKey][idxItem], 'utf-8');
+
+ if not len(dParams[sKey]) > 1:
+ dParams[sKey] = dParams[sKey][0];
+ self._dParams = dParams;
+
+ #
+ # Figure out the requested action and validate it.
+ #
+ if self.ksParamAction in self._dParams:
+ self._sAction = self._dParams[self.ksParamAction];
+ self._asCheckedParams.append(self.ksParamAction);
+ else:
+ self._sAction = self.ksActionDefault;
+
+ if isinstance(self._sAction, list) or self._sAction not in self._dDispatch:
+ raise WuiException('Unknown action "%s" requested' % (self._sAction,));
+
+ #
+ # Call action handler and generate the page (if necessary).
+ #
+ if self._oCurUser is not None:
+ self._debugProcessDispatch();
+ if self._dDispatch[self._sAction]() is self.ksDispatchRcAllDone:
+ return True;
+ else:
+ self._unauthorizedUser();
+
+ if self._sRedirectTo is None:
+ self._generatePage();
+ else:
+ self._redirectPage();
+ return True;
+
+
+ def dprint(self, sText):
+ """ Debug printing. """
+ if config.g_kfWebUiDebug:
+ self._oSrvGlue.dprint(sText);
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
new file mode 100755
index 00000000..c91e6036
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py
@@ -0,0 +1,1290 @@
+# -*- coding: utf-8 -*-
+# $Id: wuicontentbase.py $
+
+"""
+Test Manager Web-UI - Content Base Classes.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Standard python imports.
+import copy;
+import sys;
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager import config;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException;
+from testmanager.webui.wuihlpform import WuiHlpForm;
+from testmanager.core import db;
+from testmanager.core.base import AttributeChangeEntryPre;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiHtmlBase(object): # pylint: disable=too-few-public-methods
+ """
+ Base class for HTML objects.
+ """
+
+ def __init__(self):
+ """Dummy init to shut up pylint."""
+ pass; # pylint: disable=unnecessary-pass
+
+ def toHtml(self):
+
+ """
+ Must be overridden by sub-classes.
+ """
+ assert False;
+ return '';
+
+ def __str__(self):
+ """ String representation is HTML, simplifying formatting and such. """
+ return self.toHtml();
+
+
+class WuiLinkBase(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For passing links from WuiListContentBase._formatListEntry.
+ """
+
+ def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True, sExtraAttrs = ''):
+ WuiHtmlBase.__init__(self);
+ self.sName = sName
+ self.sUrl = sUrlBase
+ self.sConfirm = sConfirm;
+ self.sTitle = sTitle;
+ self.fBracketed = fBracketed;
+ self.sExtraAttrs = sExtraAttrs;
+
+ if dParams:
+ # Do some massaging of None arguments.
+ dParams = dict(dParams);
+ for sKey in dParams:
+ if dParams[sKey] is None:
+ dParams[sKey] = '';
+ self.sUrl += '?' + webutils.encodeUrlParams(dParams);
+
+ if sFragmentId is not None:
+ self.sUrl += '#' + sFragmentId;
+
+ def setBracketed(self, fBracketed):
+ """Changes the bracketing style."""
+ self.fBracketed = fBracketed;
+ return True;
+
+ def toHtml(self):
+ """
+ Returns a simple HTML anchor element.
+ """
+ sExtraAttrs = self.sExtraAttrs;
+ if self.sConfirm is not None:
+ sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),);
+ if self.sTitle is not None:
+ sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),);
+ if sExtraAttrs and sExtraAttrs[-1] != ' ':
+ sExtraAttrs += ' ';
+
+ sFmt = '[<a %shref="%s">%s</a>]';
+ if not self.fBracketed:
+ sFmt = '<a %shref="%s">%s</a>';
+ return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName));
+
+ @staticmethod
+ def estimateStringWidth(sString):
+ """
+ Takes a string and estimate it's width so the caller can pad with
+ U+2002 before tab in a title text. This is very very rough.
+ """
+ cchWidth = 0;
+ for ch in sString:
+ if ch.isupper() or ch in u'wm\u2007\u2003\u2001\u3000':
+ cchWidth += 2;
+ else:
+ cchWidth += 1;
+ return cchWidth;
+
+ @staticmethod
+ def getStringWidthPaddingEx(cchWidth, cchMaxWidth):
+ """ Works with estiamteStringWidth(). """
+ if cchWidth + 2 <= cchMaxWidth:
+ return u'\u2002' * ((cchMaxWidth - cchWidth) * 2 // 3)
+ return u'';
+
+ @staticmethod
+ def getStringWidthPadding(sString, cchMaxWidth):
+ """ Works with estiamteStringWidth(). """
+ return WuiLinkBase.getStringWidthPaddingEx(WuiLinkBase.estimateStringWidth(sString), cchMaxWidth);
+
+ @staticmethod
+ def padStringToWidth(sString, cchMaxWidth):
+ """ Works with estimateStringWidth. """
+ cchWidth = WuiLinkBase.estimateStringWidth(sString);
+ if cchWidth < cchMaxWidth:
+ return sString + WuiLinkBase.getStringWidthPaddingEx(cchWidth, cchMaxWidth);
+ return sString;
+
+
+class WuiTmLink(WuiLinkBase): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager. """
+
+ kdDbgParams = [];
+
+ def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True):
+
+ # Add debug parameters if necessary.
+ if self.kdDbgParams:
+ if not dParams:
+ dParams = dict(self.kdDbgParams);
+ else:
+ dParams = dict(dParams);
+ for sKey in self.kdDbgParams:
+ if sKey not in dParams:
+ dParams[sKey] = self.kdDbgParams[sKey];
+
+ WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed);
+
+
+class WuiAdminLink(WuiTmLink): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager's admin portion. """
+
+ def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None,
+ sFragmentId = None, fBracketed = True):
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ if not dParams:
+ dParams = {};
+ else:
+ dParams = dict(dParams);
+ if sAction is not None:
+ dParams[WuiAdmin.ksParamAction] = sAction;
+ if tsEffectiveDate is not None:
+ dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate;
+ WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+ sFragmentId = sFragmentId, fBracketed = fBracketed);
+
+class WuiMainLink(WuiTmLink): # pylint: disable=too-few-public-methods
+ """ Local link to the test manager's main portion. """
+
+ def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True):
+ if not dParams:
+ dParams = {};
+ else:
+ dParams = dict(dParams);
+ from testmanager.webui.wuimain import WuiMain;
+ if sAction is not None:
+ dParams[WuiMain.ksParamAction] = sAction;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
+ sFragmentId = sFragmentId, fBracketed = fBracketed);
+
+class WuiSvnLink(WuiLinkBase): # pylint: disable=too-few-public-methods
+ """
+ For linking to a SVN revision.
+ """
+ def __init__(self, iRevision, sName = None, fBracketed = True, sExtraAttrs = ''):
+ if sName is None:
+ sName = 'r%s' % (iRevision,);
+ WuiLinkBase.__init__(self, sName, config.g_ksTracLogUrlPrefix, { 'rev': iRevision,},
+ fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
+
+class WuiSvnLinkWithTooltip(WuiSvnLink): # pylint: disable=too-few-public-methods
+ """
+ For linking to a SVN revision with changelog tooltip.
+ """
+ def __init__(self, iRevision, sRepository, sName = None, fBracketed = True):
+ sExtraAttrs = ' onmouseover="return svnHistoryTooltipShow(event,\'%s\',%s);" onmouseout="return tooltipHide();"' \
+ % ( sRepository, iRevision, );
+ WuiSvnLink.__init__(self, iRevision, sName = sName, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
+
+class WuiBuildLogLink(WuiLinkBase):
+ """
+ For linking to a build log.
+ """
+ def __init__(self, sUrl, sName = None, fBracketed = True):
+ assert sUrl;
+ if sName is None:
+ sName = 'Build log';
+ if not webutils.hasSchema(sUrl):
+ WuiLinkBase.__init__(self, sName, config.g_ksBuildLogUrlPrefix + sUrl, fBracketed = fBracketed);
+ else:
+ WuiLinkBase.__init__(self, sName, sUrl, fBracketed = fBracketed);
+
+class WuiRawHtml(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For passing raw html from WuiListContentBase._formatListEntry.
+ """
+ def __init__(self, sHtml):
+ self.sHtml = sHtml;
+ WuiHtmlBase.__init__(self);
+
+ def toHtml(self):
+ return self.sHtml;
+
+class WuiHtmlKeeper(WuiHtmlBase): # pylint: disable=too-few-public-methods
+ """
+ For keeping a list of elements, concatenating their toHtml output together.
+ """
+ def __init__(self, aoInitial = None, sSep = ' '):
+ WuiHtmlBase.__init__(self);
+ self.sSep = sSep;
+ self.aoKept = [];
+ if aoInitial is not None:
+ if isinstance(aoInitial, WuiHtmlBase):
+ self.aoKept.append(aoInitial);
+ else:
+ self.aoKept.extend(aoInitial);
+
+ def append(self, oObject):
+ """ Appends one objects. """
+ self.aoKept.append(oObject);
+
+ def extend(self, aoObjects):
+ """ Appends a list of objects. """
+ self.aoKept.extend(aoObjects);
+
+ def toHtml(self):
+ return self.sSep.join(oObj.toHtml() for oObj in self.aoKept);
+
+class WuiSpanText(WuiRawHtml): # pylint: disable=too-few-public-methods
+ """
+ Outputs the given text within a span of the given CSS class.
+ """
+ def __init__(self, sSpanClass, sText, sTitle = None):
+ if sTitle is None:
+ WuiRawHtml.__init__(self,
+ u'<span class="%s">%s</span>'
+ % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),));
+ else:
+ WuiRawHtml.__init__(self,
+ u'<span class="%s" title="%s">%s</span>'
+ % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),));
+
+class WuiElementText(WuiRawHtml): # pylint: disable=too-few-public-methods
+ """
+ Outputs the given element text.
+ """
+ def __init__(self, sText):
+ WuiRawHtml.__init__(self, webutils.escapeElem(sText));
+
+
+class WuiContentBase(object): # pylint: disable=too-few-public-methods
+ """
+ Base for the content classes.
+ """
+
+ ## The text/symbol for a very short add link.
+ ksShortAddLink = u'\u2795'
+ ## HTML hex entity string for ksShortAddLink.
+ ksShortAddLinkHtml = '&#x2795;;'
+ ## The text/symbol for a very short edit link.
+ ksShortEditLink = u'\u270D'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortEditLinkHtml = '&#x270d;'
+ ## The text/symbol for a very short details link.
+ ksShortDetailsLink = u'\U0001f6c8\ufe0e'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortDetailsLinkHtml = '&#x1f6c8;;&#xfe0e;'
+ ## The text/symbol for a very short change log / details / previous page link.
+ ksShortChangeLogLink = u'\u2397'
+ ## HTML hex entity string for ksShortDetailsLink.
+ ksShortChangeLogLinkHtml = '&#x2397;'
+ ## The text/symbol for a very short reports link.
+ ksShortReportLink = u'\U0001f4ca\ufe0e'
+ ## HTML hex entity string for ksShortReportLink.
+ ksShortReportLinkHtml = '&#x1f4ca;&#xfe0e;'
+ ## The text/symbol for a very short test results link.
+ ksShortTestResultsLink = u'\U0001f5d0\ufe0e'
+
+
+ def __init__(self, fnDPrint = None, oDisp = None):
+ self._oDisp = oDisp; # WuiDispatcherBase.
+ self._fnDPrint = fnDPrint;
+ if fnDPrint is None and oDisp is not None:
+ self._fnDPrint = oDisp.dprint;
+
+ def dprint(self, sText):
+ """ Debug printing. """
+ if self._fnDPrint:
+ self._fnDPrint(sText);
+
+ @staticmethod
+ def formatTsShort(oTs):
+ """
+ Formats a timestamp (db rep) into a short form.
+ """
+ oTsZulu = db.dbTimestampToZuluDatetime(oTs);
+ sTs = oTsZulu.strftime('%Y-%m-%d %H:%M:%SZ');
+ return unicode(sTs).replace('-', u'\u2011').replace(' ', u'\u00a0');
+
+ def getNowTs(self):
+ """ Gets a database compatible current timestamp from python. See db.dbTimestampPythonNow(). """
+ return db.dbTimestampPythonNow();
+
+ def formatIntervalShort(self, oInterval):
+ """
+ Formats an interval (db rep) into a short form.
+ """
+ # default formatting for negative intervals.
+ if oInterval.days < 0:
+ return str(oInterval);
+
+ # Figure the hour, min and sec counts.
+ cHours = oInterval.seconds // 3600;
+ cMinutes = (oInterval.seconds % 3600) // 60;
+ cSeconds = oInterval.seconds - cHours * 3600 - cMinutes * 60;
+
+ # Tailor formatting to the interval length.
+ if oInterval.days > 0:
+ if oInterval.days > 1:
+ return '%d days, %d:%02d:%02d' % (oInterval.days, cHours, cMinutes, cSeconds);
+ return '1 day, %d:%02d:%02d' % (cHours, cMinutes, cSeconds);
+ if cMinutes > 0 or cSeconds >= 30 or cHours > 0:
+ return '%d:%02d:%02d' % (cHours, cMinutes, cSeconds);
+ if cSeconds >= 10:
+ return '%d.%ds' % (cSeconds, oInterval.microseconds // 100000);
+ if cSeconds > 0:
+ return '%d.%02ds' % (cSeconds, oInterval.microseconds // 10000);
+ return '%d ms' % (oInterval.microseconds // 1000,);
+
+ @staticmethod
+ def genericPageWalker(iCurItem, cItems, sHrefFmt, cWidth = 11, iBase = 1, sItemName = 'page'):
+ """
+ Generic page walker generator.
+
+ sHrefFmt has three %s sequences:
+ 1. The first is the page number link parameter (0-based).
+ 2. The title text, iBase-based number or text.
+ 3. The link text, iBase-based number or text.
+ """
+
+ # Calc display range.
+ iStart = 0 if iCurItem - cWidth // 2 <= cWidth // 4 else iCurItem - cWidth // 2;
+ iEnd = iStart + cWidth;
+ if iEnd > cItems:
+ iEnd = cItems;
+ if cItems > cWidth:
+ iStart = cItems - cWidth;
+
+ sHtml = u'';
+
+ # Previous page (using << >> because &laquo; and &raquo are too tiny).
+ if iCurItem > 0:
+ sHtml += '%s&nbsp;&nbsp;' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '&lt;&lt;');
+ else:
+ sHtml += '&lt;&lt;&nbsp;&nbsp;';
+
+ # 1 2 3 4...
+ if iStart > 0:
+ sHtml += '%s&nbsp; ... &nbsp;\n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),);
+
+ sHtml += '&nbsp;\n'.join(sHrefFmt % (i, '%s %d' % (sItemName, i + iBase), i + iBase) if i != iCurItem
+ else unicode(i + iBase)
+ for i in range(iStart, iEnd));
+ if iEnd < cItems:
+ sHtml += '&nbsp; ... &nbsp;%s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase));
+
+ # Next page.
+ if iCurItem + 1 < cItems:
+ sHtml += '&nbsp;&nbsp;%s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '&gt;&gt;');
+ else:
+ sHtml += '&nbsp;&nbsp;&gt;&gt;';
+
+ return sHtml;
+
+class WuiSingleContentBase(WuiContentBase): # pylint: disable=too-few-public-methods
+ """
+ Base for the content classes working on a single data object (oData).
+ """
+ def __init__(self, oData, oDisp = None, fnDPrint = None):
+ WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint);
+ self._oData = oData; # Usually ModelDataBase.
+
+
+class WuiFormContentBase(WuiSingleContentBase): # pylint: disable=too-few-public-methods
+ """
+ Base class for simple input form content classes (single data object).
+ """
+
+ ## @name Form mode.
+ ## @{
+ ksMode_Add = 'add';
+ ksMode_Edit = 'edit';
+ ksMode_Show = 'show';
+ ## @}
+
+ ## Default action mappings.
+ kdSubmitActionMappings = {
+ ksMode_Add: 'AddPost',
+ ksMode_Edit: 'EditPost',
+ };
+
+ def __init__(self, oData, sMode, sCoreName, oDisp, sTitle, sId = None, fEditable = True, sSubmitAction = None):
+ WuiSingleContentBase.__init__(self, copy.copy(oData), oDisp);
+ assert sMode in [self.ksMode_Add, self.ksMode_Edit, self.ksMode_Show];
+ assert len(sTitle) > 1;
+ assert sId is None or sId;
+
+ self._sMode = sMode;
+ self._sCoreName = sCoreName;
+ self._sActionBase = 'ksAction' + sCoreName;
+ self._sTitle = sTitle;
+ self._sId = sId if sId is not None else (type(oData).__name__.lower() + 'form');
+ self._fEditable = fEditable and (oDisp is None or not oDisp.isReadOnlyUser())
+ self._sSubmitAction = sSubmitAction;
+ if sSubmitAction is None and sMode != self.ksMode_Show:
+ self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]);
+ self._sRedirectTo = None;
+
+
+ def _populateForm(self, oForm, oData):
+ """
+ Populates the form. oData has parameter NULL values.
+ This must be reimplemented by the child.
+ """
+ _ = oForm; _ = oData;
+ raise Exception('Reimplement me!');
+
+ def _generatePostFormContent(self, oData):
+ """
+ Generate optional content that comes below the form.
+ Returns a list of tuples, where the first tuple element is the title
+ and the second the content. I.e. similar to show() output.
+ """
+ _ = oData;
+ return [];
+
+ @staticmethod
+ def _calcChangeLogEntryLinks(aoEntries, iEntry):
+ """
+ Returns an array of links to go with the change log entry.
+ """
+ _ = aoEntries; _ = iEntry;
+ ## @todo detect deletion and recreation.
+ ## @todo view details link.
+ ## @todo restore link (need new action)
+ ## @todo clone link.
+ return [];
+
+ @staticmethod
+ def _guessChangeLogEntryDescription(aoEntries, iEntry):
+ """
+ Guesses the action + author that caused the change log entry.
+ Returns descriptive string.
+ """
+ oEntry = aoEntries[iEntry];
+
+ # Figure the author of the change.
+ if oEntry.sAuthor is not None:
+ sAuthor = '%s (#%s)' % (oEntry.sAuthor, oEntry.uidAuthor,);
+ elif oEntry.uidAuthor is not None:
+ sAuthor = '#%d (??)' % (oEntry.uidAuthor,);
+ else:
+ sAuthor = None;
+
+ # Figure the action.
+ if oEntry.oOldRaw is None:
+ if sAuthor is None:
+ return 'Created by batch job.';
+ return 'Created by %s.' % (sAuthor,);
+
+ if sAuthor is None:
+ return 'Automatically updated.'
+ return 'Modified by %s.' % (sAuthor,);
+
+ @staticmethod
+ def formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams):
+ """
+ Formats one change log entry into one or more HTML table rows.
+
+ The sUrl and dParams arguments are used to construct links to historical
+ data using the tsEffective value. If no links wanted, they'll both be None.
+
+ Note! The parameters are given as array + index in case someone wishes
+ to access adjacent entries later in order to generate better
+ change descriptions.
+ """
+ oEntry = aoEntries[iEntry];
+
+ # Turn the effective date into a URL if we can:
+ if sUrl:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = oEntry.tsEffective;
+ sEffective = WuiLinkBase(WuiFormContentBase.formatTsShort(oEntry.tsEffective), sUrl,
+ dParams, fBracketed = False).toHtml();
+ else:
+ sEffective = webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective))
+
+ # The primary row.
+ sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
+ sContent = ' <tr class="%s">\n' \
+ ' <td rowspan="%d">%s</td>\n' \
+ ' <td rowspan="%d">%s</td>\n' \
+ ' <td colspan="3">%s%s</td>\n' \
+ ' </tr>\n' \
+ % ( sRowClass,
+ len(oEntry.aoChanges) + 1, sEffective,
+ len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)),
+ WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry),
+ ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),);
+
+ # Additional rows for each changed attribute.
+ j = 0;
+ for oChange in oEntry.aoChanges:
+ if isinstance(oChange, AttributeChangeEntryPre):
+ sContent += ' <tr class="%s%s"><td>%s</td>'\
+ '<td><div class="tdpre">%s%s%s</div></td>' \
+ '<td><div class="tdpre">%s%s%s</div></td></tr>\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even',
+ webutils.escapeElem(oChange.sAttr),
+ '<pre>' if oChange.sOldText else '',
+ webutils.escapeElem(oChange.sOldText),
+ '</pre>' if oChange.sOldText else '',
+ '<pre>' if oChange.sNewText else '',
+ webutils.escapeElem(oChange.sNewText),
+ '</pre>' if oChange.sNewText else '', );
+ else:
+ sContent += ' <tr class="%s%s"><td>%s</td><td>%s</td><td>%s</td></tr>\n' \
+ % ( sRowClass, 'odd' if j & 1 else 'even',
+ webutils.escapeElem(oChange.sAttr),
+ webutils.escapeElem(oChange.sOldText),
+ webutils.escapeElem(oChange.sNewText), );
+ j += 1;
+
+ return sContent;
+
+ def _showChangeLogNavi(self, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, sWhere):
+ """
+ Returns the HTML for the change log navigator.
+ Note! See also _generateNavigation.
+ """
+ sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
+ sNavigation += ' <table class="tmlistnavtab">\n' \
+ ' <tr>\n';
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
+ if tsNow is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = tsNow;
+
+ # Prev and combo box in one cell. Both inside the form for formatting reasons.
+ sNavigation += ' <td>\n' \
+ ' <form name="ChangeLogEntriesPerPageForm" method="GET">\n'
+
+ # Prev
+ if iPageNo > 0:
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1;
+ sNavigation += '<a href="?%s#tmchangelog">Previous</a>\n' \
+ % (webutils.encodeUrlParams(dParams),);
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
+ else:
+ sNavigation += 'Previous\n';
+
+ # Entries per page selector.
+ del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage];
+ sNavigation += '&nbsp; &nbsp;\n' \
+ ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value + \'#tmchangelog\';" ' \
+ 'title="Max change log entries per page">\n' \
+ % (WuiDispatcherBase.ksParamChangeLogEntriesPerPage,
+ webutils.encodeUrlParams(dParams),
+ WuiDispatcherBase.ksParamChangeLogEntriesPerPage);
+ dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
+
+ for iEntriesPerPage in [2, 4, 8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 8192]:
+ sNavigation += ' <option value="%d" %s>%d entries per page</option>\n' \
+ % ( iEntriesPerPage,
+ 'selected="selected"' if iEntriesPerPage == cEntriesPerPage else '',
+ iEntriesPerPage );
+ sNavigation += ' </select>\n';
+
+ # End of cell (and form).
+ sNavigation += ' </form>\n' \
+ ' </td>\n';
+
+ # Next
+ if fMoreEntries:
+ dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1;
+ sNavigation += ' <td><a href="?%s#tmchangelog">Next</a></td>\n' \
+ % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNavigation += ' <td>Next</td>\n';
+
+ sNavigation += ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+ return sNavigation;
+
+ def setRedirectTo(self, sRedirectTo):
+ """
+ For setting the hidden redirect-to field.
+ """
+ self._sRedirectTo = sRedirectTo;
+ return True;
+
+ def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True):
+ """
+ Render the change log, returning raw HTML.
+ aoEntries is an array of ChangeLogEntry.
+ """
+ sContent = '\n' \
+ '<hr>\n' \
+ '<div id="tmchangelog">\n' \
+ ' <h3>Change Log </h3>\n';
+ if fShowNavigation:
+ sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top');
+ sContent += ' <table class="tmtable tmchangelog">\n' \
+ ' <thead class="tmheader">' \
+ ' <tr>' \
+ ' <th rowspan="2">When</th>\n' \
+ ' <th rowspan="2">Expire (excl)</th>\n' \
+ ' <th colspan="3">Changes</th>\n' \
+ ' </tr>\n' \
+ ' <tr>\n' \
+ ' <th>Attribute</th>\n' \
+ ' <th>Old value</th>\n' \
+ ' <th>New value</th>\n' \
+ ' </tr>\n' \
+ ' </thead>\n' \
+ ' <tbody>\n';
+
+ if self._sMode == self.ksMode_Show:
+ sUrl = self._oDisp.getUrlNoParams();
+ dParams = self._oDisp.getParameters();
+ else:
+ sUrl = None;
+ dParams = None;
+
+ for iEntry, _ in enumerate(aoEntries):
+ sContent += self.formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams);
+
+ sContent += ' </tbody>\n' \
+ ' </table>\n';
+ if fShowNavigation and len(aoEntries) >= 8:
+ sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom');
+ sContent += '</div>\n\n';
+ return sContent;
+
+ def _generateTopRowFormActions(self, oData):
+ """
+ Returns a list of WuiTmLinks.
+ """
+ aoActions = [];
+ if self._sMode == self.ksMode_Show and self._fEditable:
+ # Remove _idGen and effective date since we're always editing the current data,
+ # and make sure the primary ID is present. Also remove change log stuff.
+ dParams = self._oDisp.getParameters();
+ if hasattr(oData, 'ksIdGenAttr'):
+ sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+ if sIdGenParam in dParams:
+ del dParams[sIdGenParam];
+ for sParam in [ WuiDispatcherBase.ksParamEffectiveDate, ] + list(WuiDispatcherBase.kasChangeLogParams):
+ if sParam in dParams:
+ del dParams[sParam];
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
+ aoActions.append(WuiTmLink('Edit', '', dParams));
+
+ # Add clone operation if available. This uses the same data selection as for showing details. No change log.
+ if hasattr(self._oDisp, self._sActionBase + 'Clone'):
+ dParams = self._oDisp.getParameters();
+ for sParam in WuiDispatcherBase.kasChangeLogParams:
+ if sParam in dParams:
+ del dParams[sParam];
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone');
+ aoActions.append(WuiTmLink('Clone', '', dParams));
+
+ elif self._sMode == self.ksMode_Edit:
+ # Details views the details at a given time, so we need either idGen or an effecive date + regular id.
+ dParams = {};
+ if hasattr(oData, 'ksIdGenAttr'):
+ sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
+ dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr);
+ elif hasattr(oData, 'tsEffective'):
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective;
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
+ aoActions.append(WuiTmLink('Details', '', dParams));
+
+ # Add delete operation if available.
+ if hasattr(self._oDisp, self._sActionBase + 'DoRemove'):
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'DoRemove');
+ dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
+ aoActions.append(WuiTmLink('Delete', '', dParams, sConfirm = "Are you absolutely sure?"));
+
+ return aoActions;
+
+ def showForm(self, dErrors = None, sErrorMsg = None):
+ """
+ Render the form.
+ """
+ oForm = WuiHlpForm(self._sId,
+ '?' + webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sSubmitAction}),
+ dErrors if dErrors is not None else {},
+ fReadOnly = self._sMode == self.ksMode_Show);
+
+ self._oData.convertToParamNull();
+
+ # If form cannot be constructed due to some reason we
+ # need to show this reason
+ try:
+ self._populateForm(oForm, self._oData);
+ if self._sRedirectTo is not None:
+ oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo);
+ except WuiException as oXcpt:
+ sContent = unicode(oXcpt)
+ else:
+ sContent = oForm.finalize();
+
+ # Add any post form content.
+ atPostFormContent = self._generatePostFormContent(self._oData);
+ if atPostFormContent:
+ for iSection, tSection in enumerate(atPostFormContent):
+ (sSectionTitle, sSectionContent) = tSection;
+ sContent += u'<div id="postform-%d" class="tmformpostsection">\n' % (iSection,);
+ if sSectionTitle:
+ sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),);
+ sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,);
+ sContent += sSectionContent;
+ sContent += u' </div>\n' \
+ u'</div>\n';
+
+ # Add action to the top.
+ aoActions = self._generateTopRowFormActions(self._oData);
+ if aoActions:
+ sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions));
+ sContent = sActionLinks + sContent;
+
+ # Add error info to the top.
+ if sErrorMsg is not None:
+ sContent = '<p class="tmerrormsg">' + webutils.escapeElem(sErrorMsg) + '</p>\n' + sContent;
+
+ return (self._sTitle, sContent);
+
+ def getListOfItems(self, asListItems = tuple(), asSelectedItems = tuple()):
+ """
+ Format generic list which should be used by HTML form
+ """
+ aoRet = []
+ for sListItem in asListItems:
+ fEnabled = sListItem in asSelectedItems;
+ aoRet.append((sListItem, fEnabled, sListItem))
+ return aoRet
+
+
+class WuiListContentBase(WuiContentBase):
+ """
+ Base for the list content classes.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments
+ sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None, fTimeNavigation = True):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying.
+ self._iPage = iPage;
+ self._cItemsPerPage = cItemsPerPage;
+ self._tsEffectiveDate = tsEffectiveDate;
+ self._fTimeNavigation = fTimeNavigation;
+ self._sTitle = sTitle; assert len(sTitle) > 1;
+ if sId is None:
+ sId = sTitle.strip().replace(' ', '').lower();
+ assert sId.strip();
+ self._sId = sId;
+ self._asColumnHeaders = [];
+ self._asColumnAttribs = [];
+ self._aaiColumnSorting = []; ##< list of list of integers
+ self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers
+
+ def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63):
+ """
+ Helper functions for formatting comment cell.
+ Returns None or WuiRawHtml instance.
+ """
+ # Nothing to do for empty comments.
+ if sComment is None:
+ return None;
+ sComment = sComment.strip();
+ if not sComment:
+ return None;
+
+ # Restrict the text if necessary, making the whole text available thru mouse-over.
+ ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size.
+ if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines:
+ sShortHtml = '';
+ for iLine, sLine in enumerate(sComment.split('\n')):
+ if iLine >= cMaxLines:
+ break;
+ if iLine > 0:
+ sShortHtml += '<br>\n';
+ if len(sLine) > cchMaxLine:
+ sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]);
+ sShortHtml += '...';
+ else:
+ sShortHtml += webutils.escapeElem(sLine);
+ return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,));
+
+ return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),));
+
+ def _formatListEntry(self, iEntry):
+ """
+ Formats the specified list entry as a list of column values.
+ Returns HTML for a table row.
+
+ The child class really need to override this!
+ """
+ # ASSUMES ModelDataBase children.
+ asRet = [];
+ for sAttr in self._aoEntries[0].getDataAttributes():
+ asRet.append(getattr(self._aoEntries[iEntry], sAttr));
+ return asRet;
+
+ def _formatListEntryHtml(self, iEntry):
+ """
+ Formats the specified list entry as HTML.
+ Returns HTML for a table row.
+
+ The child class can override this to
+ """
+ if (iEntry + 1) & 1:
+ sRow = u' <tr class="tmodd">\n';
+ else:
+ sRow = u' <tr class="tmeven">\n';
+
+ aoValues = self._formatListEntry(iEntry);
+ assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
+
+ for i, _ in enumerate(aoValues):
+ if i < len(self._asColumnAttribs) and self._asColumnAttribs[i]:
+ sRow += u' <td ' + self._asColumnAttribs[i] + '>';
+ else:
+ sRow += u' <td>';
+
+ if isinstance(aoValues[i], WuiHtmlBase):
+ sRow += aoValues[i].toHtml();
+ elif isinstance(aoValues[i], list):
+ if aoValues[i]:
+ for oElement in aoValues[i]:
+ if isinstance(oElement, WuiHtmlBase):
+ sRow += oElement.toHtml();
+ elif db.isDbTimestamp(oElement):
+ sRow += webutils.escapeElem(self.formatTsShort(oElement));
+ else:
+ sRow += webutils.escapeElem(unicode(oElement));
+ sRow += ' ';
+ elif db.isDbTimestamp(aoValues[i]):
+ sRow += webutils.escapeElem(self.formatTsShort(aoValues[i]));
+ elif db.isDbInterval(aoValues[i]):
+ sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i]));
+ elif aoValues[i] is not None:
+ sRow += webutils.escapeElem(unicode(aoValues[i]));
+
+ sRow += u'</td>\n';
+
+ return sRow + u' </tr>\n';
+
+ @staticmethod
+ def generateTimeNavigationComboBox(sWhere, dParams, tsEffective):
+ """
+ Generates the HTML for the xxxx ago combo box form.
+ """
+ sNavigation = '<form name="TmTimeNavCombo-%s" method="GET">\n' % (sWhere,);
+ sNavigation += ' <select name="%s" onchange="window.location=' % (WuiDispatcherBase.ksParamEffectiveDate);
+ sNavigation += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), WuiDispatcherBase.ksParamEffectiveDate)
+ sNavigation += 'this.options[this.selectedIndex].value;" title="Effective date">\n';
+
+ aoWayBackPoints = [
+ ('+0000-00-00 00:00:00.00', 'Now', ' title="Present Day. Present Time."'), # lain :)
+
+ ('-0000-00-00 01:00:00.00', '1 hour ago', ''),
+ ('-0000-00-00 02:00:00.00', '2 hours ago', ''),
+ ('-0000-00-00 03:00:00.00', '3 hours ago', ''),
+
+ ('-0000-00-01 00:00:00.00', '1 day ago', ''),
+ ('-0000-00-02 00:00:00.00', '2 days ago', ''),
+ ('-0000-00-03 00:00:00.00', '3 days ago', ''),
+
+ ('-0000-00-07 00:00:00.00', '1 week ago', ''),
+ ('-0000-00-14 00:00:00.00', '2 weeks ago', ''),
+ ('-0000-00-21 00:00:00.00', '3 weeks ago', ''),
+
+ ('-0000-01-00 00:00:00.00', '1 month ago', ''),
+ ('-0000-02-00 00:00:00.00', '2 months ago', ''),
+ ('-0000-03-00 00:00:00.00', '3 months ago', ''),
+ ('-0000-04-00 00:00:00.00', '4 months ago', ''),
+ ('-0000-05-00 00:00:00.00', '5 months ago', ''),
+ ('-0000-06-00 00:00:00.00', 'Half a year ago', ''),
+
+ ('-0001-00-00 00:00:00.00', '1 year ago', ''),
+ ]
+ fSelected = False;
+ for sTimestamp, sWayBackPointCaption, sExtraAttrs in aoWayBackPoints:
+ if sTimestamp == tsEffective:
+ fSelected = True;
+ sNavigation += ' <option value="%s"%s%s>%s</option>\n' \
+ % (webutils.quoteUrl(sTimestamp),
+ ' selected="selected"' if sTimestamp == tsEffective else '',
+ sExtraAttrs, sWayBackPointCaption, );
+ if not fSelected and tsEffective != '':
+ sNavigation += ' <option value="%s" selected>%s</option>\n' \
+ % (webutils.quoteUrl(tsEffective), WuiContentBase.formatTsShort(tsEffective))
+
+ sNavigation += ' </select>\n' \
+ '</form>\n';
+ return sNavigation;
+
+ @staticmethod
+ def generateTimeNavigationDateTime(sWhere, dParams, sNow):
+ """
+ Generates HTML for a form with date + time input fields.
+
+ Note! Modifies dParams!
+ """
+
+ #
+ # Date + time input fields. We use a java script helper to combine the two
+ # into a hidden field as there is no portable datetime input field type.
+ #
+ sNavigation = '<form method="get" action="?" onchange="timeNavigationUpdateHiddenEffDate(this,\'%s\')">' % (sWhere,);
+ if sNow is None:
+ sNow = utils.getIsoTimestamp();
+ else:
+ sNow = utils.normalizeIsoTimestampToZulu(sNow);
+ asSplit = sNow.split('T');
+ sNavigation += ' <input type="date" value="%s" id="EffDate%s"/> ' % (asSplit[0], sWhere, );
+ sNavigation += ' <input type="time" value="%s" id="EffTime%s"/> ' % (asSplit[1][:8], sWhere,);
+ sNavigation += ' <input type="hidden" name="%s" value="%s" id="EffDateTime%s"/>' \
+ % (WuiDispatcherBase.ksParamEffectiveDate, webutils.escapeAttr(sNow), sWhere);
+ for sKey in dParams:
+ sNavigation += ' <input type="hidden" name="%s" value="%s"/>' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(dParams[sKey]));
+ sNavigation += ' <input type="submit" value="Set"/>\n' \
+ '</form>\n';
+ return sNavigation;
+
+ ## @todo move to better place! WuiMain uses it.
+ @staticmethod
+ def generateTimeNavigation(sWhere, dParams, tsEffectiveAbs, sPreamble = '', sPostamble = '', fKeepPageNo = False):
+ """
+ Returns HTML for time navigation.
+
+ Note! Modifies dParams!
+ Note! Views without a need for a timescale just stubs this method.
+ """
+ sNavigation = '<div class="tmtimenav-%s tmtimenav">%s' % (sWhere, sPreamble,);
+
+ #
+ # Prepare the URL parameters.
+ #
+ if WuiDispatcherBase.ksParamPageNo in dParams: # Forget about page No when changing a period
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+ if not fKeepPageNo and WuiDispatcherBase.ksParamEffectiveDate in dParams:
+ tsEffectiveParam = dParams[WuiDispatcherBase.ksParamEffectiveDate];
+ del dParams[WuiDispatcherBase.ksParamEffectiveDate];
+ else:
+ tsEffectiveParam = ''
+
+ #
+ # Generate the individual parts.
+ #
+ sNavigation += WuiListContentBase.generateTimeNavigationDateTime(sWhere, dParams, tsEffectiveAbs);
+ sNavigation += WuiListContentBase.generateTimeNavigationComboBox(sWhere, dParams, tsEffectiveParam);
+
+ sNavigation += '%s</div>' % (sPostamble,);
+ return sNavigation;
+
+ def _generateTimeNavigation(self, sWhere, sPreamble = '', sPostamble = ''):
+ """
+ Returns HTML for time navigation.
+
+ Note! Views without a need for a timescale just stubs this method.
+ """
+ return self.generateTimeNavigation(sWhere, self._oDisp.getParameters(), self._oDisp.getEffectiveDateParam(),
+ sPreamble, sPostamble)
+
+ @staticmethod
+ def generateItemPerPageSelector(sWhere, dParams, cCurItemsPerPage):
+ """
+ Generate HTML code for items per page selector.
+ Note! Modifies dParams!
+ """
+
+ # Drop the current page count parameter.
+ if WuiDispatcherBase.ksParamItemsPerPage in dParams:
+ del dParams[WuiDispatcherBase.ksParamItemsPerPage];
+
+ # Remove the current page number.
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo];
+
+ sHtmlItemsPerPageSelector = '<form name="TmItemsPerPageForm-%s" method="GET" class="tmitemsperpage-%s tmitemsperpage">\n'\
+ ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \
+ % (sWhere, WuiDispatcherBase.ksParamItemsPerPage, sWhere,
+ webutils.encodeUrlParams(dParams),
+ WuiDispatcherBase.ksParamItemsPerPage)
+
+ acItemsPerPage = [16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
+ for cItemsPerPage in acItemsPerPage:
+ sHtmlItemsPerPageSelector += ' <option value="%d" %s>%d per page</option>\n' \
+ % (cItemsPerPage,
+ 'selected="selected"' if cItemsPerPage == cCurItemsPerPage else '',
+ cItemsPerPage)
+ sHtmlItemsPerPageSelector += ' </select>\n' \
+ '</form>\n';
+
+ return sHtmlItemsPerPageSelector
+
+
+ def _generateNavigation(self, sWhere):
+ """
+ Return HTML for navigation.
+ """
+
+ #
+ # ASSUMES the dispatcher/controller code fetches one entry more than
+ # needed to fill the page to indicate further records.
+ #
+ sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
+ sNavigation += ' <table class="tmlistnavtab">\n' \
+ ' <tr>\n';
+ dParams = self._oDisp.getParameters();
+ dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
+ if self._tsEffectiveDate is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate;
+
+ # Prev
+ if self._iPage > 0:
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1;
+ sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNavigation += ' <td></td>\n';
+
+ # Time scale.
+ if self._fTimeNavigation:
+ sNavigation += '<td align="center" class="tmtimenav">';
+ sNavigation += self._generateTimeNavigation(sWhere);
+ sNavigation += '</td>';
+
+ # page count and next.
+ sNavigation += '<td align="right" class="tmnextanditemsperpage">\n';
+
+ if len(self._aoEntries) > self._cItemsPerPage:
+ dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1;
+ sNavigation += ' <a href="?%s">Next</a>\n' % (webutils.encodeUrlParams(dParams),);
+ sNavigation += self.generateItemPerPageSelector(sWhere, dParams, self._cItemsPerPage);
+ sNavigation += '</td>\n';
+ sNavigation += ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+ return sNavigation;
+
+ def _checkSortingByColumnAscending(self, aiColumns):
+ """
+ Checks if we're sorting by this column.
+
+ Returns 0 if not sorting by this, negative if descending, positive if ascending. The
+ value indicates the priority (nearer to 0 is higher).
+ """
+ if len(aiColumns) <= len(self._aiSelectedSortColumns):
+ aiColumns = list(aiColumns);
+ aiNegColumns = list([-i for i in aiColumns]); # pylint: disable=consider-using-generator
+ i = 0;
+ while i + len(aiColumns) <= len(self._aiSelectedSortColumns):
+ aiSub = list(self._aiSelectedSortColumns[i : i + len(aiColumns)]);
+ if aiSub == aiColumns:
+ return 1 + i;
+ if aiSub == aiNegColumns:
+ return -1 - i;
+ i += 1;
+ return 0;
+
+ def _generateTableHeaders(self):
+ """
+ Generate table headers.
+ Returns raw html string.
+ Overridable.
+ """
+
+ sHtml = ' <thead class="tmheader"><tr>';
+ for iHeader, oHeader in enumerate(self._asColumnHeaders):
+ if isinstance(oHeader, WuiHtmlBase):
+ sHtml += '<th>' + oHeader.toHtml() + '</th>';
+ elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None:
+ sHtml += '<th>'
+ iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]);
+ if iSorting > 0:
+ sDirection = '&nbsp;&#x25b4;' if iSorting == 1 else '<small>&nbsp;&#x25b5;</small>';
+ sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]);
+ else:
+ sDirection = '';
+ if iSorting < 0:
+ sDirection = '&nbsp;&#x25be;' if iSorting == -1 else '<small>&nbsp;&#x25bf;</small>'
+ sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]);
+ sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \
+ % (WuiDispatcherBase.ksParamSortColumns, sSortParams);
+ sHtml += webutils.escapeElem(oHeader) + '</a>' + sDirection + '</th>';
+ else:
+ sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
+ sHtml += '</tr><thead>\n';
+ return sHtml
+
+ def _generateTable(self):
+ """
+ show worker that just generates the table.
+ """
+
+ #
+ # Create a table.
+ # If no colum headers are provided, fall back on database field
+ # names, ASSUMING that the entries are ModelDataBase children.
+ # Note! the cellspacing is for IE8.
+ #
+ sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n';
+
+ if not self._asColumnHeaders:
+ self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
+
+ sPageBody += self._generateTableHeaders();
+
+ #
+ # Format the body and close the table.
+ #
+ sPageBody += ' <tbody>\n';
+ for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)):
+ sPageBody += self._formatListEntryHtml(iEntry);
+ sPageBody += ' </tbody>\n' \
+ '</table>\n';
+ return sPageBody;
+
+ def _composeTitle(self):
+ """Composes the title string (return value)."""
+ sTitle = self._sTitle;
+ if self._iPage != 0:
+ sTitle += ' (page ' + unicode(self._iPage + 1) + ')'
+ if self._tsEffectiveDate is not None:
+ sTitle += ' as per ' + unicode(self.formatTsShort(self._tsEffectiveDate));
+ return sTitle;
+
+
+ def show(self, fShowNavigation = True):
+ """
+ Displays the list.
+ Returns (Title, HTML) on success, raises exception on error.
+ """
+
+ sPageBody = ''
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('top');
+
+ if self._aoEntries:
+ sPageBody += self._generateTable();
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('bottom');
+ else:
+ sPageBody += '<p>No entries.</p>'
+
+ return (self._composeTitle(), sPageBody);
+
+
+class WuiListContentWithActionBase(WuiListContentBase):
+ """
+ Base for the list content with action classes.
+ """
+
+ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments
+ sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None):
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId,
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+ self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class.
+ self._sAction = None; # Set by the child class.
+ self._sCheckboxName = None; # Set by the child class.
+ self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">'
+ % ('' if sId is None else sId)), ];
+ self._asColumnAttribs = [ 'align="center"', ];
+ self._aaiColumnSorting = [ None, ];
+
+ def _getCheckBoxColumn(self, iEntry, sValue):
+ """
+ Used by _formatListEntry implementations, returns a WuiRawHtmlBase object.
+ """
+ _ = iEntry;
+ return WuiRawHtml('<input type="checkbox" name="%s" value="%s">'
+ % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue))));
+
+ def show(self, fShowNavigation=True):
+ """
+ Displays the list.
+ Returns (Title, HTML) on success, raises exception on error.
+ """
+ assert self._aoActions is not None;
+ assert self._sAction is not None;
+
+ sPageBody = '<script language="JavaScript">\n' \
+ 'function toggle%s(oSource) {\n' \
+ ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \
+ ' for(var i in aoCheckboxes)\n' \
+ ' aoCheckboxes[i].checked = oSource.checked;\n' \
+ '}\n' \
+ '</script>\n' \
+ % ('' if self._sId is None else self._sId, self._sCheckboxName,);
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('top');
+ if self._aoEntries:
+
+ sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \
+ % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),);
+ sPageBody += self._generateTable();
+
+ sPageBody += ' <label>Actions</label>\n' \
+ ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \
+ % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),);
+ for oValue, sText, _ in self._aoActions:
+ sPageBody += ' <option value="%s">%s</option>\n' \
+ % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), );
+ sPageBody += ' </select>\n';
+ sPageBody += ' <input type="submit"></input>\n';
+ sPageBody += '</form>\n';
+ if fShowNavigation:
+ sPageBody += self._generateNavigation('bottom');
+ else:
+ sPageBody += '<p>No entries.</p>'
+
+ return (self._composeTitle(), sPageBody);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py
new file mode 100755
index 00000000..040b3217
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py
@@ -0,0 +1,660 @@
+# -*- coding: utf-8 -*-
+# $Id: wuigraphwiz.py $
+
+"""
+Test Manager WUI - Graph Wizard
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+import functools;
+
+# Validation Kit imports.
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuihlpgraph import WuiHlpLineGraphErrorbarY, WuiHlpGraphDataTableEx;
+from testmanager.webui.wuireport import WuiReportBase;
+
+from common import utils, webutils;
+from common import constants;
+
+
+class WuiGraphWiz(WuiReportBase):
+ """Construct a graph for analyzing test results (values) across builds and testboxes."""
+
+ ## @name Series name parts.
+ ## @{
+ kfSeriesName_TestBox = 1;
+ kfSeriesName_Product = 2;
+ kfSeriesName_Branch = 4;
+ kfSeriesName_BuildType = 8;
+ kfSeriesName_OsArchs = 16;
+ kfSeriesName_TestCase = 32;
+ kfSeriesName_TestCaseArgs = 64;
+ kfSeriesName_All = 127;
+ ## @}
+
+
+ def __init__(self, oModel, dParams, fSubReport = False, fnDPrint = None, oDisp = None):
+ WuiReportBase.__init__(self, oModel, dParams, fSubReport = fSubReport, fnDPrint = fnDPrint, oDisp = oDisp);
+
+ # Select graph implementation.
+ if dParams[WuiMain.ksParamGraphWizImpl] == 'charts':
+ from testmanager.webui.wuihlpgraphgooglechart import WuiHlpLineGraphErrorbarY as MyGraph;
+ self.oGraphClass = MyGraph;
+ elif dParams[WuiMain.ksParamGraphWizImpl] == 'matplotlib':
+ from testmanager.webui.wuihlpgraphmatplotlib import WuiHlpLineGraphErrorbarY as MyGraph;
+ self.oGraphClass = MyGraph;
+ else:
+ self.oGraphClass = WuiHlpLineGraphErrorbarY;
+
+
+ #
+ def _figureSeriesNameBits(self, aoSeries):
+ """ Figures out the method (bitmask) to use when naming series. """
+ if len(aoSeries) <= 1:
+ return WuiGraphWiz.kfSeriesName_TestBox;
+
+ # Start with all and drop unnecessary specs one-by-one.
+ fRet = WuiGraphWiz.kfSeriesName_All;
+
+ if [oSrs.idTestBox for oSrs in aoSeries].count(aoSeries[0].idTestBox) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestBox;
+
+ if [oSrs.idBuildCategory for oSrs in aoSeries].count(aoSeries[0].idBuildCategory) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Product;
+ fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
+ fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
+ fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
+ else:
+ if [oSrs.oBuildCategory.sProduct for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sProduct) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Product;
+ if [oSrs.oBuildCategory.sBranch for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sBranch) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_Branch;
+ if [oSrs.oBuildCategory.sType for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sType) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_BuildType;
+
+ # Complicated.
+ fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs;
+ daTestBoxes = {};
+ for oSeries in aoSeries:
+ if oSeries.idTestBox in daTestBoxes:
+ daTestBoxes[oSeries.idTestBox].append(oSeries);
+ else:
+ daTestBoxes[oSeries.idTestBox] = [oSeries,];
+ for aoSeriesPerTestBox in daTestBoxes.values():
+ if len(aoSeriesPerTestBox) >= 0:
+ asOsArches = aoSeriesPerTestBox[0].oBuildCategory.asOsArches;
+ for i in range(1, len(aoSeriesPerTestBox)):
+ if aoSeriesPerTestBox[i].oBuildCategory.asOsArches != asOsArches:
+ fRet |= WuiGraphWiz.kfSeriesName_OsArchs;
+ break;
+
+ if aoSeries[0].oTestCaseArgs is None:
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
+ if [oSrs.idTestCase for oSrs in aoSeries].count(aoSeries[0].idTestCase) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
+ else:
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCase;
+ if [oSrs.idTestCaseArgs for oSrs in aoSeries].count(aoSeries[0].idTestCaseArgs) == len(aoSeries):
+ fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs;
+
+ return fRet;
+
+ def _getSeriesNameFromBits(self, oSeries, fBits):
+ """ Creates a series name from bits (kfSeriesName_xxx). """
+ assert fBits != 0;
+ sName = '';
+
+ if fBits & WuiGraphWiz.kfSeriesName_Product:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sProduct;
+
+ if fBits & WuiGraphWiz.kfSeriesName_Branch:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sBranch;
+
+ if fBits & WuiGraphWiz.kfSeriesName_BuildType:
+ if sName: sName += ' / ';
+ sName += oSeries.oBuildCategory.sType;
+
+ if fBits & WuiGraphWiz.kfSeriesName_OsArchs:
+ if sName: sName += ' / ';
+ sName += ' & '.join(oSeries.oBuildCategory.asOsArches);
+
+ if fBits & WuiGraphWiz.kfSeriesName_TestCaseArgs:
+ if sName: sName += ' / ';
+ if oSeries.idTestCaseArgs is not None:
+ sName += oSeries.oTestCase.sName + ':#' + str(oSeries.idTestCaseArgs);
+ else:
+ sName += oSeries.oTestCase.sName;
+ elif fBits & WuiGraphWiz.kfSeriesName_TestCase:
+ if sName: sName += ' / ';
+ sName += oSeries.oTestCase.sName;
+
+ if fBits & WuiGraphWiz.kfSeriesName_TestBox:
+ if sName: sName += ' / ';
+ sName += oSeries.oTestBox.sName;
+
+ return sName;
+
+ def _calcGraphName(self, oSeries, fSeriesName, sSampleName):
+ """ Constructs a name for the graph. """
+ fGraphName = ~fSeriesName & ( WuiGraphWiz.kfSeriesName_TestBox
+ | WuiGraphWiz.kfSeriesName_Product
+ | WuiGraphWiz.kfSeriesName_Branch
+ | WuiGraphWiz.kfSeriesName_BuildType
+ );
+ sName = self._getSeriesNameFromBits(oSeries, fGraphName);
+ if sName: sName += ' - ';
+ sName += sSampleName;
+ return sName;
+
+ def _calcSampleName(self, oCollection):
+ """ Constructs a name for a sample source (collection). """
+ if oCollection.sValue is not None:
+ asSampleName = [oCollection.sValue, 'in',];
+ elif oCollection.sType == self._oModel.ksTypeElapsed:
+ asSampleName = ['Elapsed time', 'for', ];
+ elif oCollection.sType == self._oModel.ksTypeResult:
+ asSampleName = ['Error count', 'for',];
+ else:
+ return 'Invalid collection type: "%s"' % (oCollection.sType,);
+
+ sTestName = ', '.join(oCollection.asTests if oCollection.asTests[0] else oCollection.asTests[1:]);
+ if sTestName == '':
+ # Use the testcase name if there is only one for all series.
+ if not oCollection.aoSeries:
+ return asSampleName[0];
+ if len(oCollection.aoSeries) > 1:
+ idTestCase = oCollection.aoSeries[0].idTestCase;
+ for oSeries in oCollection.aoSeries:
+ if oSeries.idTestCase != idTestCase:
+ return asSampleName[0];
+ sTestName = oCollection.aoSeries[0].oTestCase.sName;
+ return ' '.join(asSampleName) + ' ' + sTestName;
+
+
+ def _splitSeries(self, aoSeries):
+ """
+ Splits the data series (ReportGraphModel.DataSeries) into one or more graphs.
+
+ Returns an array of data series arrays.
+ """
+ # Must be at least two series for something to be splittable.
+ if len(aoSeries) <= 1:
+ if not aoSeries:
+ return [];
+ return [aoSeries,];
+
+ # Split on unit.
+ dUnitSeries = {};
+ for oSeries in aoSeries:
+ if oSeries.iUnit not in dUnitSeries:
+ dUnitSeries[oSeries.iUnit] = [];
+ dUnitSeries[oSeries.iUnit].append(oSeries);
+
+ # Sort the per-unit series since the build category was only sorted by ID.
+ for iUnit in dUnitSeries:
+ def mycmp(oSelf, oOther):
+ """ __cmp__ like function. """
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sProduct, oOther.oBuildCategory.sProduct);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sBranch, oOther.oBuildCategory.sBranch);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oBuildCategory.sType, oOther.oBuildCategory.sType);
+ if iCmp != 0:
+ return iCmp;
+ iCmp = utils.stricmp(oSelf.oTestBox.sName, oOther.oTestBox.sName);
+ if iCmp != 0:
+ return iCmp;
+ return 0;
+ dUnitSeries[iUnit] = sorted(dUnitSeries[iUnit], key = functools.cmp_to_key(mycmp));
+
+ # Split the per-unit series up if necessary.
+ cMaxPerGraph = self._dParams[WuiMain.ksParamGraphWizMaxPerGraph];
+ aaoRet = [];
+ for aoUnitSeries in dUnitSeries.values():
+ while len(aoUnitSeries) > cMaxPerGraph:
+ aaoRet.append(aoUnitSeries[:cMaxPerGraph]);
+ aoUnitSeries = aoUnitSeries[cMaxPerGraph:];
+ if aoUnitSeries:
+ aaoRet.append(aoUnitSeries);
+
+ return aaoRet;
+
+ def _configureGraph(self, oGraph):
+ """
+ Configures oGraph according to user parameters and other config settings.
+
+ Returns oGraph.
+ """
+ oGraph.setWidth(self._dParams[WuiMain.ksParamGraphWizWidth])
+ oGraph.setHeight(self._dParams[WuiMain.ksParamGraphWizHeight])
+ oGraph.setDpi(self._dParams[WuiMain.ksParamGraphWizDpi])
+ oGraph.setErrorBarY(self._dParams[WuiMain.ksParamGraphWizErrorBarY]);
+ oGraph.setFontSize(self._dParams[WuiMain.ksParamGraphWizFontSize]);
+ if hasattr(oGraph, 'setXkcdStyle'):
+ oGraph.setXkcdStyle(self._dParams[WuiMain.ksParamGraphWizXkcdStyle]);
+
+ return oGraph;
+
+ def _generateInteractiveForm(self):
+ """
+ Generates the HTML for the interactive form.
+ Returns (sTopOfForm, sEndOfForm)
+ """
+
+ #
+ # The top of the form.
+ #
+ sTop = '<form action="#" method="get" id="graphwiz-form">\n' \
+ ' <input type="hidden" name="%s" value="%s"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz,
+ WuiMain.ksParamGraphWizSrcTestSetId, self._dParams[WuiMain.ksParamGraphWizSrcTestSetId],
+ );
+
+ sTop += ' <div id="graphwiz-nav">\n';
+ sTop += ' <script type="text/javascript">\n' \
+ ' window.onresize = function(){ return graphwizOnResizeRecalcWidth("graphwiz-nav", "%s"); }\n' \
+ ' window.onload = function(){ return graphwizOnLoadRememberWidth("graphwiz-nav"); }\n' \
+ ' </script>\n' \
+ % ( WuiMain.ksParamGraphWizWidth, );
+
+ #
+ # Top: First row.
+ #
+ sTop += ' <div id="graphwiz-top-1">\n';
+
+ # time.
+ sNow = self._dParams[WuiMain.ksParamEffectiveDate];
+ if sNow is None: sNow = '';
+ sTop += ' <div id="graphwiz-time">\n';
+ sTop += ' <label for="%s">Starting:</label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-time-input"/>\n' \
+ % ( WuiMain.ksParamEffectiveDate,
+ WuiMain.ksParamEffectiveDate, WuiMain.ksParamEffectiveDate, sNow, );
+
+ sTop += ' <input type="hidden" name="%s" value="%u"/>\n' % ( WuiMain.ksParamReportPeriods, 1, );
+ sTop += ' <label for="%s"> Going back:\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-period-input"/>\n' \
+ % ( WuiMain.ksParamReportPeriodInHours,
+ WuiMain.ksParamReportPeriodInHours, WuiMain.ksParamReportPeriodInHours,
+ utils.formatIntervalHours(self._dParams[WuiMain.ksParamReportPeriodInHours]) );
+ sTop += ' </div>\n';
+
+ # Graph options top row.
+ sTop += ' <div id="graphwiz-top-options-1">\n';
+
+ # graph type.
+ sTop += ' <label for="%s">Graph:</label>\n' \
+ ' <select name="%s" id="%s">\n' \
+ % ( WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, );
+ for (sImpl, sDesc) in WuiMain.kaasGraphWizImplCombo:
+ sTop += ' <option value="%s"%s>%s</option>\n' \
+ % (sImpl, ' selected' if sImpl == self._dParams[WuiMain.ksParamGraphWizImpl] else '', sDesc);
+ sTop += ' </select>\n';
+
+ # graph size.
+ sTop += ' <label for="%s">Graph size:</label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input"> x\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input">\n' \
+ ' <label for="%s">Dpi:</label>'\
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-dpi-input">\n' \
+ ' <button type="button" onclick="%s">Defaults</button>\n' \
+ % ( WuiMain.ksParamGraphWizWidth,
+ WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizWidth, self._dParams[WuiMain.ksParamGraphWizWidth],
+ WuiMain.ksParamGraphWizHeight, WuiMain.ksParamGraphWizHeight, self._dParams[WuiMain.ksParamGraphWizHeight],
+ WuiMain.ksParamGraphWizDpi,
+ WuiMain.ksParamGraphWizDpi, WuiMain.ksParamGraphWizDpi, self._dParams[WuiMain.ksParamGraphWizDpi],
+ webutils.escapeAttr('return graphwizSetDefaultSizeValues("graphwiz-nav", "%s", "%s", "%s");'
+ % ( WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizHeight,
+ WuiMain.ksParamGraphWizDpi )),
+ );
+
+ sTop += ' </div>\n'; # (options row 1)
+
+ sTop += ' </div>\n'; # (end of row 1)
+
+ #
+ # Top: Second row.
+ #
+ sTop += ' <div id="graphwiz-top-2">\n';
+
+ # Submit
+ sFormButton = '<button type="submit">Refresh</button>\n';
+ sTop += ' <div id="graphwiz-top-submit">' + sFormButton + '</div>\n';
+
+
+ # Options.
+ sTop += ' <div id="graphwiz-top-options-2">\n';
+
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
+ ' <label for="%s">Tabular data</label>\n' \
+ % ( WuiMain.ksParamGraphWizTabular, WuiMain.ksParamGraphWizTabular,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizTabular] else '',
+ WuiMain.ksParamGraphWizTabular);
+
+ if hasattr(self.oGraphClass, 'setXkcdStyle'):
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \
+ ' <label for="%s">xkcd-style</label>\n' \
+ % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizXkcdStyle] else '',
+ WuiMain.ksParamGraphWizXkcdStyle);
+ elif self._dParams[WuiMain.ksParamGraphWizXkcdStyle]:
+ sTop += ' <input type="hidden" name="%s" id="%s" value="1"/>\n' \
+ % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, );
+
+ if not hasattr(self.oGraphClass, 'kfNoErrorBarsSupport'):
+ sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s title="%s"/>\n' \
+ ' <label for="%s">Error bars,</label>\n' \
+ ' <label for="%s">max: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxerrorbar-input" title="%s"/>\n' \
+ % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizErrorBarY] else '',
+ 'Error bars shows some of the max and min results on the Y-axis.',
+ WuiMain.ksParamGraphWizErrorBarY,
+ WuiMain.ksParamGraphWizMaxErrorBarY,
+ WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
+ self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY],
+ 'Maximum number of Y-axis error bar per graph. (Too many makes it unreadable.)'
+ );
+ else:
+ if self._dParams[WuiMain.ksParamGraphWizErrorBarY]:
+ sTop += '<input type="hidden" name="%s" id="%s" value="1">\n' \
+ % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, );
+ sTop += '<input type="hidden" name="%s" id="%s" value="%u">\n' \
+ % ( WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY,
+ self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], );
+
+ sTop += ' <label for="%s">Font size: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-fontsize-input"/>\n' \
+ % ( WuiMain.ksParamGraphWizFontSize,
+ WuiMain.ksParamGraphWizFontSize, WuiMain.ksParamGraphWizFontSize,
+ self._dParams[WuiMain.ksParamGraphWizFontSize], );
+
+ sTop += ' <label for="%s">Data series: </label>\n' \
+ ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxpergraph-input" title="%s"/>\n' \
+ % ( WuiMain.ksParamGraphWizMaxPerGraph,
+ WuiMain.ksParamGraphWizMaxPerGraph, WuiMain.ksParamGraphWizMaxPerGraph,
+ self._dParams[WuiMain.ksParamGraphWizMaxPerGraph],
+ 'Max data series per graph.' );
+
+ sTop += ' </div>\n'; # (options row 2)
+
+ sTop += ' </div>\n'; # (end of row 2)
+
+ sTop += ' </div>\n'; # end of top.
+
+ #
+ # The end of the page selection.
+ #
+ sEnd = ' <div id="graphwiz-end-selection">\n';
+
+ #
+ # Testbox selection
+ #
+ aidTestBoxes = list(self._dParams[WuiMain.ksParamGraphWizTestBoxIds]);
+ sEnd += ' <div id="graphwiz-testboxes" class="graphwiz-end-selection-group">\n' \
+ ' <h3>TestBox Selection:</h3>\n' \
+ ' <ol class="tmgraph-testboxes">\n';
+
+ # Get a list of eligible testboxes from the DB.
+ for oTestBox in self._oModel.getEligibleTestBoxes():
+ try: aidTestBoxes.remove(oTestBox.idTestBox);
+ except: sChecked = '';
+ else: sChecked = ' checked';
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u"%s/>' \
+ '<label for="gw-tb-%u">%s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, sChecked,
+ oTestBox.idTestBox, oTestBox.sName);
+
+ # List testboxes that have been checked in a different period or something.
+ for idTestBox in aidTestBoxes:
+ oTestBox = self._oModel.oCache.getTestBox(idTestBox);
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u" checked/>' \
+ '<label for="gw-tb-%u">%s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox,
+ oTestBox.idTestBox, oTestBox.sName);
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #
+ # Build category selection.
+ #
+ aidBuildCategories = list(self._dParams[WuiMain.ksParamGraphWizBuildCatIds]);
+ sEnd += ' <div id="graphwiz-buildcategories" class="graphwiz-end-selection-group">\n' \
+ ' <h3>Build Category Selection:</h3>\n' \
+ ' <ol class="tmgraph-buildcategories">\n';
+ for oBuildCat in self._oModel.getEligibleBuildCategories():
+ try: aidBuildCategories.remove(oBuildCat.idBuildCategory);
+ except: sChecked = '';
+ else: sChecked = ' checked';
+ sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-bc-%u" %s/>' \
+ '<label for="gw-bc-%u">%s / %s / %s / %s</label></li>\n' \
+ % ( WuiMain.ksParamGraphWizBuildCatIds, oBuildCat.idBuildCategory, oBuildCat.idBuildCategory, sChecked,
+ oBuildCat.idBuildCategory,
+ oBuildCat.sProduct, oBuildCat.sBranch, oBuildCat.sType, ' & '.join(oBuildCat.asOsArches) );
+ assert not aidBuildCategories; # SQL should return all currently selected.
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #
+ # Testcase variations.
+ #
+ sEnd += ' <div id="graphwiz-testcase-variations" class="graphwiz-end-selection-group">\n' \
+ ' <h3>Miscellaneous:</h3>\n' \
+ ' <ol>';
+
+ sEnd += ' <li>\n' \
+ ' <input type="checkbox" id="%s" name="%s" value="1"%s/>\n' \
+ ' <label for="%s">Separate by testcase variation.</label>\n' \
+ ' </li>\n' \
+ % ( WuiMain.ksParamGraphWizSepTestVars, WuiMain.ksParamGraphWizSepTestVars,
+ ' checked' if self._dParams[WuiMain.ksParamGraphWizSepTestVars] else '',
+ WuiMain.ksParamGraphWizSepTestVars );
+
+
+ sEnd += ' <li>\n' \
+ ' <lable for="%s">Test case ID:</label>\n' \
+ ' <input type="text" id="%s" name="%s" value="%s" readonly/>\n' \
+ ' </li>\n' \
+ % ( WuiMain.ksParamGraphWizTestCaseIds,
+ WuiMain.ksParamGraphWizTestCaseIds, WuiMain.ksParamGraphWizTestCaseIds,
+ ','.join([str(i) for i in self._dParams[WuiMain.ksParamGraphWizTestCaseIds]]), );
+
+ sEnd += ' </ol>\n' \
+ ' </div>\n';
+
+ #sEnd += ' <h3>&nbsp;</h3>\n';
+
+ #
+ # Finish up the form.
+ #
+ sEnd += ' <div id="graphwiz-end-submit"><p>' + sFormButton + '</p></div>\n';
+ sEnd += ' </div>\n' \
+ '</form>\n';
+
+ return (sTop, sEnd);
+
+ def generateReportBody(self):
+ fInteractive = not self._fSubReport;
+
+ # Quick mockup.
+ self._sTitle = 'Graph Wizzard';
+
+ sHtml = '';
+ sHtml += '<h2>Incomplete code - no complaints yet, thank you!!</h2>\n';
+
+ #
+ # Create a form for altering the data we're working with.
+ #
+ if fInteractive:
+ (sTopOfForm, sEndOfForm) = self._generateInteractiveForm();
+ sHtml += sTopOfForm;
+ del sTopOfForm;
+
+ #
+ # Emit the graphs. At least one per sample source.
+ #
+ sHtml += ' <div id="graphwiz-graphs">\n';
+ iGraph = 0;
+ aoCollections = self._oModel.fetchGraphData();
+ for iCollection, oCollection in enumerate(aoCollections):
+ # Name the graph and add a checkbox for removing it.
+ sSampleName = self._calcSampleName(oCollection);
+ sHtml += ' <div class="graphwiz-collection" id="graphwiz-source-%u">\n' % (iCollection,);
+ if fInteractive:
+ sHtml += ' <div class="graphwiz-src-select">\n' \
+ ' <input type="checkbox" name="%s" id="%s" value="%s:%s%s" checked class="graphwiz-src-input">\n' \
+ ' <label for="%s">%s</label>\n' \
+ ' </div>\n' \
+ % ( WuiMain.ksParamReportSubjectIds, WuiMain.ksParamReportSubjectIds, oCollection.sType,
+ ':'.join([str(idStr) for idStr in oCollection.aidStrTests]),
+ ':%u' % oCollection.idStrValue if oCollection.idStrValue else '',
+ WuiMain.ksParamReportSubjectIds, sSampleName );
+
+ if oCollection.aoSeries:
+ #
+ # Split the series into sub-graphs as needed and produce SVGs.
+ #
+ aaoSeries = self._splitSeries(oCollection.aoSeries);
+ for aoSeries in aaoSeries:
+ # Gather the data for this graph. (Most big stuff is passed by
+ # reference, so there shouldn't be any large memory penalty for
+ # repacking the data here.)
+ sYUnit = None;
+ if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
+ sYUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
+ oData = WuiHlpGraphDataTableEx(sXUnit = 'Build revision', sYUnit = sYUnit);
+
+ fSeriesName = self._figureSeriesNameBits(aoSeries);
+ for oSeries in aoSeries:
+ sSeriesName = self._getSeriesNameFromBits(oSeries, fSeriesName);
+ asHtmlTooltips = None;
+ if len(oSeries.aoRevInfo) == len(oSeries.aiRevisions):
+ asHtmlTooltips = [];
+ for i, oRevInfo in enumerate(oSeries.aoRevInfo):
+ sPlusMinus = '';
+ if oSeries.acSamples[i] > 1:
+ sPlusMinus = ' (+%s/-%s; %u samples)' \
+ % ( utils.formatNumber(oSeries.aiErrorBarAbove[i]),
+ utils.formatNumber(oSeries.aiErrorBarBelow[i]),
+ oSeries.acSamples[i])
+ sTooltip = '<table class=\'graphwiz-tt\'><tr><td>%s:</td><td>%s %s %s</td></tr>'\
+ '<tr><td>Rev:</td><td>r%s</td></tr>' \
+ % ( sSeriesName,
+ utils.formatNumber(oSeries.aiValues[i]),
+ sYUnit, sPlusMinus,
+ oSeries.aiRevisions[i],
+ );
+ if oRevInfo.sAuthor is not None:
+ sMsg = oRevInfo.sMessage[:80].strip();
+ #if sMsg.find('\n') >= 0:
+ # sMsg = sMsg[:sMsg.find('\n')].strip();
+ sTooltip += '<tr><td>Author:</td><td>%s</td></tr>' \
+ '<tr><td>Date:</td><td>%s</td><tr>' \
+ '<tr><td>Message:</td><td>%s%s</td></tr>' \
+ % ( oRevInfo.sAuthor,
+ self.formatTsShort(oRevInfo.tsCreated),
+ sMsg, '...' if len(oRevInfo.sMessage) > len(sMsg) else '');
+ sTooltip += '</table>';
+ asHtmlTooltips.append(sTooltip);
+ oData.addDataSeries(sSeriesName, oSeries.aiRevisions, oSeries.aiValues, asHtmlTooltips,
+ oSeries.aiErrorBarBelow, oSeries.aiErrorBarAbove);
+ # Render the data into a graph.
+ oGraph = self.oGraphClass('tmgraph-%u' % (iGraph,), oData, self._oDisp);
+ self._configureGraph(oGraph);
+
+ oGraph.setTitle(self._calcGraphName(aoSeries[0], fSeriesName, sSampleName));
+ sHtml += ' <div class="graphwiz-graph" id="graphwiz-graph-%u">\n' % (iGraph,);
+ sHtml += oGraph.renderGraph();
+ sHtml += '\n </div>\n';
+ iGraph += 1;
+
+ #
+ # Emit raw tabular data if requested.
+ #
+ if self._dParams[WuiMain.ksParamGraphWizTabular]:
+ sHtml += ' <div class="graphwiz-tab-div" id="graphwiz-tab-%u">\n' \
+ ' <table class="tmtable graphwiz-tab">\n' \
+ % (iCollection, );
+ for aoSeries in aaoSeries:
+ if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0:
+ sUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit];
+ else:
+ sUnit = str(aoSeries[0].iUnit);
+
+ for iSeries, oSeries in enumerate(aoSeries):
+ sColor = self.oGraphClass.calcSeriesColor(iSeries);
+
+ sHtml += '<thead class="tmheader">\n' \
+ ' <tr class="graphwiz-tab graphwiz-tab-new-series-row">\n' \
+ ' <th colspan="5"><span style="background-color:%s;">&nbsp;&nbsp;</span> %s</th>\n' \
+ ' </tr>\n' \
+ ' <tr class="graphwiz-tab graphwiz-tab-col-hdr-row">\n' \
+ ' <th>Revision</th><th>Value (%s)</th><th>&Delta;max</th><th>&Delta;min</th>' \
+ '<th>Samples</th>\n' \
+ ' </tr>\n' \
+ '</thead>\n' \
+ % ( sColor,
+ self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs),
+ sUnit );
+
+ for i, iRevision in enumerate(oSeries.aiRevisions):
+ sHtml += ' <tr class="%s"><td>r%s</td><td>%s</td><td>+%s</td><td>-%s</td><td>%s</td></tr>\n' \
+ % ( 'tmodd' if i & 1 else 'tmeven',
+ iRevision, oSeries.aiValues[i],
+ oSeries.aiErrorBarAbove[i], oSeries.aiErrorBarBelow[i],
+ oSeries.acSamples[i]);
+ sHtml += ' </table>\n' \
+ ' </div>\n';
+ else:
+ sHtml += '<i>No results.</i>\n';
+ sHtml += ' </div>\n'
+ sHtml += ' </div>\n';
+
+ #
+ # Finish the form.
+ #
+ if fInteractive:
+ sHtml += sEndOfForm;
+
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
new file mode 100755
index 00000000..38015e83
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
@@ -0,0 +1,1111 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpform.py $
+
+"""
+Test Manager Web-UI - Form Helpers.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard python imports.
+import copy;
+import sys;
+
+# Validation Kit imports.
+from common import utils;
+from common.webutils import escapeAttr, escapeElem;
+from testmanager import config;
+from testmanager.core.schedgroup import SchedGroupMemberData, SchedGroupDataEx;
+from testmanager.core.testcaseargs import TestCaseArgsData;
+from testmanager.core.testgroup import TestGroupMemberData, TestGroupDataEx;
+from testmanager.core.testbox import TestBoxDataForSchedGroup;
+
+# Python 3 hacks:
+if sys.version_info[0] >= 3:
+ unicode = str; # pylint: disable=redefined-builtin,invalid-name
+
+
+class WuiHlpForm(object):
+ """
+ Helper for constructing a form.
+ """
+
+ ksItemsList = 'ksItemsList'
+
+ ksOnSubmit_AddReturnToFieldWithCurrentUrl = '+AddReturnToFieldWithCurrentUrl+';
+
+ def __init__(self, sId, sAction, dErrors = None, fReadOnly = False, sOnSubmit = None):
+ self._fFinalized = False;
+ self._fReadOnly = fReadOnly;
+ self._dErrors = dErrors if dErrors is not None else {};
+
+ if sOnSubmit == self.ksOnSubmit_AddReturnToFieldWithCurrentUrl:
+ sOnSubmit = u'return addRedirectToInputFieldWithCurrentUrl(this)';
+ if sOnSubmit is None: sOnSubmit = u'';
+ else: sOnSubmit = u' onsubmit=\"%s\"' % (escapeAttr(sOnSubmit),);
+
+ self._sBody = u'\n' \
+ u'<div id="%s" class="tmform">\n' \
+ u' <form action="%s" method="post"%s>\n' \
+ u' <ul>\n' \
+ % (sId, sAction, sOnSubmit);
+
+ def _add(self, sText):
+ """Internal worker for appending text to the body."""
+ assert not self._fFinalized;
+ if not self._fFinalized:
+ self._sBody += utils.toUnicode(sText, errors='ignore');
+ return True;
+ return False;
+
+ def _escapeErrorText(self, sText):
+ """Escapes error text, preserving some predefined HTML tags."""
+ if sText.find('<br>') >= 0:
+ asParts = sText.split('<br>');
+ for i, _ in enumerate(asParts):
+ asParts[i] = escapeElem(asParts[i].strip());
+ sText = '<br>\n'.join(asParts);
+ else:
+ sText = escapeElem(sText);
+ return sText;
+
+ def _addLabel(self, sName, sLabel, sDivSubClass = 'normal'):
+ """Internal worker for adding a label."""
+ if sName in self._dErrors:
+ sError = self._dErrors[sName];
+ if utils.isString(sError): # List error trick (it's an associative array).
+ return self._add(u' <li>\n'
+ u' <div class="tmform-field"><div class="tmform-field-%s">\n'
+ u' <label for="%s" class="tmform-error-label">%s\n'
+ u' <span class="tmform-error-desc">%s</span>\n'
+ u' </label>\n'
+ % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel),
+ self._escapeErrorText(sError), ) );
+ return self._add(u' <li>\n'
+ u' <div class="tmform-field"><div class="tmform-field-%s">\n'
+ u' <label for="%s">%s</label>\n'
+ % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel)) );
+
+
+ def finalize(self):
+ """
+ Finalizes the form and returns the body.
+ """
+ if not self._fFinalized:
+ self._add(u' </ul>\n'
+ u' </form>\n'
+ u'</div>\n'
+ u'<div class="clear"></div>\n' );
+ return self._sBody;
+
+ def addTextHidden(self, sName, sValue, sExtraAttribs = ''):
+ """Adds a hidden text input."""
+ return self._add(u' <div class="tmform-field-hidden">\n'
+ u' <input name="%s" id="%s" type="text" hidden%s value="%s" class="tmform-hidden">\n'
+ u' </div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeElem(str(sValue)) ));
+ #
+ # Non-input stuff.
+ #
+ def addNonText(self, sValue, sLabel, sName = 'non-text', sPostHtml = ''):
+ """Adds a read-only text input."""
+ self._addLabel(sName, sLabel, 'string');
+ if sValue is None: sValue = '';
+ return self._add(u' <p>%s%s</p>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeElem(unicode(sValue)), sPostHtml ));
+
+ def addRawHtml(self, sRawHtml, sLabel, sName = 'raw-html'):
+ """Adds a read-only text input."""
+ self._addLabel(sName, sLabel, 'string');
+ self._add(sRawHtml);
+ return self._add(u' </div></div>\n'
+ u' </li>\n');
+
+
+ #
+ # Text input fields.
+ #
+ def addText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''):
+ """Adds a text input."""
+ if self._fReadOnly:
+ return self.addTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs);
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
+ self._addLabel(sName, sLabel, sSubClass);
+ if sValue is None: sValue = '';
+ return self._add(u' <input name="%s" id="%s" type="text"%s value="%s">%s\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml ));
+
+ def addTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only text input."""
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass);
+ self._addLabel(sName, sLabel, sSubClass);
+ if sValue is None: sValue = '';
+ return self._add(u' <input name="%s" id="%s" type="text" readonly%s value="%s" class="tmform-input-readonly">'
+ u'%s\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml ));
+
+ def addWideText(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a wide text input."""
+ return self.addText(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addWideTextRO(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a wide read-only text input."""
+ return self.addTextRO(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def _adjustMultilineTextAttribs(self, sExtraAttribs, sValue):
+ """ Internal helper for setting good default sizes for textarea based on content."""
+ if sExtraAttribs.find('cols') < 0 and sExtraAttribs.find('width') < 0:
+ sExtraAttribs = 'cols="96%" ' + sExtraAttribs;
+
+ if sExtraAttribs.find('rows') < 0 and sExtraAttribs.find('width') < 0:
+ if sValue is None: sValue = '';
+ else: sValue = sValue.strip();
+
+ cRows = sValue.count('\n') + (not sValue.endswith('\n'));
+ if cRows * 80 < len(sValue):
+ cRows += 2;
+ cRows = max(min(cRows, 16), 2);
+ sExtraAttribs = ('rows="%s" ' % (cRows,)) + sExtraAttribs;
+
+ return sExtraAttribs;
+
+ def addMultilineText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''):
+ """Adds a multiline text input."""
+ if self._fReadOnly:
+ return self.addMultilineTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs);
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
+ self._addLabel(sName, sLabel, sSubClass)
+ if sValue is None: sValue = '';
+ sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
+ return self._add(u' <textarea name="%s" id="%s" %s>%s</textarea>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue),
+ escapeElem(sNewValue)))
+
+ def addMultilineTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''):
+ """Adds a multiline read-only text input."""
+ if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass)
+ self._addLabel(sName, sLabel, sSubClass)
+ if sValue is None: sValue = '';
+ sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue)
+ return self._add(u' <textarea name="%s" id="%s" readonly %s>%s</textarea>\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue),
+ escapeElem(sNewValue)))
+
+ def addInt(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an integer input."""
+ return self.addText(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addIntRO(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an integer input."""
+ return self.addTextRO(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addLong(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a long input."""
+ return self.addText(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addLongRO(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a long input."""
+ return self.addTextRO(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addUuid(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds an UUID input."""
+ return self.addText(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addUuidRO(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only UUID input."""
+ return self.addTextRO(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml);
+
+ def addTimestampRO(self, sName, sTimestamp, sLabel, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only database string timstamp input."""
+ return self.addTextRO(sName, sTimestamp, sLabel, 'timestamp', sExtraAttribs, sPostHtml = sPostHtml);
+
+
+ #
+ # Text areas.
+ #
+
+
+ #
+ # Combo boxes.
+ #
+ def addComboBox(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a combo box."""
+ if self._fReadOnly:
+ return self.addComboBoxRO(sName, sSelected, sLabel, aoOptions, sExtraAttribs, sPostHtml);
+ self._addLabel(sName, sLabel, 'combobox');
+ self._add(' <select name="%s" id="%s" class="tmform-combobox"%s>\n'
+ % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+ sSelected = unicode(sSelected);
+ for iValue, sText, _ in aoOptions:
+ sValue = unicode(iValue);
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
+ escapeElem(sText)));
+ return self._add(u' </select>' + sPostHtml + '\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
+ def addComboBoxRO(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''):
+ """Adds a read-only combo box."""
+ self.addTextHidden(sName, sSelected);
+ self._addLabel(sName, sLabel, 'combobox-readonly');
+ self._add(u' <select name="%s" id="%s" disabled class="tmform-combobox"%s>\n'
+ % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs));
+ sSelected = unicode(sSelected);
+ for iValue, sText, _ in aoOptions:
+ sValue = unicode(iValue);
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % (escapeAttr(sValue), ' selected' if sValue == sSelected else '',
+ escapeElem(sText)));
+ return self._add(u' </select>' + sPostHtml + '\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
+ #
+ # Check boxes.
+ #
+ @staticmethod
+ def _reinterpretBool(fValue):
+ """Reinterprets a value as a boolean type."""
+ if fValue is not type(True):
+ if fValue is None:
+ fValue = False;
+ elif str(fValue) in ('True', 'true', '1'):
+ fValue = True;
+ else:
+ fValue = False;
+ return fValue;
+
+ def addCheckBox(self, sName, fChecked, sLabel, sExtraAttribs = ''):
+ """Adds an check box."""
+ if self._fReadOnly:
+ return self.addCheckBoxRO(sName, fChecked, sLabel, sExtraAttribs);
+ self._addLabel(sName, sLabel, 'checkbox');
+ fChecked = self._reinterpretBool(fChecked);
+ return self._add(u' <input name="%s" id="%s" type="checkbox"%s%s value="1" class="tmform-checkbox">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs));
+
+ def addCheckBoxRO(self, sName, fChecked, sLabel, sExtraAttribs = ''):
+ """Adds an readonly check box."""
+ self._addLabel(sName, sLabel, 'checkbox');
+ fChecked = self._reinterpretBool(fChecked);
+ # Hack Alert! The onclick and onkeydown are for preventing editing and fake readonly/disabled.
+ return self._add(u' <input name="%s" id="%s" type="checkbox"%s readonly%s value="1" class="readonly"\n'
+ u' onclick="return false" onkeydown="return false">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs));
+
+ #
+ # List of items to check
+ #
+ def _addList(self, sName, aoRows, sLabel, fUseTable = False, sId = 'dummy', sExtraAttribs = ''):
+ """
+ Adds a list of items to check.
+
+ @param sName Name of HTML form element
+ @param aoRows List of [sValue, fChecked, sName] sub-arrays.
+ @param sLabel Label of HTML form element
+ """
+ fReadOnly = self._fReadOnly; ## @todo add this as a parameter.
+ if fReadOnly:
+ sExtraAttribs += ' readonly onclick="return false" onkeydown="return false"';
+
+ self._addLabel(sName, sLabel, 'list');
+ if not aoRows:
+ return self._add('No items</div></div></li>')
+ sNameEscaped = escapeAttr(sName);
+
+ self._add(' <div class="tmform-checkboxes-container" id="%s">\n' % (escapeAttr(sId),));
+ if fUseTable:
+ self._add(' <table>\n');
+ for asRow in aoRows:
+ assert len(asRow) == 3; # Don't allow sloppy input data!
+ fChecked = self._reinterpretBool(asRow[1])
+ self._add(u' <tr>\n'
+ u' <td><input type="checkbox" name="%s" value="%s"%s%s></td>\n'
+ u' <td>%s</td>\n'
+ u' </tr>\n'
+ % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs,
+ escapeElem(unicode(asRow[2])), ));
+ self._add(u' </table>\n');
+ else:
+ for asRow in aoRows:
+ assert len(asRow) == 3; # Don't allow sloppy input data!
+ fChecked = self._reinterpretBool(asRow[1])
+ self._add(u' <div class="tmform-checkbox-holder">'
+ u'<input type="checkbox" name="%s" value="%s"%s%s> %s</input></div>\n'
+ % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs,
+ escapeElem(unicode(asRow[2])),));
+ return self._add(u' </div></div></div>\n'
+ u' </li>\n');
+
+
+ def addListOfOsArches(self, sName, aoOsArches, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for OS/ARCH selection.
+ asOsArches is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoOsArches, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-os-arches',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTypes(self, sName, aoTypes, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for build type selection.
+ aoTypes is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTypes, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-build-types',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestCases(self, sName, aoTestCases, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for test box (dependency) selection.
+ aoTestCases is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testcases',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfResources(self, sName, aoTestCases, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for resource selection.
+ aoTestCases is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-resources',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestGroups(self, sName, aoTestGroups, sLabel, sExtraAttribs = ''):
+ """
+ List of checkboxes for test group selection.
+ aoTestGroups is a list of [sValue, fChecked, sName] sub-arrays.
+ """
+ return self._addList(sName, aoTestGroups, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testgroups',
+ sExtraAttribs = sExtraAttribs);
+
+ def addListOfTestCaseArgs(self, sName, aoVariations, sLabel): # pylint: disable=too-many-statements
+ """
+ Adds a list of test case argument variations to the form.
+
+ @param sName Name of HTML form element
+ @param aoVariations List of TestCaseArgsData instances.
+ @param sLabel Label of HTML form element
+ """
+ self._addLabel(sName, sLabel);
+
+ sTableId = u'TestArgsExtendingListRoot';
+ fReadOnly = self._fReadOnly; ## @todo argument?
+ sReadOnlyAttr = u' readonly class="tmform-input-readonly"' if fReadOnly else '';
+
+ sHtml = u'<li>\n'
+
+ #
+ # Define javascript function for extending the list of test case
+ # variations. Doing it here so we can use the python constants. This
+ # also permits multiple argument lists on one page should that ever be
+ # required...
+ #
+ if not fReadOnly:
+ sHtml += u'<script type="text/javascript">\n'
+ sHtml += u'\n';
+ sHtml += u'g_%s_aItems = { %s };\n' % (sName, ', '.join(('%s: 1' % (i,)) for i in range(len(aoVariations))),);
+ sHtml += u'g_%s_cItems = %s;\n' % (sName, len(aoVariations),);
+ sHtml += u'g_%s_iIdMod = %s;\n' % (sName, len(aoVariations) + 32);
+ sHtml += u'\n';
+ sHtml += u'function %s_removeEntry(sId)\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' if (g_%s_cItems > 1)\n' % (sName,);
+ sHtml += u' {\n';
+ sHtml += u' g_%s_cItems--;\n' % (sName,);
+ sHtml += u' delete g_%s_aItems[sId];\n' % (sName,);
+ sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName);
+ sHtml += u'\n';
+ for iInput in range(8):
+ sHtml += u' removeHtmlNode(\'%s[\' + sId + \'][%s]\');\n' % (sName, iInput,);
+ sHtml += u' }\n';
+ sHtml += u'}\n';
+ sHtml += u'\n';
+ sHtml += u'function %s_extendListEx(sSubName, cGangMembers, cSecTimeout, sArgs, sTestBoxReqExpr, sBuildReqExpr)\n' \
+ % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' var oElement = document.getElementById(\'%s\');\n' % (sTableId,);
+ sHtml += u' var oTBody = document.createElement(\'tbody\');\n';
+ sHtml += u' var sHtml = \'\';\n';
+ sHtml += u' var sId;\n';
+ sHtml += u'\n';
+ sHtml += u' g_%s_iIdMod += 1;\n' % (sName,);
+ sHtml += u' sId = g_%s_iIdMod.toString();\n' % (sName,);
+
+ oVarDefaults = TestCaseArgsData();
+ oVarDefaults.convertToParamNull();
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-first-row">\';\n';
+ sHtml += u' sHtml += \' <td>Sub-Name:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-subname">' \
+ '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + sSubName + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sSubName, sName,);
+ sHtml += u' sHtml += \' <td>Gang Members:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-tiny-int">' \
+ '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + cGangMembers + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_cGangMembers, sName,);
+ sHtml += u' sHtml += \' <td>Timeout:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-int">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][1]" value="\'+ cSecTimeout + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_cSecTimeout, sName,);
+ sHtml += u' sHtml += \' <td><a href="#" onclick="%s_removeEntry(\\\'\' + sId + \'\\\');"> Remove</a></td>\';\n' \
+ % (sName, );
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n';
+ sHtml += u' sHtml += \' <td>Arguments:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sArgs + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sArgs, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n';
+ sHtml += u' sHtml += \' <td>TestBox Reqs:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sTestBoxReqExpr' \
+ u' + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' sHtml += \'<tr class="tmform-testcasevars-final-row">\';\n';
+ sHtml += u' sHtml += \' <td>Build Reqs:</td>\';\n';
+ sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sBuildReqExpr + \'"></td>\';\n' \
+ % (sName, TestCaseArgsData.ksParam_sBuildReqExpr, sName,);
+ sHtml += u' sHtml += \' <td></td>\';\n';
+ sHtml += u' sHtml += \'</tr>\';\n'
+ sHtml += u'\n';
+ sHtml += u' oTBody.id = \'%s[\' + sId + \'][6]\';\n' % (sName,);
+ sHtml += u' oTBody.innerHTML = sHtml;\n';
+ sHtml += u'\n';
+ sHtml += u' oElement.appendChild(oTBody);\n';
+ sHtml += u'\n';
+ sHtml += u' g_%s_aItems[sId] = 1;\n' % (sName,);
+ sHtml += u' g_%s_cItems++;\n' % (sName,);
+ sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName);
+ sHtml += u'}\n';
+ sHtml += u'function %s_extendList()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("%s", "%s", "%s", "%s", "%s", "%s");\n' % (sName,
+ escapeAttr(unicode(oVarDefaults.sSubName)), escapeAttr(unicode(oVarDefaults.cGangMembers)),
+ escapeAttr(unicode(oVarDefaults.cSecTimeout)), escapeAttr(oVarDefaults.sArgs),
+ escapeAttr(oVarDefaults.sTestBoxReqExpr), escapeAttr(oVarDefaults.sBuildReqExpr), );
+ sHtml += u'}\n';
+ if config.g_kfVBoxSpecific:
+ sSecTimeoutDef = escapeAttr(unicode(oVarDefaults.cSecTimeout));
+ sHtml += u'function vbox_%s_add_uni()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("1-raw", "1", "%s", "--cpu-counts 1 --virt-modes raw", ' \
+ u' "", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \
+ u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-np", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \
+ u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'function vbox_%s_add_uni_amd64()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \
+ u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("1-np", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \
+ u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'function vbox_%s_add_smp()\n' % (sName,);
+ sHtml += u'{\n';
+ sHtml += u' %s_extendListEx("2-hw", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt",' \
+ u' "fCpuHwVirt is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("2-np", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt-np",' \
+ u' "fCpuNestedPaging is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("3-hw", "1", "%s", "--cpu-counts 3 --virt-modes hwvirt",' \
+ u' "fCpuHwVirt is True and cCpus >= 3", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u' %s_extendListEx("4-np", "1", "%s", "--cpu-counts 4 --virt-modes hwvirt-np ",' \
+ u' "fCpuNestedPaging is True and cCpus >= 4", "");\n' % (sName, sSecTimeoutDef);
+ #sHtml += u' %s_extendListEx("6-hw", "1", "%s", "--cpu-counts 6 --virt-modes hwvirt",' \
+ # u' "fCpuHwVirt is True and cCpus >= 6", "");\n' % (sName, sSecTimeoutDef);
+ #sHtml += u' %s_extendListEx("8-np", "1", "%s", "--cpu-counts 8 --virt-modes hwvirt-np",' \
+ # u' "fCpuNestedPaging is True and cCpus >= 8", "");\n' % (sName, sSecTimeoutDef);
+ sHtml += u'}\n';
+ sHtml += u'</script>\n';
+
+
+ #
+ # List current entries.
+ #
+ sHtml += u'<input type="hidden" name="%s" id="%s" value="%s">\n' \
+ % (sName, sName, ','.join(unicode(i) for i in range(len(aoVariations))), );
+ sHtml += u' <table id="%s" class="tmform-testcasevars">\n' % (sTableId,)
+ if not fReadOnly:
+ sHtml += u' <caption>\n' \
+ u' <a href="#" onClick="%s_extendList()">Add</a>\n' % (sName,);
+ if config.g_kfVBoxSpecific:
+ sHtml += u' [<a href="#" onClick="vbox_%s_add_uni()">Single CPU Variations</a>\n' % (sName,);
+ sHtml += u' <a href="#" onClick="vbox_%s_add_uni_amd64()">amd64</a>]\n' % (sName,);
+ sHtml += u' [<a href="#" onClick="vbox_%s_add_smp()">SMP Variations</a>]\n' % (sName,);
+ sHtml += u' </caption>\n';
+
+ dSubErrors = {};
+ if sName in self._dErrors and isinstance(self._dErrors[sName], dict):
+ dSubErrors = self._dErrors[sName];
+
+ for iVar, _ in enumerate(aoVariations):
+ oVar = copy.copy(aoVariations[iVar]);
+ oVar.convertToParamNull();
+
+ sHtml += u'<tbody id="%s[%s][6]">\n' % (sName, iVar,)
+ sHtml += u' <tr class="tmform-testcasevars-first-row">\n' \
+ u' <td>Sub-name:</td>' \
+ u' <td class="tmform-field-subname"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \
+ u' <td>Gang Members:</td>' \
+ u' <td class="tmform-field-tiny-int"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \
+ u' <td>Timeout:</td>' \
+ u' <td class="tmform-field-int"><input name="%s[%s][%s]" id="%s[%s][2]" value="%s"%s></td>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sSubName, sName, iVar, oVar.sSubName, sReadOnlyAttr,
+ sName, iVar, TestCaseArgsData.ksParam_cGangMembers, sName, iVar, oVar.cGangMembers, sReadOnlyAttr,
+ sName, iVar, TestCaseArgsData.ksParam_cSecTimeout, sName, iVar,
+ utils.formatIntervalSeconds2(oVar.cSecTimeout), sReadOnlyAttr, );
+ if not fReadOnly:
+ sHtml += u' <td><a href="#" onclick="%s_removeEntry(\'%s\');">Remove</a></td>\n' \
+ % (sName, iVar);
+ else:
+ sHtml += u' <td></td>\n';
+ sHtml += u' <td class="tmform-testcasevars-stupid-border-column"></td>\n' \
+ u' </tr>\n';
+
+ sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \
+ u' <td>Arguments:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][3]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sArgs, sName, iVar, escapeAttr(oVar.sArgs), sReadOnlyAttr)
+
+ sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \
+ u' <td>TestBox Reqs:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][4]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName, iVar,
+ escapeAttr(oVar.sTestBoxReqExpr), sReadOnlyAttr)
+
+ sHtml += u' <tr class="tmform-testcasevars-final-row">\n' \
+ u' <td>Build Reqs:</td>' \
+ u' <td class="tmform-field-wide100" colspan="6">' \
+ u'<input name="%s[%s][%s]" id="%s[%s][5]" value="%s"%s></td>\n' \
+ u' <td></td>\n' \
+ u' </tr>\n' \
+ % ( sName, iVar, TestCaseArgsData.ksParam_sBuildReqExpr, sName, iVar,
+ escapeAttr(oVar.sBuildReqExpr), sReadOnlyAttr)
+
+
+ if iVar in dSubErrors:
+ sHtml += u' <tr><td colspan="4"><p align="left" class="tmform-error-desc">%s</p></td></tr>\n' \
+ % (self._escapeErrorText(dSubErrors[iVar]),);
+
+ sHtml += u'</tbody>\n';
+ sHtml += u' </table>\n'
+ sHtml += u'</li>\n'
+
+ return self._add(sHtml)
+
+ def addListOfTestGroupMembers(self, sName, aoTestGroupMembers, aoAllTestCases, sLabel, # pylint: disable=too-many-locals
+ fReadOnly = True):
+ """
+ For WuiTestGroup.
+ """
+ assert len(aoTestGroupMembers) <= len(aoAllTestCases);
+ self._addLabel(sName, sLabel);
+ if not aoAllTestCases:
+ return self._add('<li>No testcases.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( TestGroupDataEx.ksParam_aidTestCases,
+ ','.join([unicode(oTestCase.idTestCase) for oTestCase in aoAllTestCases]), ));
+
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th rowspan="2"></th>\n'
+ u' <th rowspan="2">Test Case</th>\n'
+ u' <th rowspan="2">All Vars</th>\n'
+ u' <th rowspan="2">Priority [0..31]</th>\n'
+ u' <th colspan="4" align="center">Variations</th>\n'
+ u' </tr>\n'
+ u' <tr>\n'
+ u' <th>Included</th>\n'
+ u' <th>Gang size</th>\n'
+ u' <th>Timeout</th>\n'
+ u' <th>Arguments</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ if self._fReadOnly:
+ fReadOnly = True;
+ sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+
+ oDefMember = TestGroupMemberData();
+ aoTestGroupMembers = list(aoTestGroupMembers); # Copy it so we can pop.
+ for iTestCase, _ in enumerate(aoAllTestCases):
+ oTestCase = aoAllTestCases[iTestCase];
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoTestGroupMembers):
+ if aoTestGroupMembers[i].oTestCase.idTestCase == oTestCase.idTestCase:
+ oMember = aoTestGroupMembers.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestCase.idTestCase,);
+ self._add(u' <tr class="%s">\n'
+ u' <td rowspan="%d">\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestCase
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestCase & 1 else 'tmeven',
+ len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_idTestCase, oTestCase.idTestCase,
+ sPrefix, TestGroupMemberData.ksParam_idTestGroup, -1 if oMember is None else oMember.idTestGroup,
+ sPrefix, TestGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ TestGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestCase.idTestCase, oTestCase.idTestCase, escapeElem(oTestCase.sName),
+ ));
+ self._add(u' <td rowspan="%d" align="left">%s</td>\n'
+ % ( len(oTestCase.aoTestCaseArgs), escapeElem(oTestCase.sName), ));
+
+ self._add(u' <td rowspan="%d" title="Include all variations (checked) or choose a set?">\n'
+ u' <input name="%s[%s]" type="checkbox"%s%s value="-1">\n'
+ u' </td>\n'
+ % ( len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs,
+ ' checked' if oMember is None or oMember.aidTestCaseArgs is None else '', sCheckBoxAttr, ));
+
+ self._add(u' <td rowspan="%d" align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( len(oTestCase.aoTestCaseArgs),
+ sPrefix, TestGroupMemberData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ # Argument variations.
+ aidTestCaseArgs = [] if oMember is None or oMember.aidTestCaseArgs is None else oMember.aidTestCaseArgs;
+ for iVar, oVar in enumerate(oTestCase.aoTestCaseArgs):
+ if iVar > 0:
+ self._add(' <tr class="%s">\n' % ('tmodd' if iTestCase & 1 else 'tmeven',));
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="checkbox"%s%s value="%d">'
+ u' </td>\n'
+ % ( sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs,
+ ' checked' if oVar.idTestCaseArgs in aidTestCaseArgs else '', sCheckBoxAttr, oVar.idTestCaseArgs,
+ ));
+ self._add(u' <td align="center">%s</td>\n'
+ u' <td align="center">%s</td>\n'
+ u' <td align="left">%s</td>\n'
+ % ( oVar.cGangMembers,
+ 'Default' if oVar.cSecTimeout is None else oVar.cSecTimeout,
+ escapeElem(oVar.sArgs) ));
+
+ self._add(u' </tr>\n');
+
+
+
+ if not oTestCase.aoTestCaseArgs:
+ self._add(u' <td></td> <td></td> <td></td> <td></td>\n'
+ u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupMembers(self, sName, aoSchedGroupMembers, aoAllRelevantTestGroups, # pylint: disable=too-many-locals
+ sLabel, idSchedGroup, fReadOnly = True):
+ """
+ For WuiAdminSchedGroup.
+ """
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoSchedGroupMembers) <= len(aoAllRelevantTestGroups);
+ self._addLabel(sName, sLabel);
+ if not aoAllRelevantTestGroups:
+ return self._add(u'<li>No test groups.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( SchedGroupDataEx.ksParam_aidTestGroups,
+ ','.join([unicode(oTestGroup.idTestGroup) for oTestGroup in aoAllRelevantTestGroups]), ));
+
+ self._add(u'<table class="tmformtbl tmformtblschedgroupmembers">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th></th>\n'
+ u' <th>Test Group</th>\n'
+ u' <th>Priority [0..31]</th>\n'
+ u' <th>Prerequisite Test Group</th>\n'
+ u' <th>Weekly schedule</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+ sComboBoxAttr = u' disabled' if fReadOnly else '';
+
+ oDefMember = SchedGroupMemberData();
+ aoSchedGroupMembers = list(aoSchedGroupMembers); # Copy it so we can pop.
+ for iTestGroup, _ in enumerate(aoAllRelevantTestGroups):
+ oTestGroup = aoAllRelevantTestGroups[iTestGroup];
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupMembers):
+ if aoSchedGroupMembers[i].oTestGroup.idTestGroup == oTestGroup.idTestGroup:
+ oMember = aoSchedGroupMembers.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestGroup.idTestGroup,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestGroup & 1 else 'tmeven',
+ sPrefix, SchedGroupMemberData.ksParam_idTestGroup, oTestGroup.idTestGroup,
+ sPrefix, SchedGroupMemberData.ksParam_idSchedGroup, idSchedGroup,
+ sPrefix, SchedGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, SchedGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, SchedGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestGroup.idTestGroup, oTestGroup.idTestGroup, escapeElem(oTestGroup.sName),
+ ));
+ self._add(u' <td>%s</td>\n' % ( escapeElem(oTestGroup.sName), ));
+
+ self._add(u' <td>\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ self._add(u' <td>\n'
+ u' <select name="%s[%s]" id="%s[%s]" class="tmform-combobox"%s>\n'
+ u' <option value="-1"%s>None</option>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq,
+ sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq,
+ sComboBoxAttr,
+ ' selected' if oMember is None or oMember.idTestGroupPreReq is None else '',
+ ));
+ for oTestGroup2 in aoAllRelevantTestGroups:
+ if oTestGroup2 != oTestGroup:
+ fSelected = oMember is not None and oTestGroup2.idTestGroup == oMember.idTestGroupPreReq;
+ self._add(' <option value="%s"%s>%s</option>\n'
+ % ( oTestGroup2.idTestGroup, ' selected' if fSelected else '', escapeElem(oTestGroup2.sName), ));
+ self._add(u' </select>\n'
+ u' </td>\n');
+
+ self._add(u' <td>\n'
+ u' Todo<input name="%s[%s]" type="hidden" value="%s">\n'
+ u' </td>\n'
+ % ( sPrefix, SchedGroupMemberData.ksParam_bmHourlySchedule,
+ '' if oMember is None else oMember.bmHourlySchedule, ));
+
+ self._add(u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupBoxes(self, sName, aoSchedGroupBoxes, # pylint: disable=too-many-locals
+ aoAllRelevantTestBoxes, sLabel, idSchedGroup, fReadOnly = True,
+ fUseTable = False): # (str, list[TestBoxDataEx], list[TestBoxDataEx], str, bool, bool) -> str
+ """
+ For WuiAdminSchedGroup.
+ """
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoSchedGroupBoxes) <= len(aoAllRelevantTestBoxes);
+ self._addLabel(sName, sLabel);
+ if not aoAllRelevantTestBoxes:
+ return self._add(u'<li>No test boxes.</li>\n')
+
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( SchedGroupDataEx.ksParam_aidTestBoxes,
+ ','.join([unicode(oTestBox.idTestBox) for oTestBox in aoAllRelevantTestBoxes]), ));
+
+ sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+ oDefMember = TestBoxDataForSchedGroup();
+ aoSchedGroupBoxes = list(aoSchedGroupBoxes); # Copy it so we can pop.
+
+ from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink;
+
+ if not fUseTable:
+ #
+ # Non-table version (see also addListOfOsArches).
+ #
+ self._add(' <div class="tmform-checkboxes-container">\n');
+
+ for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes):
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupBoxes):
+ if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox:
+ oMember = aoSchedGroupBoxes.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrf = u'%s[%d]' % (sName, oTestBox.idTestBox,);
+ self._add(u' <div class="tmform-checkbox-holder tmshade%u">\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ % ( iTestBox & 7,
+ sPrf, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox,
+ sPrf, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup,
+ sPrf, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrf, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrf, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName),
+ ));
+
+ self._add(u' <span class="tmform-priority tmform-testbox-priority">'
+ u'<input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s title="%s"></span>\n'
+ % ( sPrf, TestBoxDataForSchedGroup.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '',
+ escapeAttr("Priority [0..31]. Higher value means run more often.") ));
+
+ self._add(u' <span class="tmform-testbox-name">%s</span>\n'
+ % ( WuiTestBoxDetailsLink(oTestBox, sName = '%s (%s)' % (oTestBox.sName, oTestBox.sOs,)),));
+ self._add(u' </div>\n');
+ return self._add(u' </div></div></div>\n'
+ u' </li>\n');
+
+ #
+ # Table version.
+ #
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th></th>\n'
+ u' <th>Test Box</th>\n'
+ u' <th>Priority [0..31]</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes):
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoSchedGroupBoxes):
+ if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox:
+ oMember = aoSchedGroupBoxes.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oTestBox.idTestBox,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iTestBox & 1 else 'tmeven',
+ sPrefix, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName),
+ ));
+ self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oTestBox.sName), ));
+
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix,
+ TestBoxDataForSchedGroup.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+
+ self._add(u' </tr>\n');
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+ def addListOfSchedGroupsForTestBox(self, sName, aoInSchedGroups, aoAllSchedGroups, sLabel, # pylint: disable=too-many-locals
+ idTestBox, fReadOnly = None):
+ # type: (str, TestBoxInSchedGroupDataEx, SchedGroupData, str, bool) -> str
+ """
+ For WuiTestGroup.
+ """
+ from testmanager.core.testbox import TestBoxInSchedGroupData, TestBoxDataEx;
+
+ if fReadOnly is None or self._fReadOnly:
+ fReadOnly = self._fReadOnly;
+ assert len(aoInSchedGroups) <= len(aoAllSchedGroups);
+
+ # Only show selected groups in read-only mode.
+ if fReadOnly:
+ aoAllSchedGroups = [oCur.oSchedGroup for oCur in aoInSchedGroups]
+
+ self._addLabel(sName, sLabel);
+ if not aoAllSchedGroups:
+ return self._add('<li>No scheduling groups.</li>\n')
+
+ # Add special parameter with all the scheduling group IDs in the form.
+ self._add(u'<input name="%s" type="hidden" value="%s">\n'
+ % ( TestBoxDataEx.ksParam_aidSchedGroups,
+ ','.join([unicode(oSchedGroup.idSchedGroup) for oSchedGroup in aoAllSchedGroups]), ));
+
+ # Table header.
+ self._add(u'<table class="tmformtbl">\n'
+ u' <thead>\n'
+ u' <tr>\n'
+ u' <th rowspan="2"></th>\n'
+ u' <th rowspan="2">Schedulding Group</th>\n'
+ u' <th rowspan="2">Priority [0..31]</th>\n'
+ u' </tr>\n'
+ u' </thead>\n'
+ u' <tbody>\n'
+ );
+
+ # Table body.
+ if self._fReadOnly:
+ fReadOnly = True;
+ sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else '';
+
+ oDefMember = TestBoxInSchedGroupData();
+ aoInSchedGroups = list(aoInSchedGroups); # Copy it so we can pop.
+ for iSchedGroup, oSchedGroup in enumerate(aoAllSchedGroups):
+
+ # Is it a member?
+ oMember = None;
+ for i, _ in enumerate(aoInSchedGroups):
+ if aoInSchedGroups[i].idSchedGroup == oSchedGroup.idSchedGroup:
+ oMember = aoInSchedGroups.pop(i);
+ break;
+
+ # Start on the rows...
+ sPrefix = u'%s[%d]' % (sName, oSchedGroup.idSchedGroup,);
+ self._add(u' <tr class="%s">\n'
+ u' <td>\n'
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective
+ u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor
+ u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list)
+ u' </td>\n'
+ % ( 'tmodd' if iSchedGroup & 1 else 'tmeven',
+ sPrefix, TestBoxInSchedGroupData.ksParam_idSchedGroup, oSchedGroup.idSchedGroup,
+ sPrefix, TestBoxInSchedGroupData.ksParam_idTestBox, idTestBox,
+ sPrefix, TestBoxInSchedGroupData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire,
+ sPrefix, TestBoxInSchedGroupData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective,
+ sPrefix, TestBoxInSchedGroupData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor,
+ TestBoxDataEx.ksParam_aoInSchedGroups, '' if oMember is None else ' checked', sCheckBoxAttr,
+ oSchedGroup.idSchedGroup, oSchedGroup.idSchedGroup, escapeElem(oSchedGroup.sName),
+ ));
+ self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oSchedGroup.sName), ));
+
+ self._add(u' <td align="center">\n'
+ u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n'
+ u' </td>\n'
+ % ( sPrefix, TestBoxInSchedGroupData.ksParam_iSchedPriority,
+ (oMember if oMember is not None else oDefMember).iSchedPriority,
+ ' readonly class="tmform-input-readonly"' if fReadOnly else '', ));
+ self._add(u' </tr>\n');
+
+ return self._add(u' </tbody>\n'
+ u'</table>\n');
+
+
+ #
+ # Buttons.
+ #
+ def addSubmit(self, sLabel = 'Submit'):
+ """Adds the submit button to the form."""
+ if self._fReadOnly:
+ return True;
+ return self._add(u' <li>\n'
+ u' <br>\n'
+ u' <div class="tmform-field"><div class="tmform-field-submit">\n'
+ u' <label>&nbsp;</label>\n'
+ u' <input type="submit" value="%s">\n'
+ u' </div></div>\n'
+ u' </li>\n'
+ % (escapeElem(sLabel),));
+
+ def addReset(self):
+ """Adds a reset button to the form."""
+ if self._fReadOnly:
+ return True;
+ return self._add(u' <li>\n'
+ u' <div class="tmform-button"><div class="tmform-button-reset">\n'
+ u' <input type="reset" value="%s">\n'
+ u' </div></div>\n'
+ u' </li>\n');
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py
new file mode 100755
index 00000000..ed08bb0f
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraph.py $
+
+"""
+Test Manager Web-UI - Graph Helpers.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+class WuiHlpGraphDataTable(object): # pylint: disable=too-few-public-methods
+ """
+ Data table container.
+ """
+
+ class Row(object): # pylint: disable=too-few-public-methods
+ """A row."""
+ def __init__(self, sGroup, aoValues, asValues = None):
+ self.sName = sGroup;
+ self.aoValues = aoValues;
+ if asValues is None:
+ self.asValues = [str(oVal) for oVal in aoValues];
+ else:
+ assert len(asValues) == len(aoValues);
+ self.asValues = asValues;
+
+ def __init__(self, sGroupLable, asMemberLabels):
+ self.aoTable = [ WuiHlpGraphDataTable.Row(sGroupLable, asMemberLabels), ];
+ self.fHasStringValues = False;
+
+ def addRow(self, sGroup, aoValues, asValues = None):
+ """Adds a row to the data table."""
+ if asValues:
+ self.fHasStringValues = True;
+ self.aoTable.append(WuiHlpGraphDataTable.Row(sGroup, aoValues, asValues));
+ return True;
+
+ def getGroupCount(self):
+ """Gets the number of data groups (rows)."""
+ return len(self.aoTable) - 1;
+
+
+class WuiHlpGraphDataTableEx(object): # pylint: disable=too-few-public-methods
+ """
+ Data container for an table/graph with optional error bars on the Y values.
+ """
+
+ class DataSeries(object): # pylint: disable=too-few-public-methods
+ """
+ A data series.
+
+ The aoXValues, aoYValues and aoYErrorBars are parallel arrays, making a
+ series of (X,Y,Y-err-above-delta,Y-err-below-delta) points.
+
+ The error bars are optional.
+ """
+ def __init__(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None):
+ self.sName = sName;
+ self.aoXValues = aoXValues;
+ self.aoYValues = aoYValues;
+ self.asHtmlTooltips = asHtmlTooltips;
+ self.aoYErrorBarBelow = aoYErrorBarBelow;
+ self.aoYErrorBarAbove = aoYErrorBarAbove;
+
+ def __init__(self, sXUnit, sYUnit):
+ self.sXUnit = sXUnit;
+ self.sYUnit = sYUnit;
+ self.aoSeries = [];
+
+ def addDataSeries(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None):
+ """Adds an data series to the table."""
+ self.aoSeries.append(WuiHlpGraphDataTableEx.DataSeries(sName, aoXValues, aoYValues, asHtmlTooltips,
+ aoYErrorBarBelow, aoYErrorBarAbove));
+ return True;
+
+ def getDataSeriesCount(self):
+ """Gets the number of data series."""
+ return len(self.aoSeries);
+
+
+#
+# Dynamically choose implementation.
+#
+if True: # pylint: disable=using-constant-test
+ from testmanager.webui import wuihlpgraphgooglechart as GraphImplementation;
+else:
+ try:
+ import matplotlib; # pylint: disable=unused-import,import-error,import-error,wrong-import-order
+ from testmanager.webui import wuihlpgraphmatplotlib as GraphImplementation; # pylint: disable=ungrouped-imports
+ except:
+ from testmanager.webui import wuihlpgraphsimple as GraphImplementation;
+
+# pylint: disable=invalid-name
+WuiHlpBarGraph = GraphImplementation.WuiHlpBarGraph;
+WuiHlpLineGraph = GraphImplementation.WuiHlpLineGraph;
+WuiHlpLineGraphErrorbarY = GraphImplementation.WuiHlpLineGraphErrorbarY;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py
new file mode 100755
index 00000000..7ab6c6b8
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphbase.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Base Class.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+class WuiHlpGraphBase(object):
+ """
+ Base class for the Graph helpers.
+ """
+
+ ## Set of colors that can be used by child classes to color data series.
+ kasColors = \
+ [
+ '#0000ff', # Blue
+ '#00ff00', # Green
+ '#ff0000', # Red
+ '#000000', # Black
+
+ '#00ffff', # Cyan/Aqua
+ '#ff00ff', # Magenta/Fuchsia
+ '#ffff00', # Yellow
+ '#8b4513', # SaddleBrown
+
+ '#7b68ee', # MediumSlateBlue
+ '#ffc0cb', # Pink
+ '#bdb76b', # DarkKhaki
+ '#008080', # Teal
+
+ '#bc8f8f', # RosyBrown
+ '#000080', # Navy(Blue)
+ '#dc143c', # Crimson
+ '#800080', # Purple
+
+ '#daa520', # Goldenrod
+ '#40e0d0', # Turquoise
+ '#00bfff', # DeepSkyBlue
+ '#c0c0c0', # Silver
+ ];
+
+
+ def __init__(self, sId, oData, oDisp):
+ self._sId = sId;
+ self._oData = oData;
+ self._oDisp = oDisp;
+ # Graph output dimensions.
+ self._cxGraph = 1024;
+ self._cyGraph = 448;
+ self._cDpiGraph = 96;
+ # Other graph attributes
+ self._sTitle = None;
+ self._cPtFont = 8;
+
+ def headerContent(self):
+ """
+ Returns content that goes into the HTML header.
+ """
+ return '';
+
+ def renderGraph(self):
+ """
+ Renders the graph.
+ Returning HTML.
+ """
+ return '<p>renderGraph needs to be overridden by the child class!</p>';
+
+ def setTitle(self, sTitle):
+ """ Sets the graph title. """
+ self._sTitle = sTitle;
+ return True;
+
+ def setWidth(self, cx):
+ """ Sets the graph width. """
+ self._cxGraph = cx;
+ return True;
+
+ def setHeight(self, cy):
+ """ Sets the graph height. """
+ self._cyGraph = cy;
+ return True;
+
+ def setDpi(self, cDotsPerInch):
+ """ Sets the graph DPI. """
+ self._cDpiGraph = cDotsPerInch;
+ return True;
+
+ def setFontSize(self, cPtFont):
+ """ Sets the default font size. """
+ self._cPtFont = cPtFont;
+ return True;
+
+
+ @staticmethod
+ def calcSeriesColor(iSeries):
+ """ Returns a #rrggbb color code for the given series. """
+ return WuiHlpGraphBase.kasColors[iSeries % len(WuiHlpGraphBase.kasColors)];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py
new file mode 100755
index 00000000..b6861603
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py
@@ -0,0 +1,376 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphgooglechart.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Implemented using Google Charts.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import utils, webutils;
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+#*******************************************************************************
+#* Global Variables *
+#*******************************************************************************
+g_cGraphs = 0;
+
+class WuiHlpGraphGoogleChartsBase(WuiHlpGraphBase):
+ """ Base class for the Google Charts graphs. """
+ pass; # pylint: disable=unnecessary-pass
+
+
+class WuiHlpBarGraph(WuiHlpGraphGoogleChartsBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp);
+ self.fpMax = None;
+ self.fpMin = 0.0;
+ self.fYInverted = False;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Inverts the direction of the Y-axis direction. """
+ self.fYInverted = True;
+ return None;
+
+ def renderGraph(self):
+ aoTable = self._oData.aoTable # type: WuiHlpGraphDataTable
+
+ # Seems material (google.charts.Bar) cannot change the direction on the Y-axis,
+ # so we cannot get bars growing downwards from the top like we want for the
+ # reports. The classic charts OTOH cannot put X-axis labels on the top, but
+ # we just drop them all together instead, saving a little space.
+ fUseMaterial = False;
+
+ # Unique on load function.
+ global g_cGraphs;
+ iGraph = g_cGraphs;
+ g_cGraphs += 1;
+
+ sHtml = '<div id="%s">\n' % ( self._sId, );
+ sHtml += '<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>\n' \
+ '<script type="text/javascript">\n' \
+ 'google.charts.load("current", { packages: ["corechart", "bar"] });\n' \
+ 'google.setOnLoadCallback(tmDrawBarGraph%u);\n' \
+ 'function tmDrawBarGraph%u()\n' \
+ '{\n' \
+ ' var oGraph;\n' \
+ ' var dGraphOptions = \n' \
+ ' {\n' \
+ ' "title": "%s",\n' \
+ ' "hAxis": {\n' \
+ ' "title": "%s",\n' \
+ ' },\n' \
+ ' "vAxis": {\n' \
+ ' "direction": %s,\n' \
+ ' },\n' \
+ % ( iGraph,
+ iGraph,
+ webutils.escapeAttrJavaScriptStringDQ(self._sTitle) if self._sTitle is not None else '',
+ webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable and aoTable[0].sName else '',
+ '-1' if self.fYInverted else '1',
+ );
+ if fUseMaterial and self.fYInverted:
+ sHtml += ' "axes": { "x": { 0: { "side": "top" } }, "y": { "0": { "direction": -1, }, }, },\n';
+ sHtml += ' };\n';
+
+ # The data.
+ if self._oData.fHasStringValues and len(aoTable) > 1:
+ sHtml += ' var oData = new google.visualization.DataTable();\n';
+ # Column definitions.
+ sHtml += ' oData.addColumn("string", "%s");\n' \
+ % (webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable[0].sName else '',);
+ for iValue, oValue in enumerate(aoTable[0].aoValues):
+ oSampleValue = aoTable[1].aoValues[iValue];
+ if utils.isString(oSampleValue):
+ sHtml += ' oData.addColumn("string", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ else:
+ sHtml += ' oData.addColumn("number", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ sHtml += ' oData.addColumn({type: "string", role: "annotation"});\n';
+ # The data rows.
+ sHtml += ' oData.addRows([\n';
+ for oRow in aoTable[1:]:
+ if oRow.sName:
+ sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),);
+ else:
+ sRow = ' [ null';
+ for iValue, oValue in enumerate(oRow.aoValues):
+ if not utils.isString(oValue):
+ sRow += ', %s' % (oValue,);
+ else:
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ if oRow.asValues[iValue]:
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.asValues[iValue]),);
+ else:
+ sRow += ', null';
+ sHtml += sRow + '],\n';
+ sHtml += ' ]);\n';
+ else:
+ sHtml += ' var oData = google.visualization.arrayToDataTable([\n';
+ for oRow in aoTable:
+ sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),);
+ for oValue in oRow.aoValues:
+ if utils.isString(oValue):
+ sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),);
+ else:
+ sRow += ', %s' % (oValue,);
+ sHtml += sRow + '],\n';
+ sHtml += ' ]);\n';
+
+ # Create and draw.
+ if not fUseMaterial:
+ sHtml += ' oGraph = new google.visualization.ColumnChart(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ % ( self._sId, );
+ else:
+ sHtml += ' oGraph = new google.charts.Bar(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, google.charts.Bar.convertOptions(dGraphOptions));\n' \
+ % ( self._sId, );
+
+ # clean and return.
+ sHtml += ' oData = null;\n' \
+ ' return true;\n' \
+ '};\n';
+
+ sHtml += '</script>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+class WuiHlpLineGraph(WuiHlpGraphGoogleChartsBase):
+ """
+ Line graph.
+ """
+
+ ## @todo implement error bars.
+ kfNoErrorBarsSupport = True;
+
+ def __init__(self, sId, oData, oDisp = None, fErrorBarY = False):
+ # oData must be a WuiHlpGraphDataTableEx like object.
+ WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp);
+ self._cMaxErrorBars = 12;
+ self._fErrorBarY = fErrorBarY;
+
+ def setErrorBarY(self, fEnable):
+ """ Enables or Disables error bars, making this work like a line graph. """
+ self._fErrorBarY = fEnable;
+ return True;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ fSlideFilter = True;
+
+ # Tooltips?
+ cTooltips = 0;
+ for oSeries in self._oData.aoSeries:
+ cTooltips += oSeries.asHtmlTooltips is not None;
+
+ # Unique on load function.
+ global g_cGraphs;
+ iGraph = g_cGraphs;
+ g_cGraphs += 1;
+
+ sHtml = '<div id="%s">\n' % ( self._sId, );
+ if fSlideFilter:
+ sHtml += ' <table><tr><td><div id="%s_graph"/></td></tr><tr><td><div id="%s_filter"/></td></tr></table>\n' \
+ % ( self._sId, self._sId, );
+
+ sHtml += '<script type="text/javascript" src="https://www.google.com/jsapi"></script>\n' \
+ '<script type="text/javascript">\n' \
+ 'google.load("visualization", "1.0", { packages: ["corechart"%s] });\n' \
+ 'google.setOnLoadCallback(tmDrawLineGraph%u);\n' \
+ 'function tmDrawLineGraph%u()\n' \
+ '{\n' \
+ ' var fnResize;\n' \
+ ' var fnRedraw;\n' \
+ ' var idRedrawTimer = null;\n' \
+ ' var cxCur = getElementWidthById("%s") - 20;\n' \
+ ' var oGraph;\n' \
+ ' var oData = new google.visualization.DataTable();\n' \
+ ' var fpXYRatio = %u / %u;\n' \
+ ' var dGraphOptions = \n' \
+ ' {\n' \
+ ' "title": "%s",\n' \
+ ' "width": cxCur,\n' \
+ ' "height": Math.round(cxCur / fpXYRatio),\n' \
+ ' "pointSize": 2,\n' \
+ ' "fontSize": %u,\n' \
+ ' "hAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \
+ ' "vAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \
+ ' "theme": "maximized",\n' \
+ ' "tooltip": { "isHtml": %s }\n' \
+ ' };\n' \
+ % ( ', "controls"' if fSlideFilter else '',
+ iGraph,
+ iGraph,
+ self._sId,
+ self._cxGraph, self._cyGraph,
+ self._sTitle if self._sTitle is not None else '',
+ self._cPtFont * self._cDpiGraph / 72, # fudge
+ self._oData.sXUnit if self._oData.sXUnit else '',
+ self._oData.sYUnit if self._oData.sYUnit else '',
+ 'true' if cTooltips > 0 else 'false',
+ );
+ if fSlideFilter:
+ sHtml += ' var oDashboard = new google.visualization.Dashboard(document.getElementById("%s"));\n' \
+ ' var oSlide = new google.visualization.ControlWrapper({\n' \
+ ' "controlType": "NumberRangeFilter",\n' \
+ ' "containerId": "%s_filter",\n' \
+ ' "options": {\n' \
+ ' "filterColumnIndex": 0,\n' \
+ ' "ui": { "width": getElementWidthById("%s") / 2 }, \n' \
+ ' }\n' \
+ ' });\n' \
+ % ( self._sId,
+ self._sId,
+ self._sId,);
+
+ # Data variables.
+ for iSeries, oSeries in enumerate(self._oData.aoSeries):
+ sHtml += ' var aSeries%u = [\n' % (iSeries,);
+ if oSeries.asHtmlTooltips is None:
+ sHtml += '[%s,%s]' % ( oSeries.aoXValues[0], oSeries.aoYValues[0],);
+ for i in range(1, len(oSeries.aoXValues)):
+ if (i & 16) == 0: sHtml += '\n';
+ sHtml += ',[%s,%s]' % ( oSeries.aoXValues[i], oSeries.aoYValues[i], );
+ else:
+ sHtml += '[%s,%s,"%s"]' \
+ % ( oSeries.aoXValues[0], oSeries.aoYValues[0],
+ webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[0]),);
+ for i in range(1, len(oSeries.aoXValues)):
+ if (i & 16) == 0: sHtml += '\n';
+ sHtml += ',[%s,%s,"%s"]' \
+ % ( oSeries.aoXValues[i], oSeries.aoYValues[i],
+ webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[i]),);
+
+ sHtml += '];\n'
+
+ sHtml += ' oData.addColumn("number", "%s");\n' % (self._oData.sXUnit if self._oData.sXUnit else '',);
+ cVColumns = 0;
+ for oSeries in self._oData.aoSeries:
+ sHtml += ' oData.addColumn("number", "%s");\n' % (oSeries.sName,);
+ if oSeries.asHtmlTooltips:
+ sHtml += ' oData.addColumn({"type": "string", "role": "tooltip", "p": {"html": true}});\n';
+ cVColumns += 1;
+ cVColumns += 1;
+ sHtml += 'var i;\n'
+
+ cVColumsDone = 0;
+ for iSeries, oSeries in enumerate(self._oData.aoSeries):
+ sVar = 'aSeries%u' % (iSeries,);
+ sHtml += ' for (i = 0; i < %s.length; i++)\n' \
+ ' {\n' \
+ ' oData.addRow([%s[i][0]%s,%s[i][1]%s%s]);\n' \
+ % ( sVar,
+ sVar,
+ ',null' * cVColumsDone,
+ sVar,
+ '' if oSeries.asHtmlTooltips is None else ',%s[i][2]' % (sVar,),
+ ',null' * (cVColumns - cVColumsDone - 1 - (oSeries.asHtmlTooltips is not None)),
+ );
+ sHtml += ' }\n' \
+ ' %s = null\n' \
+ % (sVar,);
+ cVColumsDone += 1 + (oSeries.asHtmlTooltips is not None);
+
+ # Create and draw.
+ if fSlideFilter:
+ sHtml += ' oGraph = new google.visualization.ChartWrapper({\n' \
+ ' "chartType": "LineChart",\n' \
+ ' "containerId": "%s_graph",\n' \
+ ' "options": dGraphOptions\n' \
+ ' });\n' \
+ ' oDashboard.bind(oSlide, oGraph);\n' \
+ ' oDashboard.draw(oData);\n' \
+ % ( self._sId, );
+ else:
+ sHtml += ' oGraph = new google.visualization.LineChart(document.getElementById("%s"));\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ % ( self._sId, );
+
+ # Register a resize handler for redrawing the graph, using a timer to delay it.
+ sHtml += ' fnRedraw = function() {\n' \
+ ' var cxNew = getElementWidthById("%s") - 6;\n' \
+ ' if (Math.abs(cxNew - cxCur) > 8)\n' \
+ ' {\n' \
+ ' cxCur = cxNew;\n' \
+ ' dGraphOptions["width"] = cxNew;\n' \
+ ' dGraphOptions["height"] = Math.round(cxNew / fpXYRatio);\n' \
+ ' oGraph.draw(oData, dGraphOptions);\n' \
+ ' }\n' \
+ ' clearTimeout(idRedrawTimer);\n' \
+ ' idRedrawTimer = null;\n' \
+ ' return true;\n' \
+ ' };\n' \
+ ' fnResize = function() {\n' \
+ ' if (idRedrawTimer != null) { clearTimeout(idRedrawTimer); } \n' \
+ ' idRedrawTimer = setTimeout(fnRedraw, 512);\n' \
+ ' return true;\n' \
+ ' };\n' \
+ ' if (window.attachEvent)\n' \
+ ' { window.attachEvent("onresize", fnResize); }\n' \
+ ' else if (window.addEventListener)\n' \
+ ' { window.addEventListener("resize", fnResize, true); }\n' \
+ % ( self._sId, );
+
+ # clean up what the callbacks don't need.
+ sHtml += ' oData = null;\n' \
+ ' aaaSeries = null;\n';
+
+ # done;
+ sHtml += ' return true;\n' \
+ '};\n';
+
+ sHtml += '</script>\n' \
+ '</div>\n';
+ return sHtml;
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py
new file mode 100755
index 00000000..cb5cf893
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py
@@ -0,0 +1,341 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphmatplotlib.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Implemented using matplotlib.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard Python Import and extensions installed on the system.
+import re;
+import sys;
+if sys.version_info[0] >= 3:
+ from io import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+else:
+ from StringIO import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias
+
+import matplotlib; # pylint: disable=import-error
+matplotlib.use('Agg'); # Force backend.
+import matplotlib.pyplot; # pylint: disable=import-error
+from numpy import arange as numpy_arange; # pylint: disable=no-name-in-module,import-error,wrong-import-order
+
+# Validation Kit imports.
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+class WuiHlpGraphMatplotlibBase(WuiHlpGraphBase):
+ """ Base class for the matplotlib graphs. """
+
+ def __init__(self, sId, oData, oDisp):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+ self._fXkcdStyle = True;
+
+ def setXkcdStyle(self, fEnabled = True):
+ """ Enables xkcd style graphs for implementations that supports it. """
+ self._fXkcdStyle = fEnabled;
+ return True;
+
+ def _createFigure(self):
+ """
+ Wrapper around matplotlib.pyplot.figure that feeds the figure the
+ basic graph configuration.
+ """
+ if self._fXkcdStyle and matplotlib.__version__ > '1.2.9':
+ matplotlib.pyplot.xkcd(); # pylint: disable=no-member
+ matplotlib.rcParams.update({'font.size': self._cPtFont});
+
+ oFigure = matplotlib.pyplot.figure(figsize = (float(self._cxGraph) / self._cDpiGraph,
+ float(self._cyGraph) / self._cDpiGraph),
+ dpi = self._cDpiGraph);
+ return oFigure;
+
+ def _produceSvg(self, oFigure, fTightLayout = True):
+ """ Creates an SVG string from the given figure. """
+ oOutput = StringIO();
+ if fTightLayout:
+ oFigure.tight_layout();
+ oFigure.savefig(oOutput, format = 'svg');
+
+ if self._oDisp and self._oDisp.isBrowserGecko('20100101'):
+ # This browser will stretch images to fit if no size or width is given.
+ sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet"';
+ else:
+ # Chrome and IE likes to have the sizes as well as the viewBox.
+ sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet" \2 \4';
+ return re.sub(r'(<svg) (height="\d+pt") (version="\d+.\d+" viewBox="\d+ \d+ \d+ \d+") (width="\d+pt")',
+ sSubstitute,
+ oOutput.getvalue().decode('utf8'),
+ count = 1);
+
+class WuiHlpBarGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self.fpMax = None;
+ self.fpMin = 0.0;
+ self.cxBarWidth = None;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Inverts the direction of the Y-axis direction. """
+ ## @todo self.fYInverted = True;
+ return None;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ aoTable = self._oData.aoTable;
+
+ #
+ # Extract/structure the required data.
+ #
+ aoSeries = [];
+ for j in range(len(aoTable[1].aoValues)):
+ aoSeries.append([]);
+ asNames = [];
+ oXRange = numpy_arange(self._oData.getGroupCount());
+ fpMin = self.fpMin;
+ fpMax = self.fpMax;
+ if self.fpMax is None:
+ fpMax = float(aoTable[1].aoValues[0]);
+
+ for i in range(1, len(aoTable)):
+ asNames.append(aoTable[i].sName);
+ for j, oValue in enumerate(aoTable[i].aoValues):
+ fpValue = float(oValue);
+ aoSeries[j].append(fpValue);
+ if fpValue < fpMin:
+ fpMin = fpValue;
+ if fpValue > fpMax:
+ fpMax = fpValue;
+
+ fpMid = fpMin + (fpMax - fpMin) / 2.0;
+
+ if self.cxBarWidth is None:
+ self.cxBarWidth = 1.0 / (len(aoTable[0].asValues) + 1.1);
+
+ # Render the PNG.
+ oFigure = self._createFigure();
+ oSubPlot = oFigure.add_subplot(1, 1, 1);
+
+ aoBars = [];
+ for i, _ in enumerate(aoSeries):
+ sColor = self.calcSeriesColor(i);
+ aoBars.append(oSubPlot.bar(oXRange + self.cxBarWidth * i,
+ aoSeries[i],
+ self.cxBarWidth,
+ color = sColor,
+ align = 'edge'));
+
+ #oSubPlot.set_title('Title')
+ #oSubPlot.set_xlabel('X-axis')
+ #oSubPlot.set_xticks(oXRange + self.cxBarWidth);
+ oSubPlot.set_xticks(oXRange);
+ oLegend = oSubPlot.legend(aoTable[0].asValues, loc = 'best', fancybox = True);
+ oLegend.get_frame().set_alpha(0.5);
+ oSubPlot.set_xticklabels(asNames, ha = "left");
+ #oSubPlot.set_ylabel('Y-axis')
+ oSubPlot.set_yticks(numpy_arange(fpMin, fpMax + (fpMax - fpMin) / 10 * 0, fpMax / 10));
+ oSubPlot.grid(True);
+ fpPadding = (fpMax - fpMin) * 0.02;
+ for i, _ in enumerate(aoBars):
+ aoRects = aoBars[i]
+ for j, _ in enumerate(aoRects):
+ oRect = aoRects[j];
+ fpValue = float(aoTable[j + 1].aoValues[i]);
+ if fpValue <= fpMid:
+ oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0,
+ oRect.get_height() + fpPadding,
+ aoTable[j + 1].asValues[i],
+ ha = 'center', va = 'bottom', rotation = 'vertical', alpha = 0.6, fontsize = 'small');
+ else:
+ oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0,
+ oRect.get_height() - fpPadding,
+ aoTable[j + 1].asValues[i],
+ ha = 'center', va = 'top', rotation = 'vertical', alpha = 0.6, fontsize = 'small');
+
+ return self._produceSvg(oFigure);
+
+
+
+
+class WuiHlpLineGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Line graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None, fErrorBarY = False):
+ # oData must be a WuiHlpGraphDataTableEx like object.
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self._cMaxErrorBars = 12;
+ self._fErrorBarY = fErrorBarY;
+
+ def setErrorBarY(self, fEnable):
+ """ Enables or Disables error bars, making this work like a line graph. """
+ self._fErrorBarY = fEnable;
+ return True;
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ aoSeries = self._oData.aoSeries;
+
+ oFigure = self._createFigure();
+ oSubPlot = oFigure.add_subplot(1, 1, 1);
+ if self._oData.sYUnit is not None:
+ oSubPlot.set_ylabel(self._oData.sYUnit);
+ if self._oData.sXUnit is not None:
+ oSubPlot.set_xlabel(self._oData.sXUnit);
+
+ cSeriesNames = 0;
+ cYMin = 1000;
+ cYMax = 0;
+ for iSeries, oSeries in enumerate(aoSeries):
+ sColor = self.calcSeriesColor(iSeries);
+ cYMin = min(cYMin, min(oSeries.aoYValues));
+ cYMax = max(cYMax, max(oSeries.aoYValues));
+ if not self._fErrorBarY:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor);
+ elif len(oSeries.aoXValues) > self._cMaxErrorBars:
+ if matplotlib.__version__ < '1.3.0':
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor);
+ else:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues,
+ yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove],
+ errorevery = len(oSeries.aoXValues) / self._cMaxErrorBars,
+ color = sColor );
+ else:
+ oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues,
+ yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove],
+ color = sColor);
+ cSeriesNames += oSeries.sName is not None;
+
+ if cYMin != 0 or cYMax != 0:
+ oSubPlot.set_ylim(bottom = 0);
+
+ if cSeriesNames > 0:
+ oLegend = oSubPlot.legend([oSeries.sName for oSeries in aoSeries], loc = 'best', fancybox = True);
+ oLegend.get_frame().set_alpha(0.5);
+
+ if self._sTitle is not None:
+ oSubPlot.set_title(self._sTitle);
+
+ if self._cxGraph >= 256:
+ oSubPlot.minorticks_on();
+ oSubPlot.grid(True, 'major', axis = 'both');
+ oSubPlot.grid(True, 'both', axis = 'x');
+
+ if True: # pylint: disable=using-constant-test
+ # oSubPlot.axis('off');
+ #oSubPlot.grid(True, 'major', axis = 'none');
+ #oSubPlot.grid(True, 'both', axis = 'none');
+ matplotlib.pyplot.setp(oSubPlot, xticks = [], yticks = []);
+
+ return self._produceSvg(oFigure);
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True);
+
+
+class WuiHlpMiniSuccessRateGraph(WuiHlpGraphMatplotlibBase):
+ """
+ Mini rate graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ """
+ oData must be a WuiHlpGraphDataTableEx like object, but only aoSeries,
+ aoSeries[].aoXValues, and aoSeries[].aoYValues will be used. The
+ values are expected to be a percentage, i.e. values between 0 and 100.
+ """
+ WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp);
+ self.setFontSize(6);
+
+ def renderGraph(self): # pylint: disable=too-many-locals
+ assert len(self._oData.aoSeries) == 1;
+ oSeries = self._oData.aoSeries[0];
+
+ # hacking
+ #self.setWidth(512);
+ #self.setHeight(128);
+ # end
+
+ oFigure = self._createFigure();
+ from mpl_toolkits.axes_grid.axislines import SubplotZero; # pylint: disable=import-error
+ oAxis = SubplotZero(oFigure, 111);
+ oFigure.add_subplot(oAxis);
+
+ # Disable all the normal axis.
+ oAxis.axis['right'].set_visible(False)
+ oAxis.axis['top'].set_visible(False)
+ oAxis.axis['bottom'].set_visible(False)
+ oAxis.axis['left'].set_visible(False)
+
+ # Use the zero axis instead.
+ oAxis.axis['yzero'].set_axisline_style('-|>');
+ oAxis.axis['yzero'].set_visible(True);
+ oAxis.axis['xzero'].set_axisline_style('-|>');
+ oAxis.axis['xzero'].set_visible(True);
+
+ if oSeries.aoYValues[-1] == 100:
+ sColor = 'green';
+ elif oSeries.aoYValues[-1] > 75:
+ sColor = 'yellow';
+ else:
+ sColor = 'red';
+ oAxis.plot(oSeries.aoXValues, oSeries.aoYValues, '.-', color = sColor, linewidth = 3);
+ oAxis.fill_between(oSeries.aoXValues, oSeries.aoYValues, facecolor = sColor, alpha = 0.5)
+
+ oAxis.set_xlim(left = -0.01);
+ oAxis.set_xticklabels([]);
+ oAxis.set_xmargin(1);
+
+ oAxis.set_ylim(bottom = 0, top = 100);
+ oAxis.set_yticks([0, 50, 100]);
+ oAxis.set_ylabel('%');
+ #oAxis.set_yticklabels([]);
+ oAxis.set_yticklabels(['', '%', '']);
+
+ return self._produceSvg(oFigure, False);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py
new file mode 100755
index 00000000..9be0b565
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+# $Id: wuihlpgraphsimple.py $
+
+"""
+Test Manager Web-UI - Graph Helpers - Simple/Stub Implementation.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common.webutils import escapeAttr, escapeElem;
+from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase;
+
+
+
+class WuiHlpBarGraph(WuiHlpGraphBase):
+ """
+ Bar graph.
+ """
+
+ def __init__(self, sId, oData, oDisp = None):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+ self.cxMaxBar = 480;
+ self.fpMax = None;
+ self.fpMin = 0.0;
+
+ def setRangeMax(self, fpMax):
+ """ Sets the max range."""
+ self.fpMax = float(fpMax);
+ return None;
+
+ def invertYDirection(self):
+ """ Not supported. """
+ return None;
+
+ def renderGraph(self):
+ aoTable = self._oData.aoTable;
+ sReport = '<div class="tmbargraph">\n';
+
+ # Figure the range.
+ fpMin = self.fpMin;
+ fpMax = self.fpMax;
+ if self.fpMax is None:
+ fpMax = float(aoTable[1].aoValues[0]);
+ for i in range(1, len(aoTable)):
+ for oValue in aoTable[i].aoValues:
+ fpValue = float(oValue);
+ if fpValue < fpMin:
+ fpMin = fpValue;
+ if fpValue > fpMax:
+ fpMax = fpValue;
+ assert fpMin >= 0;
+
+ # Format the data.
+ sReport += '<table class="tmbargraphl1" border="1" id="%s">\n' % (escapeAttr(self._sId),);
+ for i in range(1, len(aoTable)):
+ oRow = aoTable[i];
+ sReport += ' <tr>\n' \
+ ' <td>%s</td>\n' \
+ ' <td height="100%%" width="%spx">\n' \
+ ' <table class="tmbargraphl2" height="100%%" width="100%%" ' \
+ 'border="0" cellspacing="0" cellpadding="0">\n' \
+ % (escapeElem(oRow.sName), escapeAttr(str(self.cxMaxBar + 2)));
+ for j, oValue in enumerate(oRow.aoValues):
+ cPct = int(float(oValue) * 100 / fpMax);
+ cxBar = int(float(oValue) * self.cxMaxBar / fpMax);
+ sValue = escapeElem(oRow.asValues[j]);
+ sColor = self.kasColors[j % len(self.kasColors)];
+ sInvColor = 'white';
+ if sColor[0] == '#' and len(sColor) == 7:
+ sInvColor = '#%06x' % (~int(sColor[1:],16) & 0xffffff,);
+
+ sReport += ' <tr><td>\n' \
+ ' <table class="tmbargraphl3" height="100%%" border="0" cellspacing="0" cellpadding="0">\n' \
+ ' <tr>\n';
+ if cPct >= 99:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \
+ '%s&nbsp;</td>\n' \
+ % (cxBar, sColor, sInvColor, sValue);
+ elif cPct < 1:
+ sReport += ' <td width="%spx" nowrap style="color:%s;">%s</td>\n' \
+ % (self.cxMaxBar - cxBar, sColor, sValue);
+ elif cPct >= 50:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \
+ '%s&nbsp;</td>\n' \
+ ' <td width="%spx" nowrap><div>&nbsp;</div></td>\n' \
+ % (cxBar, sColor, sInvColor, sValue, self.cxMaxBar - cxBar);
+ else:
+ sReport += ' <td width="%spx" nowrap bgcolor="%s"></td>\n' \
+ ' <td width="%spx" nowrap>&nbsp;%s</td>\n' \
+ % (cxBar, sColor, self.cxMaxBar - cxBar, sValue);
+ sReport += ' </tr>\n' \
+ ' </table>\n' \
+ ' </td></tr>\n'
+ sReport += ' </table>\n' \
+ ' </td>\n' \
+ ' </tr>\n';
+ if i + 1 < len(aoTable) and len(oRow.aoValues) > 1:
+ sReport += ' <tr></tr>\n'
+
+ sReport += '</table>\n';
+
+ sReport += '<div class="tmgraphlegend">\n' \
+ ' <p>Legend:\n';
+ for j, sValue in enumerate(aoTable[0].asValues):
+ sColor = self.kasColors[j % len(self.kasColors)];
+ sReport += ' <font color="%s">&#x25A0; %s</font>\n' % (sColor, escapeElem(sValue),);
+ sReport += ' </p>\n' \
+ '</div>\n';
+
+ sReport += '</div>\n';
+ return sReport;
+
+
+
+
+class WuiHlpLineGraph(WuiHlpGraphBase):
+ """
+ Line graph.
+ """
+
+ def __init__(self, sId, oData, oDisp):
+ WuiHlpGraphBase.__init__(self, sId, oData, oDisp);
+
+
+class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph):
+ """
+ Line graph with an errorbar for the Y axis.
+ """
+
+ pass; # pylint: disable=unnecessary-pass
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py
new file mode 100755
index 00000000..45a847ed
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+# $Id: wuilogviewer.py $
+
+"""
+Test Manager WUI - Log viewer
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.core.testset import TestSetData;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink;
+from testmanager.webui.wuimain import WuiMain;
+
+
+class WuiLogViewer(WuiContentBase):
+ """Log viewer."""
+
+ def __init__(self, oTestSet, oLogFile, cbChunk, iChunk, aoTimestamps, oDisp = None, fnDPrint = None):
+ WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint);
+ self._oTestSet = oTestSet;
+ self._oLogFile = oLogFile;
+ self._cbChunk = cbChunk;
+ self._iChunk = iChunk;
+ self._aoTimestamps = aoTimestamps;
+
+ def _generateNavigation(self, cbFile):
+ """Generate the HTML for the log navigation."""
+
+ dParams = {
+ WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamLogFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamLogChunkSize: self._cbChunk,
+ WuiMain.ksParamLogChunkNo: self._iChunk,
+ };
+
+ #
+ # The page walker.
+ #
+ dParams2 = dict(dParams);
+ del dParams2[WuiMain.ksParamLogChunkNo];
+ sHrefFmt = '<a href="?%s&%s=%%s" title="%%s">%%s</a>' \
+ % (webutils.encodeUrlParams(dParams2).replace('%', '%%'), WuiMain.ksParamLogChunkNo,);
+ sHtmlWalker = self.genericPageWalker(self._iChunk, (cbFile + self._cbChunk - 1) // self._cbChunk,
+ sHrefFmt, 11, 0, 'chunk');
+
+ #
+ # The chunk size selector.
+ #
+
+ dParams2 = dict(dParams);
+ del dParams2[WuiMain.ksParamLogChunkSize];
+ sHtmlSize = '<form name="ChunkSizeForm" method="GET">\n' \
+ ' Max <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \
+ % ( WuiMain.ksParamLogChunkSize, webutils.encodeUrlParams(dParams2), WuiMain.ksParamLogChunkSize,);
+
+ for cbChunk in [ 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152,
+ 4194304, 8388608, 16777216 ]:
+ sHtmlSize += ' <option value="%d" %s>%d bytes</option>\n' \
+ % (cbChunk, 'selected="selected"' if cbChunk == self._cbChunk else '', cbChunk);
+ sHtmlSize += ' </select> per page\n' \
+ '</form>\n'
+
+ #
+ # Download links.
+ #
+ oRawLink = WuiTmLink('View Raw', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamGetFileDownloadIt: False,
+ },
+ sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) );
+ oDownloadLink = WuiTmLink('Download Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet,
+ WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile,
+ WuiMain.ksParamGetFileDownloadIt: True,
+ },
+ sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) );
+ oTestSetLink = WuiTmLink('Test Set', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: self._oTestSet.idTestSet,
+ });
+
+
+ #
+ # Combine the elements and return.
+ #
+ return '<div class="tmlogviewernavi">\n' \
+ ' <table width=100%>\n' \
+ ' <tr>\n' \
+ ' <td width=20%>\n' \
+ ' ' + oTestSetLink.toHtml() + '\n' \
+ ' ' + oRawLink.toHtml() + '\n' \
+ ' ' + oDownloadLink.toHtml() + '\n' \
+ ' </td>\n' \
+ ' <td width=60% align=center>' + sHtmlWalker + '</td>' \
+ ' <td width=20% align=right>' + sHtmlSize + '</td>\n' \
+ ' </tr>\n' \
+ ' </table>\n' \
+ '</div>\n';
+
+ def _displayLog(self, oFile, offFile, cbFile, aoTimestamps):
+ """Displays the current section of the log file."""
+ from testmanager.core import db;
+
+ def prepCurTs():
+ """ Formats the current timestamp. """
+ if iCurTs < len(aoTimestamps):
+ oTsZulu = db.dbTimestampToZuluDatetime(aoTimestamps[iCurTs]);
+ return (oTsZulu.strftime('%H:%M:%S.%f'), oTsZulu.strftime('%H_%M_%S_%f'));
+ return ('~~|~~|~~|~~~~~~', '~~|~~|~~|~~~~~~'); # ASCII chars with high values. Limit hits.
+
+ def isCurLineAtOrAfterCurTs():
+ """ Checks if the current line starts with a timestamp that is after the current one. """
+ if len(sLine) >= 15 \
+ and sLine[2] == ':' \
+ and sLine[5] == ':' \
+ and sLine[8] == '.' \
+ and sLine[14] in '0123456789':
+ if sLine[:15] >= sCurTs and iCurTs < len(aoTimestamps):
+ return True;
+ return False;
+
+ # Figure the end offset.
+ offEnd = offFile + self._cbChunk;
+ offEnd = min(offEnd, cbFile);
+
+ #
+ # Here is an annoying thing, we cannot seek in zip file members. So,
+ # since we have to read from the start, we can just as well count line
+ # numbers while we're at it.
+ #
+ iCurTs = 0;
+ (sCurTs, sCurId) = prepCurTs();
+ offCur = 0;
+ iLine = 0;
+ while True:
+ sLine = oFile.readline().decode('utf-8', 'replace');
+ offLine = offCur;
+ iLine += 1;
+ offCur += len(sLine);
+ if offCur >= offFile or not sLine:
+ break;
+ while isCurLineAtOrAfterCurTs():
+ iCurTs += 1;
+ (sCurTs, sCurId) = prepCurTs();
+
+ #
+ # Got to where we wanted, format the chunk.
+ #
+ asLines = ['\n<div class="tmlog">\n<pre>\n', ];
+ while True:
+ # The timestamp IDs.
+ sPrevTs = '';
+ while isCurLineAtOrAfterCurTs():
+ if sPrevTs != sCurTs:
+ asLines.append('<a id="%s"></a>' % (sCurId,));
+ iCurTs += 1;
+ (sCurTs, sCurId) = prepCurTs();
+
+ # The line.
+ asLines.append('<a id="L%d" href="#L%d">%05d</a><a id="O%d"></a>%s\n' \
+ % (iLine, iLine, iLine, offLine, webutils.escapeElem(sLine.rstrip())));
+
+ # next
+ if offCur >= offEnd:
+ break;
+ sLine = oFile.readline().decode('utf-8', 'replace');
+ offLine = offCur;
+ iLine += 1;
+ offCur += len(sLine);
+ if not sLine:
+ break;
+ asLines.append('<pre/></div>\n');
+ return ''.join(asLines);
+
+
+ def show(self):
+ """Shows the log."""
+
+ if self._oLogFile.sDescription not in [ '', None ]:
+ sTitle = '%s - %s' % (self._oLogFile.sFile, self._oLogFile.sDescription);
+ else:
+ sTitle = '%s' % (self._oLogFile.sFile,);
+
+ #
+ # Open the log file. No universal line endings here.
+ #
+ (oFile, oSizeOrError, _) = self._oTestSet.openFile(self._oLogFile.sFile, 'rb');
+ if oFile is None:
+ return (sTitle, '<p>%s</p>\n' % (webutils.escapeElem(oSizeOrError),),);
+ cbFile = oSizeOrError;
+
+ #
+ # Generate the page.
+ #
+
+ # Start with a focus hack.
+ sHtml = '<div id="tmlogoutdiv" tabindex="0">\n' \
+ '<script lang="text/javascript">\n' \
+ 'document.getElementById(\'tmlogoutdiv\').focus();\n' \
+ '</script>\n';
+
+ sNaviHtml = self._generateNavigation(cbFile);
+ sHtml += sNaviHtml;
+
+ offFile = self._iChunk * self._cbChunk;
+ if offFile < cbFile:
+ sHtml += self._displayLog(oFile, offFile, cbFile, self._aoTimestamps);
+ sHtml += sNaviHtml;
+ else:
+ sHtml += '<p>End Of File</p>';
+
+ return (sTitle, sHtml);
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuimain.py b/src/VBox/ValidationKit/testmanager/webui/wuimain.py
new file mode 100755
index 00000000..3f059775
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuimain.py
@@ -0,0 +1,1344 @@
+# -*- coding: utf-8 -*-
+# $Id: wuimain.py $
+
+"""
+Test Manager Core - WUI - The Main page.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Standard Python imports.
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core.base import TMExceptionBase, TMTooManyRows;
+from testmanager.webui.wuibase import WuiDispatcherBase, WuiException;
+from testmanager.webui.wuicontentbase import WuiTmLink;
+from common import webutils, utils;
+
+
+
+class WuiMain(WuiDispatcherBase):
+ """
+ WUI Main page.
+
+ Note! All cylic dependency avoiance stuff goes here in the dispatcher code,
+ not in the action specific code. This keeps the uglyness in one place
+ and reduces load time dependencies in the more critical code path.
+ """
+
+ ## The name of the script.
+ ksScriptName = 'index.py'
+
+ ## @name Actions
+ ## @{
+ ksActionResultsUnGrouped = 'ResultsUnGrouped'
+ ksActionResultsGroupedBySchedGroup = 'ResultsGroupedBySchedGroup'
+ ksActionResultsGroupedByTestGroup = 'ResultsGroupedByTestGroup'
+ ksActionResultsGroupedByBuildRev = 'ResultsGroupedByBuildRev'
+ ksActionResultsGroupedByBuildCat = 'ResultsGroupedByBuildCat'
+ ksActionResultsGroupedByTestBox = 'ResultsGroupedByTestBox'
+ ksActionResultsGroupedByTestCase = 'ResultsGroupedByTestCase'
+ ksActionResultsGroupedByOS = 'ResultsGroupedByOS'
+ ksActionResultsGroupedByArch = 'ResultsGroupedByArch'
+ ksActionTestSetDetails = 'TestSetDetails';
+ ksActionTestResultDetails = ksActionTestSetDetails;
+ ksActionTestSetDetailsFromResult = 'TestSetDetailsFromResult'
+ ksActionTestResultFailureDetails = 'TestResultFailureDetails'
+ ksActionTestResultFailureAdd = 'TestResultFailureAdd'
+ ksActionTestResultFailureAddPost = 'TestResultFailureAddPost'
+ ksActionTestResultFailureEdit = 'TestResultFailureEdit'
+ ksActionTestResultFailureEditPost = 'TestResultFailureEditPost'
+ ksActionTestResultFailureDoRemove = 'TestResultFailureDoRemove'
+ ksActionViewLog = 'ViewLog'
+ ksActionGetFile = 'GetFile'
+ ksActionReportSummary = 'ReportSummary';
+ ksActionReportRate = 'ReportRate';
+ ksActionReportTestCaseFailures = 'ReportTestCaseFailures';
+ ksActionReportTestBoxFailures = 'ReportTestBoxFailures';
+ ksActionReportFailureReasons = 'ReportFailureReasons';
+ ksActionGraphWiz = 'GraphWiz';
+ ksActionVcsHistoryTooltip = 'VcsHistoryTooltip'; ##< Hardcoded in common.js.
+ ## @}
+
+ ## @name Standard report parameters
+ ## @{
+ ksParamReportPeriods = 'cPeriods';
+ ksParamReportPeriodInHours = 'cHoursPerPeriod';
+ ksParamReportSubject = 'sSubject';
+ ksParamReportSubjectIds = 'SubjectIds';
+ ## @}
+
+ ## @name Graph Wizard parameters
+ ## Common parameters: ksParamReportPeriods, ksParamReportPeriodInHours, ksParamReportSubjectIds,
+ ## ksParamReportSubject, ksParamEffectivePeriod, and ksParamEffectiveDate.
+ ## @{
+ ksParamGraphWizTestBoxIds = 'aidTestBoxes';
+ ksParamGraphWizBuildCatIds = 'aidBuildCats';
+ ksParamGraphWizTestCaseIds = 'aidTestCases';
+ ksParamGraphWizSepTestVars = 'fSepTestVars';
+ ksParamGraphWizImpl = 'enmImpl';
+ ksParamGraphWizWidth = 'cx';
+ ksParamGraphWizHeight = 'cy';
+ ksParamGraphWizDpi = 'dpi';
+ ksParamGraphWizFontSize = 'cPtFont';
+ ksParamGraphWizErrorBarY = 'fErrorBarY';
+ ksParamGraphWizMaxErrorBarY = 'cMaxErrorBarY';
+ ksParamGraphWizMaxPerGraph = 'cMaxPerGraph';
+ ksParamGraphWizXkcdStyle = 'fXkcdStyle';
+ ksParamGraphWizTabular = 'fTabular';
+ ksParamGraphWizSrcTestSetId = 'idSrcTestSet';
+ ## @}
+
+ ## @name Graph implementations values for ksParamGraphWizImpl.
+ ## @{
+ ksGraphWizImpl_Default = 'default';
+ ksGraphWizImpl_Matplotlib = 'matplotlib';
+ ksGraphWizImpl_Charts = 'charts';
+ kasGraphWizImplValid = [ ksGraphWizImpl_Default, ksGraphWizImpl_Matplotlib, ksGraphWizImpl_Charts];
+ kaasGraphWizImplCombo = [
+ ( ksGraphWizImpl_Default, 'Default' ),
+ ( ksGraphWizImpl_Matplotlib, 'Matplotlib (server)' ),
+ ( ksGraphWizImpl_Charts, 'Google Charts (client)'),
+ ];
+ ## @}
+
+ ## @name Log Viewer parameters.
+ ## @{
+ ksParamLogSetId = 'LogViewer_idTestSet';
+ ksParamLogFileId = 'LogViewer_idFile';
+ ksParamLogChunkSize = 'LogViewer_cbChunk';
+ ksParamLogChunkNo = 'LogViewer_iChunk';
+ ## @}
+
+ ## @name File getter parameters.
+ ## @{
+ ksParamGetFileSetId = 'GetFile_idTestSet';
+ ksParamGetFileId = 'GetFile_idFile';
+ ksParamGetFileDownloadIt = 'GetFile_fDownloadIt';
+ ## @}
+
+ ## @name VCS history parameters.
+ ## @{
+ ksParamVcsHistoryRepository = 'repo';
+ ksParamVcsHistoryRevision = 'rev';
+ ksParamVcsHistoryEntries = 'cEntries';
+ ## @}
+
+ ## @name Test result listing parameters.
+ ## @{
+ ## If this param is specified, then show only results for this member when results grouped by some parameter.
+ ksParamGroupMemberId = 'GroupMemberId'
+ ## Optional parameter for indicating whether to restrict the listing to failures only.
+ ksParamOnlyFailures = 'OnlyFailures';
+ ## The sheriff parameter for getting failures needing a reason or two assigned to them.
+ ksParamOnlyNeedingReason = 'OnlyNeedingReason';
+ ## Result listing sorting.
+ ksParamTestResultsSortBy = 'enmSortBy'
+ ## @}
+
+ ## Effective time period. one of the first column values in kaoResultPeriods.
+ ksParamEffectivePeriod = 'sEffectivePeriod'
+
+ ## Test result period values.
+ kaoResultPeriods = [
+ ( '1 hour', '1 hour', 1 ),
+ ( '2 hours', '2 hours', 2 ),
+ ( '3 hours', '3 hours', 3 ),
+ ( '6 hours', '6 hours', 6 ),
+ ( '12 hours', '12 hours', 12 ),
+
+ ( '1 day', '1 day', 24 ),
+ ( '2 days', '2 days', 48 ),
+ ( '3 days', '3 days', 72 ),
+
+ ( '1 week', '1 week', 168 ),
+ ( '2 weeks', '2 weeks', 336 ),
+ ( '3 weeks', '3 weeks', 504 ),
+
+ ( '1 month', '1 month', 31 * 24 ), # The approx hour count varies with the start date.
+ ( '2 months', '2 months', (31 + 31) * 24 ), # Using maximum values.
+ ( '3 months', '3 months', (31 + 30 + 31) * 24 ),
+
+ ( '6 months', '6 months', (31 + 31 + 30 + 31 + 30 + 31) * 24 ),
+
+ ( '1 year', '1 year', 365 * 24 ),
+ ];
+ ## The default test result period.
+ ksResultPeriodDefault = '6 hours';
+
+
+
+ def __init__(self, oSrvGlue):
+ WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName);
+ self._sTemplate = 'template.html'
+
+ #
+ # Populate the action dispatcher dictionary.
+ # Lambda is forbidden because of readability, speed and reducing number of imports.
+ #
+ self._dDispatch[self.ksActionResultsUnGrouped] = self._actionResultsUnGrouped;
+ self._dDispatch[self.ksActionResultsGroupedByTestGroup] = self._actionResultsGroupedByTestGroup;
+ self._dDispatch[self.ksActionResultsGroupedByBuildRev] = self._actionResultsGroupedByBuildRev;
+ self._dDispatch[self.ksActionResultsGroupedByBuildCat] = self._actionResultsGroupedByBuildCat;
+ self._dDispatch[self.ksActionResultsGroupedByTestBox] = self._actionResultsGroupedByTestBox;
+ self._dDispatch[self.ksActionResultsGroupedByTestCase] = self._actionResultsGroupedByTestCase;
+ self._dDispatch[self.ksActionResultsGroupedByOS] = self._actionResultsGroupedByOS;
+ self._dDispatch[self.ksActionResultsGroupedByArch] = self._actionResultsGroupedByArch;
+ self._dDispatch[self.ksActionResultsGroupedBySchedGroup] = self._actionResultsGroupedBySchedGroup;
+
+ self._dDispatch[self.ksActionTestSetDetails] = self._actionTestSetDetails;
+ self._dDispatch[self.ksActionTestSetDetailsFromResult] = self._actionTestSetDetailsFromResult;
+
+ self._dDispatch[self.ksActionTestResultFailureAdd] = self._actionTestResultFailureAdd;
+ self._dDispatch[self.ksActionTestResultFailureAddPost] = self._actionTestResultFailureAddPost;
+ self._dDispatch[self.ksActionTestResultFailureDetails] = self._actionTestResultFailureDetails;
+ self._dDispatch[self.ksActionTestResultFailureDoRemove] = self._actionTestResultFailureDoRemove;
+ self._dDispatch[self.ksActionTestResultFailureEdit] = self._actionTestResultFailureEdit;
+ self._dDispatch[self.ksActionTestResultFailureEditPost] = self._actionTestResultFailureEditPost;
+
+ self._dDispatch[self.ksActionViewLog] = self._actionViewLog;
+ self._dDispatch[self.ksActionGetFile] = self._actionGetFile;
+
+ self._dDispatch[self.ksActionReportSummary] = self._actionReportSummary;
+ self._dDispatch[self.ksActionReportRate] = self._actionReportRate;
+ self._dDispatch[self.ksActionReportTestCaseFailures] = self._actionReportTestCaseFailures;
+ self._dDispatch[self.ksActionReportFailureReasons] = self._actionReportFailureReasons;
+ self._dDispatch[self.ksActionGraphWiz] = self._actionGraphWiz;
+
+ self._dDispatch[self.ksActionVcsHistoryTooltip] = self._actionVcsHistoryTooltip;
+
+ # Legacy.
+ self._dDispatch['TestResultDetails'] = self._dDispatch[self.ksActionTestSetDetails];
+
+
+ #
+ # Popupate the menus.
+ #
+
+ # Additional URL parameters keeping for time navigation.
+ sExtraTimeNav = ''
+ dCurParams = oSrvGlue.getParameters()
+ if dCurParams is not None:
+ for sExtraParam in [ self.ksParamItemsPerPage, self.ksParamEffectiveDate, self.ksParamEffectivePeriod, ]:
+ if sExtraParam in dCurParams:
+ sExtraTimeNav += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
+
+ # Additional URL parameters for reports
+ sExtraReports = '';
+ if dCurParams is not None:
+ for sExtraParam in [ self.ksParamReportPeriods, self.ksParamReportPeriodInHours, self.ksParamEffectiveDate, ]:
+ if sExtraParam in dCurParams:
+ sExtraReports += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),)
+
+ # Shorthand to keep within margins.
+ sActUrlBase = self._sActionUrlBase;
+ sOnlyFailures = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyFailures: True}), sExtraTimeNav, );
+ sSheriff = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyNeedingReason: True}), sExtraTimeNav, );
+
+ self._aaoMenus = \
+ [
+ [
+ 'Sheriff', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sSheriff, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sSheriff, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sSheriff, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sSheriff, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sSheriff, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sSheriff, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sSheriff, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sSheriff, False ],
+ ]
+ ],
+ [
+ 'Reports', sActUrlBase + self.ksActionReportSummary,
+ [
+ [ 'Summary', sActUrlBase + self.ksActionReportSummary + sExtraReports, False ],
+ [ 'Success rate', sActUrlBase + self.ksActionReportRate + sExtraReports, False ],
+ [ 'Test case failures', sActUrlBase + self.ksActionReportTestCaseFailures + sExtraReports, False ],
+ [ 'Testbox failures', sActUrlBase + self.ksActionReportTestBoxFailures + sExtraReports, False ],
+ [ 'Failure reasons', sActUrlBase + self.ksActionReportFailureReasons + sExtraReports, False ],
+ ]
+ ],
+ [
+ 'Test Results', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sExtraTimeNav, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sExtraTimeNav, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sExtraTimeNav, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sExtraTimeNav, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sExtraTimeNav, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sExtraTimeNav, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sExtraTimeNav, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sExtraTimeNav, False ],
+ ]
+ ],
+ [
+ 'Test Failures', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures,
+ [
+ [ 'Grouped by', None ],
+ [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures, False ],
+ [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sOnlyFailures, False ],
+ [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sOnlyFailures, False ],
+ [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sOnlyFailures, False ],
+ [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sOnlyFailures, False ],
+ [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sOnlyFailures, False ],
+ [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sOnlyFailures, False ],
+ [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sOnlyFailures, False ],
+ [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sOnlyFailures, False ],
+ ]
+ ],
+ [
+ '> Admin', 'admin.py?' + webutils.encodeUrlParams(self._dDbgParams), []
+ ],
+ ];
+
+
+ #
+ # Overriding parent methods.
+ #
+
+ def _generatePage(self):
+ """Override parent handler in order to change page title."""
+ if self._sPageTitle is not None:
+ self._sPageTitle = 'Test Results - ' + self._sPageTitle
+
+ return WuiDispatcherBase._generatePage(self)
+
+ def _actionDefault(self):
+ """Show the default admin page."""
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ self._sAction = self.ksActionResultsUnGrouped
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _isMenuMatch(self, sMenuUrl, sActionParam):
+ if super(WuiMain, self)._isMenuMatch(sMenuUrl, sActionParam):
+ fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+ if fOnlyNeedingReason:
+ return (sMenuUrl.find(self.ksParamOnlyNeedingReason) > 0);
+ fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+ return (sMenuUrl.find(self.ksParamOnlyFailures) > 0) == fOnlyFailures \
+ and sMenuUrl.find(self.ksParamOnlyNeedingReason) < 0;
+ return False;
+
+
+ #
+ # Navigation bar stuff
+ #
+
+ def _generateSortBySelector(self, dParams, sPreamble, sPostamble):
+ """
+ Generate HTML code for the sort by selector.
+ """
+ from testmanager.core.testresults import TestResultLogic;
+
+ if self.ksParamTestResultsSortBy in dParams:
+ enmResultSortBy = dParams[self.ksParamTestResultsSortBy];
+ del dParams[self.ksParamTestResultsSortBy];
+ else:
+ enmResultSortBy = TestResultLogic.ksResultsSortByRunningAndStart;
+
+ sHtmlSortBy = '<form name="TimeForm" method="GET"> Sort by\n';
+ sHtmlSortBy += sPreamble;
+ sHtmlSortBy += '\n <select name="%s" onchange="window.location=' % (self.ksParamTestResultsSortBy,);
+ sHtmlSortBy += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamTestResultsSortBy)
+ sHtmlSortBy += 'this.options[this.selectedIndex].value;" title="Sorting by">\n'
+
+ fSelected = False;
+ for enmCode, sTitle in TestResultLogic.kaasResultsSortByTitles:
+ if enmCode == enmResultSortBy:
+ fSelected = True;
+ sHtmlSortBy += ' <option value="%s"%s>%s</option>\n' \
+ % (enmCode, ' selected="selected"' if enmCode == enmResultSortBy else '', sTitle,);
+ assert fSelected;
+ sHtmlSortBy += ' </select>\n';
+ sHtmlSortBy += sPostamble;
+ sHtmlSortBy += '\n</form>\n'
+ return sHtmlSortBy;
+
+ def _generateStatusSelector(self, dParams, fOnlyFailures):
+ """
+ Generate HTML code for the status code selector. Currently very simple.
+ """
+ dParams[self.ksParamOnlyFailures] = not fOnlyFailures;
+ return WuiTmLink('Show all results' if fOnlyFailures else 'Only show failed tests', '', dParams,
+ fBracketed = False).toHtml();
+
+ def _generateTimeWalker(self, dParams, tsEffective, sCurPeriod):
+ """
+ Generates HTML code for walking back and forth in time.
+ """
+ # Have to do some math here. :-/
+ if tsEffective is None:
+ self._oDb.execute('SELECT CURRENT_TIMESTAMP - \'' + sCurPeriod + '\'::interval');
+ tsNext = None;
+ tsPrev = self._oDb.fetchOne()[0];
+ else:
+ self._oDb.execute('SELECT %s::TIMESTAMP - \'' + sCurPeriod + '\'::interval,\n'
+ ' %s::TIMESTAMP + \'' + sCurPeriod + '\'::interval',
+ (tsEffective, tsEffective,));
+ tsPrev, tsNext = self._oDb.fetchOne();
+
+ # Forget about page No when changing a period
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ # Format.
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsPrev);
+ sPrev = '<a href="?%s" title="One period earlier">&lt;&lt;</a>&nbsp;&nbsp;' \
+ % (webutils.encodeUrlParams(dParams),);
+
+ if tsNext is not None:
+ dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsNext);
+ sNext = '&nbsp;&nbsp;<a href="?%s" title="One period later">&gt;&gt;</a>' \
+ % (webutils.encodeUrlParams(dParams),);
+ else:
+ sNext = '&nbsp;&nbsp;&gt;&gt;';
+
+ from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place.
+ return WuiListContentBase.generateTimeNavigation('top', self.getParameters(), self.getEffectiveDateParam(),
+ sPrev, sNext, False);
+
+ def _generateResultPeriodSelector(self, dParams, sCurPeriod):
+ """
+ Generate HTML code for result period selector.
+ """
+
+ if self.ksParamEffectivePeriod in dParams:
+ del dParams[self.ksParamEffectivePeriod];
+
+ # Forget about page No when changing a period
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ sHtmlPeriodSelector = '<form name="PeriodForm" method="GET">\n'
+ sHtmlPeriodSelector += ' Period is\n'
+ sHtmlPeriodSelector += ' <select name="%s" onchange="window.location=' % self.ksParamEffectivePeriod
+ sHtmlPeriodSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamEffectivePeriod)
+ sHtmlPeriodSelector += 'this.options[this.selectedIndex].value;">\n'
+
+ for sPeriodValue, sPeriodCaption, _ in self.kaoResultPeriods:
+ sHtmlPeriodSelector += ' <option value="%s"%s>%s</option>\n' \
+ % (webutils.quoteUrl(sPeriodValue),
+ ' selected="selected"' if sPeriodValue == sCurPeriod else '',
+ sPeriodCaption)
+
+ sHtmlPeriodSelector += ' </select>\n' \
+ '</form>\n'
+
+ return sHtmlPeriodSelector
+
+ def _generateGroupContentSelector(self, aoGroupMembers, iCurrentMember, sAltAction):
+ """
+ Generate HTML code for group content selector.
+ """
+
+ dParams = self.getParameters()
+
+ if self.ksParamGroupMemberId in dParams:
+ del dParams[self.ksParamGroupMemberId]
+
+ if sAltAction is not None:
+ if self.ksParamAction in dParams:
+ del dParams[self.ksParamAction];
+ dParams[self.ksParamAction] = sAltAction;
+
+ sHtmlSelector = '<form name="GroupContentForm" method="GET">\n'
+ sHtmlSelector += ' <select name="%s" onchange="window.location=' % self.ksParamGroupMemberId
+ sHtmlSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamGroupMemberId)
+ sHtmlSelector += 'this.options[this.selectedIndex].value;">\n'
+
+ sHtmlSelector += '<option value="-1">All</option>\n'
+
+ for iGroupMemberId, sGroupMemberName in aoGroupMembers:
+ if iGroupMemberId is not None:
+ sHtmlSelector += ' <option value="%s"%s>%s</option>\n' \
+ % (iGroupMemberId,
+ ' selected="selected"' if iGroupMemberId == iCurrentMember else '',
+ sGroupMemberName)
+
+ sHtmlSelector += ' </select>\n' \
+ '</form>\n'
+
+ return sHtmlSelector
+
+ def _generatePagesSelector(self, dParams, cItems, cItemsPerPage, iPage):
+ """
+ Generate HTML code for pages (1, 2, 3 ... N) selector
+ """
+
+ if WuiDispatcherBase.ksParamPageNo in dParams:
+ del dParams[WuiDispatcherBase.ksParamPageNo]
+
+ sHrefPtr = '<a href="?%s&%s=' % (webutils.encodeUrlParams(dParams).replace('%', '%%'),
+ WuiDispatcherBase.ksParamPageNo)
+ sHrefPtr += '%d">%s</a>'
+
+ cNumOfPages = (cItems + cItemsPerPage - 1) // cItemsPerPage;
+ cPagesToDisplay = 10
+ cPagesRangeStart = iPage - cPagesToDisplay // 2 \
+ if not iPage - cPagesToDisplay / 2 < 0 else 0
+ cPagesRangeEnd = cPagesRangeStart + cPagesToDisplay \
+ if not cPagesRangeStart + cPagesToDisplay > cNumOfPages else cNumOfPages
+ # Adjust pages range
+ if cNumOfPages < cPagesToDisplay:
+ cPagesRangeStart = 0
+ cPagesRangeEnd = cNumOfPages
+
+ # 1 2 3 4...
+ sHtmlPager = '&nbsp;\n'.join(sHrefPtr % (x, str(x + 1)) if x != iPage else str(x + 1)
+ for x in range(cPagesRangeStart, cPagesRangeEnd))
+ if cPagesRangeStart > 0:
+ sHtmlPager = '%s&nbsp; ... &nbsp;\n' % (sHrefPtr % (0, str(1))) + sHtmlPager
+ if cPagesRangeEnd < cNumOfPages:
+ sHtmlPager += ' ... %s\n' % (sHrefPtr % (cNumOfPages, str(cNumOfPages + 1)))
+
+ # Prev/Next (using << >> because &laquo; and &raquo are too tiny).
+ if iPage > 0:
+ dParams[WuiDispatcherBase.ksParamPageNo] = iPage - 1
+ sHtmlPager = ('<a title="Previous page" href="?%s">&lt;&lt;</a>&nbsp;&nbsp;\n'
+ % (webutils.encodeUrlParams(dParams), )) \
+ + sHtmlPager;
+ else:
+ sHtmlPager = '&lt;&lt;&nbsp;&nbsp;\n' + sHtmlPager
+
+ if iPage + 1 < cNumOfPages:
+ dParams[WuiDispatcherBase.ksParamPageNo] = iPage + 1
+ sHtmlPager += '\n&nbsp; <a title="Next page" href="?%s">&gt;&gt;</a>\n' % (webutils.encodeUrlParams(dParams),)
+ else:
+ sHtmlPager += '\n&nbsp; &gt;&gt;\n'
+
+ return sHtmlPager
+
+ def _generateItemPerPageSelector(self, dParams, cItemsPerPage):
+ """
+ Generate HTML code for items per page selector
+ Note! Modifies dParams!
+ """
+
+ from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place.
+ return WuiListContentBase.generateItemPerPageSelector('top', dParams, cItemsPerPage);
+
+ def _generateResultNavigation(self, cItems, cItemsPerPage, iPage, tsEffective, sCurPeriod, fOnlyFailures,
+ sHtmlMemberSelector):
+ """ Make custom time navigation bar for the results. """
+
+ # Generate the elements.
+ sHtmlStatusSelector = self._generateStatusSelector(self.getParameters(), fOnlyFailures);
+ sHtmlSortBySelector = self._generateSortBySelector(self.getParameters(), '', sHtmlStatusSelector);
+ sHtmlPeriodSelector = self._generateResultPeriodSelector(self.getParameters(), sCurPeriod)
+ sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, sCurPeriod);
+
+ if cItems > 0:
+ sHtmlPager = self._generatePagesSelector(self.getParameters(), cItems, cItemsPerPage, iPage)
+ sHtmlItemsPerPageSelector = self._generateItemPerPageSelector(self.getParameters(), cItemsPerPage)
+ else:
+ sHtmlPager = ''
+ sHtmlItemsPerPageSelector = ''
+
+ # Generate navigation bar
+ sHtml = '<table width=100%>\n' \
+ '<tr>\n' \
+ ' <td width=30%>' + sHtmlMemberSelector + '</td>\n' \
+ ' <td width=40% align=center>' + sHtmlTimeWalker + '</td>' \
+ ' <td width=30% align=right>\n' + sHtmlPeriodSelector + '</td>\n' \
+ '</tr>\n' \
+ '<tr>\n' \
+ ' <td width=30%>' + sHtmlSortBySelector + '</td>\n' \
+ ' <td width=40% align=center>\n' + sHtmlPager + '</td>\n' \
+ ' <td width=30% align=right>\n' + sHtmlItemsPerPageSelector + '</td>\n'\
+ '</tr>\n' \
+ '</table>\n'
+
+ return sHtml
+
+ def _generateReportNavigation(self, tsEffective, cHoursPerPeriod, cPeriods):
+ """ Make time navigation bar for the reports. """
+
+ # The period length selector.
+ dParams = self.getParameters();
+ if WuiMain.ksParamReportPeriodInHours in dParams:
+ del dParams[WuiMain.ksParamReportPeriodInHours];
+ sHtmlPeriodLength = '';
+ sHtmlPeriodLength += '<form name="ReportPeriodInHoursForm" method="GET">\n' \
+ ' Period length <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Statistics period length in hours.">\n' \
+ % (WuiMain.ksParamReportPeriodInHours,
+ webutils.encodeUrlParams(dParams),
+ WuiMain.ksParamReportPeriodInHours)
+ for cHours in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 18, 24, 48, 72, 96, 120, 144, 168 ]:
+ sHtmlPeriodLength += ' <option value="%d"%s>%d hour%s</option>\n' \
+ % (cHours, 'selected="selected"' if cHours == cHoursPerPeriod else '', cHours,
+ 's' if cHours > 1 else '');
+ sHtmlPeriodLength += ' </select>\n' \
+ '</form>\n'
+
+ # The period count selector.
+ dParams = self.getParameters();
+ if WuiMain.ksParamReportPeriods in dParams:
+ del dParams[WuiMain.ksParamReportPeriods];
+ sHtmlCountOfPeriods = '';
+ sHtmlCountOfPeriods += '<form name="ReportPeriodsForm" method="GET">\n' \
+ ' Periods <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
+ 'this.options[this.selectedIndex].value;" title="Statistics periods to report.">\n' \
+ % (WuiMain.ksParamReportPeriods,
+ webutils.encodeUrlParams(dParams),
+ WuiMain.ksParamReportPeriods)
+ for cCurPeriods in range(2, 43):
+ sHtmlCountOfPeriods += ' <option value="%d"%s>%d</option>\n' \
+ % (cCurPeriods, 'selected="selected"' if cCurPeriods == cPeriods else '', cCurPeriods);
+ sHtmlCountOfPeriods += ' </select>\n' \
+ '</form>\n'
+
+ # The time walker.
+ sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, '%d hours' % (cHoursPerPeriod));
+
+ # Combine them all.
+ sHtml = '<table width=100%>\n' \
+ ' <tr>\n' \
+ ' <td width=30% align="center">\n' + sHtmlPeriodLength + '</td>\n' \
+ ' <td width=40% align="center">\n' + sHtmlTimeWalker + '</td>' \
+ ' <td width=30% align="center">\n' + sHtmlCountOfPeriods + '</td>\n' \
+ ' </tr>\n' \
+ '</table>\n';
+ return sHtml;
+
+
+ #
+ # The rest of stuff
+ #
+
+ def _actionGroupedResultsListing( #pylint: disable=too-many-locals
+ self,
+ enmResultsGroupingType,
+ oResultsLogicType,
+ oResultFilterType,
+ oResultsListContentType):
+ """
+ Override generic listing action.
+
+ oResultsLogicType implements getEntriesCount, fetchResultsForListing and more.
+ oResultFilterType is a child of ModelFilterBase.
+ oResultsListContentType is a child of WuiListContentBase.
+ """
+ from testmanager.core.testresults import TestResultLogic;
+
+ cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 128);
+ iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0);
+ tsEffective = self.getEffectiveDateParam();
+ iGroupMemberId = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1);
+ fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False);
+ fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False);
+ enmResultSortBy = self.getStringParam(self.ksParamTestResultsSortBy,
+ asValidValues = TestResultLogic.kasResultsSortBy,
+ sDefault = TestResultLogic.ksResultsSortByRunningAndStart);
+ oFilter = oResultFilterType().initFromParams(self);
+
+ # Get testing results period and validate it
+ asValidValues = [x for (x, _, _) in self.kaoResultPeriods]
+ sCurPeriod = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues,
+ sDefault = self.ksResultPeriodDefault)
+ assert sCurPeriod != ''; # Impossible!
+
+ self._checkForUnknownParameters()
+
+ #
+ # Fetch the group members.
+ #
+ # If no grouping is selected, we'll fill the grouping combo with
+ # testboxes just to avoid having completely useless combo box.
+ #
+ oTrLogic = TestResultLogic(self._oDb);
+ sAltSelectorAction = None;
+ if enmResultsGroupingType in (TestResultLogic.ksResultsGroupingTypeNone, TestResultLogic.ksResultsGroupingTypeTestBox,):
+ aoTmp = oTrLogic.getTestBoxes(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({(x.idTestBox, '%s (%s)' % (x.sName, str(x.ip))) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+
+ if enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestBox:
+ self._sPageTitle = 'Grouped by Test Box';
+ else:
+ self._sPageTitle = 'Ungrouped results';
+ sAltSelectorAction = self.ksActionResultsGroupedByTestBox;
+ aoGroupMembers.insert(0, [None, None]); # The "All" member.
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestGroup:
+ aoTmp = oTrLogic.getTestGroups(tsNow = tsEffective, sPeriod = sCurPeriod);
+ aoGroupMembers = sorted(list({ (x.idTestGroup, x.sName ) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Test Group'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildRev:
+ aoTmp = oTrLogic.getBuilds(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.iRevision, '%s.%d' % (x.oCat.sBranch, x.iRevision)) for x in aoTmp }),
+ reverse = True, key = lambda asData: asData[0])
+ self._sPageTitle = 'Grouped by Build'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildCat:
+ aoTmp = oTrLogic.getBuildCategories(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idBuildCategory,
+ '%s / %s / %s / %s' % ( x.sProduct, x.sBranch, ', '.join(x.asOsArches), x.sType) )
+ for x in aoTmp }),
+ reverse = True, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by Build Category'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestCase:
+ aoTmp = oTrLogic.getTestCases(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idTestCase, '%s' % x.sName) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Test Case'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeOS:
+ aoTmp = oTrLogic.getOSes(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by OS'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeArch:
+ aoTmp = oTrLogic.getArchitectures(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]);
+ self._sPageTitle = 'Grouped by Architecture'
+
+ elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeSchedGroup:
+ aoTmp = oTrLogic.getSchedGroups(tsNow = tsEffective, sPeriod = sCurPeriod)
+ aoGroupMembers = sorted(list({ (x.idSchedGroup, '%s' % x.sName) for x in aoTmp }),
+ reverse = False, key = lambda asData: asData[1])
+ self._sPageTitle = 'Grouped by Scheduling Group'
+
+ else:
+ raise TMExceptionBase('Unknown grouping type')
+
+ _sPageBody = ''
+ oContent = None
+ cEntriesMax = 0
+ _dParams = self.getParameters()
+ oResultLogic = oResultsLogicType(self._oDb);
+ for idMember, sMemberName in aoGroupMembers:
+ #
+ # Count and fetch entries to be displayed.
+ #
+
+ # Skip group members that were not specified.
+ if idMember != iGroupMemberId \
+ and ( (idMember is not None and enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeNone)
+ or (iGroupMemberId > 0 and enmResultsGroupingType != TestResultLogic.ksResultsGroupingTypeNone) ):
+ continue
+
+ cEntries = oResultLogic.getEntriesCount(tsNow = tsEffective,
+ sInterval = sCurPeriod,
+ oFilter = oFilter,
+ enmResultsGroupingType = enmResultsGroupingType,
+ iResultsGroupingValue = idMember,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ if cEntries == 0: # Do not display empty groups
+ continue
+ aoEntries = oResultLogic.fetchResultsForListing(iPage * cItemsPerPage,
+ cItemsPerPage,
+ tsNow = tsEffective,
+ sInterval = sCurPeriod,
+ oFilter = oFilter,
+ enmResultSortBy = enmResultSortBy,
+ enmResultsGroupingType = enmResultsGroupingType,
+ iResultsGroupingValue = idMember,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ cEntriesMax = max(cEntriesMax, cEntries)
+
+ #
+ # Format them.
+ #
+ oContent = oResultsListContentType(aoEntries,
+ cEntries,
+ iPage,
+ cItemsPerPage,
+ tsEffective,
+ fnDPrint = self._oSrvGlue.dprint,
+ oDisp = self)
+
+ (_, sHtml) = oContent.show(fShowNavigation = False)
+ if sMemberName is not None:
+ _sPageBody += '<table width=100%><tr><td>'
+
+ _dParams[self.ksParamGroupMemberId] = idMember
+ sLink = WuiTmLink(sMemberName, '', _dParams, fBracketed = False).toHtml()
+
+ _sPageBody += '<h2>%s (%d)</h2></td>' % (sLink, cEntries)
+ _sPageBody += '<td><br></td>'
+ _sPageBody += '</tr></table>'
+ _sPageBody += sHtml
+ _sPageBody += '<br>'
+
+ #
+ # Complete the page by slapping navigation controls at the top and
+ # bottom of it.
+ #
+ sHtmlNavigation = self._generateResultNavigation(cEntriesMax, cItemsPerPage, iPage,
+ tsEffective, sCurPeriod, fOnlyFailures,
+ self._generateGroupContentSelector(aoGroupMembers, iGroupMemberId,
+ sAltSelectorAction));
+ if cEntriesMax > 0:
+ self._sPageBody = sHtmlNavigation + _sPageBody + sHtmlNavigation;
+ else:
+ self._sPageBody = sHtmlNavigation + '<p align="center"><i>No data to display</i></p>\n';
+
+ #
+ # Now, generate a filter control panel for the side bar.
+ #
+ if hasattr(oFilter, 'kiBranches'):
+ oFilter.aCriteria[oFilter.kiBranches].fExpanded = True;
+ if hasattr(oFilter, 'kiTestStatus'):
+ oFilter.aCriteria[oFilter.kiTestStatus].fExpanded = True;
+ self._sPageFilter = self._generateResultFilter(oFilter, oResultLogic, tsEffective, sCurPeriod,
+ enmResultsGroupingType = enmResultsGroupingType,
+ aoGroupMembers = aoGroupMembers,
+ fOnlyFailures = fOnlyFailures,
+ fOnlyNeedingReason = fOnlyNeedingReason);
+ return True;
+
+ def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType = None, aoGroupMembers = None,
+ fOnlyFailures = False, fOnlyNeedingReason = False):
+ """
+ Generates the result filter for the left hand side.
+ """
+ _ = enmResultsGroupingType; _ = aoGroupMembers; _ = fOnlyFailures; _ = fOnlyNeedingReason;
+ oResultLogic.fetchPossibleFilterOptions(oFilter, tsNow, sPeriod)
+
+ # Add non-filter parameters as hidden fields so we can use 'GET' and have URLs to bookmark.
+ self._dSideMenuFormAttrs['method'] = 'GET';
+ sHtml = u'';
+ for sKey, oValue in self._oSrvGlue.getParameters().items():
+ if len(sKey) > 3:
+ if hasattr(oValue, 'startswith'):
+ sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttr(oValue),);
+ else:
+ for oSubValue in oValue:
+ sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \
+ % (webutils.escapeAttr(sKey), webutils.escapeAttr(oSubValue),);
+
+ # Generate the filter panel.
+ sHtml += u'<div id="side-filters">\n' \
+ u' <p>Filters' \
+ u' <span class="tm-side-filter-title-buttons"><input type="submit" value="Apply" />\n' \
+ u' <a href="javascript:toggleSidebarSize();" class="tm-sidebar-size-link">&#x00bb;&#x00bb;</a></span></p>\n';
+ sHtml += u' <dl>\n';
+ for oCrit in oFilter.aCriteria:
+ if oCrit.aoPossible or oCrit.sType == oCrit.ksType_Ranges:
+ if ( oCrit.oSub is None \
+ and ( oCrit.sState == oCrit.ksState_Selected \
+ or (len(oCrit.aoPossible) <= 2 and oCrit.sType != oCrit.ksType_Ranges))) \
+ or ( oCrit.oSub is not None \
+ and ( oCrit.sState == oCrit.ksState_Selected \
+ or oCrit.oSub.sState == oCrit.ksState_Selected \
+ or (len(oCrit.aoPossible) <= 2 and len(oCrit.oSub.aoPossible) <= 2))) \
+ or oCrit.fExpanded is True:
+ sClass = 'sf-collapsible';
+ sChar = '&#9660;';
+ else:
+ sClass = 'sf-expandable';
+ sChar = '&#9654;';
+
+ sHtml += u' <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsibleDtDd(this);">%s %s</a> ' \
+ % (sClass, sChar, webutils.escapeElem(oCrit.sName),);
+ sHtml += u'<span class="tm-side-filter-dt-buttons">';
+ if oCrit.sInvVarNm is not None:
+ sHtml += u'<input id="sf-union-%s" class="tm-side-filter-union-input" ' \
+ u'name="%s" value="1" type="checkbox"%s />' \
+ u'<label for="sf-union-%s" class="tm-side-filter-union-input"></label>' \
+ % ( oCrit.sInvVarNm, oCrit.sInvVarNm, ' checked' if oCrit.fInverted else '', oCrit.sInvVarNm,);
+ sHtml += u' <input type="submit" value="Apply" />';
+ sHtml += u'</span>';
+ sHtml += u'</dt>\n' \
+ u' <dd class="%s">\n' \
+ u' <ul>\n' \
+ % (sClass);
+
+ if oCrit.sType == oCrit.ksType_Ranges:
+ assert not oCrit.oSub;
+ assert not oCrit.aoPossible;
+ asValues = [];
+ for tRange in oCrit.aoSelected:
+ if tRange[0] == tRange[1]:
+ asValues.append('%s' % (tRange[0],));
+ else:
+ asValues.append('%s-%s' % (tRange[0] if tRange[0] is not None else 'inf',
+ tRange[1] if tRange[1] is not None else 'inf'));
+ sHtml += u' <li title="%s"><input type="text" name="%s" value="%s"/></li>\n' \
+ % ( webutils.escapeAttr('comma separate list of numerical ranges'), oCrit.sVarNm,
+ ', '.join(asValues), );
+ else:
+ for oDesc in oCrit.aoPossible:
+ fChecked = oDesc.oValue in oCrit.aoSelected;
+ sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s%s/>%s%s</label>\n' \
+ % ( ' class="side-filter-irrelevant"' if oDesc.fIrrelevant else '',
+ (' title="%s"' % (webutils.escapeAttr(oDesc.sHover,)) if oDesc.sHover is not None else ''),
+ oCrit.sVarNm,
+ oDesc.oValue,
+ ' checked' if fChecked else '',
+ ' onclick="toggleCollapsibleCheckbox(this);"' if oDesc.aoSubs is not None else '',
+ webutils.escapeElem(oDesc.sDesc),
+ '<span class="side-filter-count"> [%u]</span>' % (oDesc.cTimes) if oDesc.cTimes is not None
+ else '', );
+ if oDesc.aoSubs is not None:
+ sHtml += u' <ul class="sf-checkbox-%s">\n' % ('collapsible' if fChecked else 'expandable', );
+ for oSubDesc in oDesc.aoSubs:
+ fSubChecked = oSubDesc.oValue in oCrit.oSub.aoSelected;
+ sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s/>%s%s</label>\n' \
+ % ( ' class="side-filter-irrelevant"' if oSubDesc.fIrrelevant else '',
+ ' title="%s"' % ( webutils.escapeAttr(oSubDesc.sHover,) if oSubDesc.sHover is not None
+ else ''),
+ oCrit.oSub.sVarNm, oSubDesc.oValue, ' checked' if fSubChecked else '',
+ webutils.escapeElem(oSubDesc.sDesc),
+ '<span class="side-filter-count"> [%u]</span>' % (oSubDesc.cTimes)
+ if oSubDesc.cTimes is not None else '', );
+
+ sHtml += u' </ul>\n';
+ sHtml += u' </li>';
+
+ sHtml += u' </ul>\n' \
+ u' </dd>\n';
+
+ sHtml += u' </dl>\n';
+ sHtml += u' <input type="submit" value="Apply"/>\n';
+ sHtml += u' <input type="reset" value="Reset"/>\n';
+ sHtml += u' <button type="button" onclick="clearForm(\'side-menu-form\');">Clear</button>\n';
+ sHtml += u'</div>\n';
+ return sHtml;
+
+ def _actionResultsUnGrouped(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ #return self._actionResultsListing(TestResultLogic, WuiGroupedResultList)?
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestGroup(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestGroup,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByBuildRev(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildRev,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByBuildCat(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildCat,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestBox(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestBox,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByTestCase(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestCase,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByOS(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeOS,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedByArch(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeArch,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+ def _actionResultsGroupedBySchedGroup(self):
+ """ Action wrapper. """
+ from testmanager.webui.wuitestresult import WuiGroupedResultList;
+ from testmanager.core.testresults import TestResultLogic, TestResultFilter;
+ return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeSchedGroup,
+ TestResultLogic, TestResultFilter, WuiGroupedResultList);
+
+
+ def _actionTestSetDetailsCommon(self, idTestSet):
+ """Show test case execution result details."""
+ from testmanager.core.build import BuildDataEx;
+ from testmanager.core.testbox import TestBoxData;
+ from testmanager.core.testcase import TestCaseDataEx;
+ from testmanager.core.testcaseargs import TestCaseArgsDataEx;
+ from testmanager.core.testgroup import TestGroupData;
+ from testmanager.core.testresults import TestResultLogic;
+ from testmanager.core.testset import TestSetData;
+ from testmanager.webui.wuitestresult import WuiTestResult;
+
+ self._sTemplate = 'template-details.html';
+ self._checkForUnknownParameters()
+
+ oTestSetData = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ try:
+ (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet);
+ except TMTooManyRows:
+ (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet, 2);
+ oBuildDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuild, oTestSetData.tsCreated);
+ try: oBuildValidationKitDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuildTestSuite,
+ oTestSetData.tsCreated);
+ except: oBuildValidationKitDataEx = None;
+ oTestBoxData = TestBoxData().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestBox);
+ oTestGroupData = TestGroupData().initFromDbWithId(self._oDb, ## @todo This bogus time wise. Bad DB design?
+ oTestSetData.idTestGroup, oTestSetData.tsCreated);
+ oTestCaseDataEx = TestCaseDataEx().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestCase,
+ oTestSetData.tsConfig);
+ oTestCaseArgsDataEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(self._oDb, oTestSetData.idGenTestCaseArgs,
+ oTestSetData.tsConfig);
+
+ oContent = WuiTestResult(oDisp = self, fnDPrint = self._oSrvGlue.dprint);
+ (self._sPageTitle, self._sPageBody) = oContent.showTestCaseResultDetails(oTestResultTree,
+ oTestSetData,
+ oBuildDataEx,
+ oBuildValidationKitDataEx,
+ oTestBoxData,
+ oTestGroupData,
+ oTestCaseDataEx,
+ oTestCaseArgsDataEx);
+ return True
+
+ def _actionTestSetDetails(self):
+ """Show test case execution result details."""
+ from testmanager.core.testset import TestSetData;
+
+ idTestSet = self.getIntParam(TestSetData.ksParam_idTestSet);
+ return self._actionTestSetDetailsCommon(idTestSet);
+
+ def _actionTestSetDetailsFromResult(self):
+ """Show test case execution result details."""
+ from testmanager.core.testresults import TestResultData;
+ from testmanager.core.testset import TestSetData;
+ idTestResult = self.getIntParam(TestSetData.ksParam_idTestResult);
+ oTestResultData = TestResultData().initFromDbWithId(self._oDb, idTestResult);
+ return self._actionTestSetDetailsCommon(oTestResultData.idTestSet);
+
+
+ def _actionTestResultFailureAdd(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure);
+
+ def _actionTestResultFailureAddPost(self):
+ """Add test result failure result"""
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ if self.ksParamRedirectTo not in self._dParams:
+ raise WuiException('Missing parameter ' + self.ksParamRedirectTo);
+
+ return self._actionGenericFormAddPost(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
+ def _actionTestResultFailureDoRemove(self):
+ """ Action wrapper. """
+ from testmanager.core.testresultfailures import TestResultFailureData, TestResultFailureLogic;
+ return self._actionGenericDoRemove(TestResultFailureLogic, TestResultFailureData.ksParam_idTestResult,
+ self.ksActionResultsUnGrouped);
+
+ def _actionTestResultFailureDetails(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, 'idTestResult');
+
+ def _actionTestResultFailureEdit(self):
+ """ Pro forma. """
+ from testmanager.core.testresultfailures import TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure,
+ TestResultFailureData.ksParam_idTestResult);
+
+ def _actionTestResultFailureEditPost(self):
+ """Edit test result failure result"""
+ from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData;
+ from testmanager.webui.wuitestresultfailure import WuiTestResultFailure;
+ return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic,
+ WuiTestResultFailure, self.ksActionResultsUnGrouped);
+
+ def _actionViewLog(self):
+ """
+ Log viewer action.
+ """
+ from testmanager.core.testresults import TestResultLogic, TestResultFileDataEx;
+ from testmanager.core.testset import TestSetData, TestSetLogic;
+ from testmanager.webui.wuilogviewer import WuiLogViewer;
+
+ self._sTemplate = 'template-details.html'; ## @todo create new template (background color, etc)
+ idTestSet = self.getIntParam(self.ksParamLogSetId, iMin = 1);
+ idLogFile = self.getIntParam(self.ksParamLogFileId, iMin = 0, iDefault = 0);
+ cbChunk = self.getIntParam(self.ksParamLogChunkSize, iMin = 256, iMax = 16777216, iDefault = 1024*1024);
+ iChunk = self.getIntParam(self.ksParamLogChunkNo, iMin = 0,
+ iMax = config.g_kcMbMaxMainLog * 1048576 / cbChunk, iDefault = 0);
+ self._checkForUnknownParameters();
+
+ oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if idLogFile == 0:
+ oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet);
+ aoTimestamps = TestResultLogic(self._oDb).fetchTimestampsForLogViewer(idTestSet);
+ else:
+ oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idLogFile);
+ aoTimestamps = [];
+ if oTestFile.sMime not in [ 'text/plain',]:
+ raise WuiException('The log view does not display files of type: %s' % (oTestFile.sMime,));
+
+ oContent = WuiLogViewer(oTestSet, oTestFile, cbChunk, iChunk, aoTimestamps,
+ oDisp = self, fnDPrint = self._oSrvGlue.dprint);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionGetFile(self):
+ """
+ Get file action.
+ """
+ from testmanager.core.testset import TestSetData, TestSetLogic;
+ from testmanager.core.testresults import TestResultFileDataEx;
+
+ idTestSet = self.getIntParam(self.ksParamGetFileSetId, iMin = 1);
+ idFile = self.getIntParam(self.ksParamGetFileId, iMin = 0, iDefault = 0);
+ fDownloadIt = self.getBoolParam(self.ksParamGetFileDownloadIt, fDefault = True);
+ self._checkForUnknownParameters();
+
+ #
+ # Get the file info and open it.
+ #
+ oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet);
+ if idFile == 0:
+ oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet);
+ else:
+ oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idFile);
+
+ (oFile, oSizeOrError, _) = oTestSet.openFile(oTestFile.sFile, 'rb');
+ if oFile is None:
+ raise Exception(oSizeOrError);
+
+ #
+ # Send the file.
+ #
+ self._oSrvGlue.setHeaderField('Content-Type', oTestFile.getMimeWithEncoding());
+ if fDownloadIt:
+ self._oSrvGlue.setHeaderField('Content-Disposition', 'attachment; filename="TestSet-%d-%s"'
+ % (idTestSet, oTestFile.sFile,));
+ while True:
+ abChunk = oFile.read(262144);
+ if not abChunk:
+ break;
+ self._oSrvGlue.writeRaw(abChunk);
+ return self.ksDispatchRcAllDone;
+
+ def _actionGenericReport(self, oModelType, oFilterType, oReportType):
+ """
+ Generic report action.
+ oReportType is a child of WuiReportContentBase.
+ oFilterType is a child of ModelFilterBase.
+ oModelType is a child of ReportModelBase.
+ """
+ from testmanager.core.report import ReportModelBase;
+
+ tsEffective = self.getEffectiveDateParam();
+ cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 2, iMax = 99, iDefault = 7);
+ cHoursPerPeriod = self.getIntParam(self.ksParamReportPeriodInHours, iMin = 1, iMax = 168, iDefault = 24);
+ sSubject = self.getStringParam(self.ksParamReportSubject, ReportModelBase.kasSubjects,
+ ReportModelBase.ksSubEverything);
+ if sSubject == ReportModelBase.ksSubEverything:
+ aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, aiDefaults = []);
+ else:
+ aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, iMin = 1);
+ if aidSubjects is None:
+ raise WuiException('Missing parameter %s' % (self.ksParamReportSubjectIds,));
+
+ aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns,
+ iMin = -getattr(oReportType, 'kcMaxSortColumns', cPeriods) + 1,
+ iMax = getattr(oReportType, 'kcMaxSortColumns', cPeriods), aiDefaults = []);
+ aiSortColumns = [];
+ for iSortColumn in aiSortColumnsDup:
+ if iSortColumn not in aiSortColumns:
+ aiSortColumns.append(iSortColumn);
+
+ oFilter = oFilterType().initFromParams(self);
+ self._checkForUnknownParameters();
+
+ dParams = \
+ {
+ self.ksParamEffectiveDate: tsEffective,
+ self.ksParamReportPeriods: cPeriods,
+ self.ksParamReportPeriodInHours: cHoursPerPeriod,
+ self.ksParamReportSubject: sSubject,
+ self.ksParamReportSubjectIds: aidSubjects,
+ };
+ ## @todo oFilter.
+
+ oModel = oModelType(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter);
+ oContent = oReportType(oModel, dParams, fSubReport = False, aiSortColumns = aiSortColumns,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ sNavi = self._generateReportNavigation(tsEffective, cHoursPerPeriod, cPeriods);
+ self._sPageBody = sNavi + self._sPageBody;
+
+ if hasattr(oFilter, 'kiBranches'):
+ oFilter.aCriteria[oFilter.kiBranches].fExpanded = True;
+ self._sPageFilter = self._generateResultFilter(oFilter, oModel, tsEffective, '%s hours' % (cHoursPerPeriod * cPeriods,));
+ return True;
+
+ def _actionReportSummary(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportSummary;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSummary);
+
+ def _actionReportRate(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportSuccessRate;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSuccessRate);
+
+ def _actionReportTestCaseFailures(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportTestCaseFailures;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportTestCaseFailures);
+
+ def _actionReportFailureReasons(self):
+ """ Action wrapper. """
+ from testmanager.core.report import ReportLazyModel, ReportFilter;
+ from testmanager.webui.wuireport import WuiReportFailureReasons;
+ return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportFailureReasons);
+
+ def _actionGraphWiz(self):
+ """
+ Graph wizard action.
+ """
+ from testmanager.core.report import ReportModelBase, ReportGraphModel;
+ from testmanager.webui.wuigraphwiz import WuiGraphWiz;
+ self._sTemplate = 'template-graphwiz.html';
+
+ tsEffective = self.getEffectiveDateParam();
+ cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 1, iMax = 1, iDefault = 1); # Not needed yet.
+ sTmp = self.getStringParam(self.ksParamReportPeriodInHours, sDefault = '3 weeks');
+ (cHoursPerPeriod, sError) = utils.parseIntervalHours(sTmp);
+ if sError is not None: raise WuiException(sError);
+ asSubjectIds = self.getListOfStrParams(self.ksParamReportSubjectIds);
+ sSubject = self.getStringParam(self.ksParamReportSubject, [ReportModelBase.ksSubEverything],
+ ReportModelBase.ksSubEverything); # dummy
+ aidTestBoxes = self.getListOfIntParams(self.ksParamGraphWizTestBoxIds, iMin = 1, aiDefaults = []);
+ aidBuildCats = self.getListOfIntParams(self.ksParamGraphWizBuildCatIds, iMin = 1, aiDefaults = []);
+ aidTestCases = self.getListOfIntParams(self.ksParamGraphWizTestCaseIds, iMin = 1, aiDefaults = []);
+ fSepTestVars = self.getBoolParam(self.ksParamGraphWizSepTestVars, fDefault = False);
+
+ enmGraphImpl = self.getStringParam(self.ksParamGraphWizImpl, asValidValues = self.kasGraphWizImplValid,
+ sDefault = self.ksGraphWizImpl_Default);
+ cx = self.getIntParam(self.ksParamGraphWizWidth, iMin = 128, iMax = 8192, iDefault = 1280);
+ cy = self.getIntParam(self.ksParamGraphWizHeight, iMin = 128, iMax = 8192, iDefault = int(cx * 5 / 16) );
+ cDotsPerInch = self.getIntParam(self.ksParamGraphWizDpi, iMin = 64, iMax = 512, iDefault = 96);
+ cPtFont = self.getIntParam(self.ksParamGraphWizFontSize, iMin = 6, iMax = 32, iDefault = 8);
+ fErrorBarY = self.getBoolParam(self.ksParamGraphWizErrorBarY, fDefault = False);
+ cMaxErrorBarY = self.getIntParam(self.ksParamGraphWizMaxErrorBarY, iMin = 8, iMax = 9999999, iDefault = 18);
+ cMaxPerGraph = self.getIntParam(self.ksParamGraphWizMaxPerGraph, iMin = 1, iMax = 24, iDefault = 8);
+ fXkcdStyle = self.getBoolParam(self.ksParamGraphWizXkcdStyle, fDefault = False);
+ fTabular = self.getBoolParam(self.ksParamGraphWizTabular, fDefault = False);
+ idSrcTestSet = self.getIntParam(self.ksParamGraphWizSrcTestSetId, iDefault = None);
+ self._checkForUnknownParameters();
+
+ dParams = \
+ {
+ self.ksParamEffectiveDate: tsEffective,
+ self.ksParamReportPeriods: cPeriods,
+ self.ksParamReportPeriodInHours: cHoursPerPeriod,
+ self.ksParamReportSubject: sSubject,
+ self.ksParamReportSubjectIds: asSubjectIds,
+ self.ksParamGraphWizTestBoxIds: aidTestBoxes,
+ self.ksParamGraphWizBuildCatIds: aidBuildCats,
+ self.ksParamGraphWizTestCaseIds: aidTestCases,
+ self.ksParamGraphWizSepTestVars: fSepTestVars,
+
+ self.ksParamGraphWizImpl: enmGraphImpl,
+ self.ksParamGraphWizWidth: cx,
+ self.ksParamGraphWizHeight: cy,
+ self.ksParamGraphWizDpi: cDotsPerInch,
+ self.ksParamGraphWizFontSize: cPtFont,
+ self.ksParamGraphWizErrorBarY: fErrorBarY,
+ self.ksParamGraphWizMaxErrorBarY: cMaxErrorBarY,
+ self.ksParamGraphWizMaxPerGraph: cMaxPerGraph,
+ self.ksParamGraphWizXkcdStyle: fXkcdStyle,
+ self.ksParamGraphWizTabular: fTabular,
+ self.ksParamGraphWizSrcTestSetId: idSrcTestSet,
+ };
+
+ oModel = ReportGraphModel(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, asSubjectIds,
+ aidTestBoxes, aidBuildCats, aidTestCases, fSepTestVars);
+ oContent = WuiGraphWiz(oModel, dParams, fSubReport = False, fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
+ def _actionVcsHistoryTooltip(self):
+ """
+ Version control system history.
+ """
+ from testmanager.webui.wuivcshistory import WuiVcsHistoryTooltip;
+ from testmanager.core.vcsrevisions import VcsRevisionLogic;
+
+ self._sTemplate = 'template-tooltip.html';
+ iRevision = self.getIntParam(self.ksParamVcsHistoryRevision, iMin = 0, iMax = 999999999);
+ sRepository = self.getStringParam(self.ksParamVcsHistoryRepository);
+ cEntries = self.getIntParam(self.ksParamVcsHistoryEntries, iMin = 1, iMax = 1024, iDefault = 8);
+ self._checkForUnknownParameters();
+
+ aoEntries = VcsRevisionLogic(self._oDb).fetchTimeline(sRepository, iRevision, cEntries);
+ oContent = WuiVcsHistoryTooltip(aoEntries, sRepository, iRevision, cEntries,
+ fnDPrint = self._oSrvGlue.dprint, oDisp = self);
+ (self._sPageTitle, self._sPageBody) = oContent.show();
+ return True;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuireport.py b/src/VBox/ValidationKit/testmanager/webui/wuireport.py
new file mode 100755
index 00000000..f1625176
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuireport.py
@@ -0,0 +1,817 @@
+# -*- coding: utf-8 -*-
+# $Id: wuireport.py $
+
+"""
+Test Manager WUI - Reports.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+
+# Validation Kit imports.
+from common import webutils;
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink, WuiSvnLinkWithTooltip;
+from testmanager.webui.wuihlpgraph import WuiHlpGraphDataTable, WuiHlpBarGraph;
+from testmanager.webui.wuitestresult import WuiTestSetLink, WuiTestResultsForTestCaseLink, WuiTestResultsForTestBoxLink;
+from testmanager.webui.wuiadmintestcase import WuiTestCaseDetailsLink;
+from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLinkShort;
+from testmanager.core.report import ReportModelBase, ReportFilter;
+from testmanager.core.testresults import TestResultFilter;
+
+
+class WuiReportSummaryLink(WuiTmLink):
+ """ Generic report summary link. """
+
+ def __init__(self, sSubject, aIdSubjects, sName = WuiContentBase.ksShortReportLink,
+ tsNow = None, cPeriods = None, cHoursPerPeriod = None, fBracketed = False, dExtraParams = None):
+ from testmanager.webui.wuimain import WuiMain;
+ dParams = {
+ WuiMain.ksParamAction: WuiMain.ksActionReportSummary,
+ WuiMain.ksParamReportSubject: sSubject,
+ WuiMain.ksParamReportSubjectIds: aIdSubjects,
+ };
+ if dExtraParams is not None:
+ dParams.update(dExtraParams);
+ if tsNow is not None:
+ dParams[WuiMain.ksParamEffectiveDate] = tsNow;
+ if cPeriods is not None:
+ dParams[WuiMain.ksParamReportPeriods] = cPeriods;
+ if cPeriods is not None:
+ dParams[WuiMain.ksParamReportPeriodInHours] = cHoursPerPeriod;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed);
+
+
+class WuiReportBase(WuiContentBase):
+ """
+ Base class for the reports.
+ """
+
+ def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self._oModel = oModel;
+ self._dParams = dParams;
+ self._fSubReport = fSubReport;
+ self._sTitle = None;
+ self._aiSortColumns = aiSortColumns;
+
+ # Additional URL parameters for reports:
+ from testmanager.webui.wuimain import WuiMain;
+ self._dExtraParams = ReportFilter().strainParameters({} if oDisp is None else oDisp.getParameters(),
+ (WuiMain.ksParamReportPeriods,
+ WuiMain.ksParamReportPeriodInHours,
+ WuiMain.ksParamEffectiveDate,));
+ # Additional URL parameters for test results:
+ self._dExtraTestResultsParams = TestResultFilter().strainParameters(oDisp.getParameters(),
+ (WuiMain.ksParamEffectiveDate,));
+ self._dExtraTestResultsParams[WuiMain.ksParamEffectivePeriod] = self.getPeriodForTestResults();
+
+
+ def generateNavigator(self, sWhere):
+ """
+ Generates the navigator (manipulate _dParams).
+ Returns HTML.
+ """
+ assert sWhere in ('top', 'bottom',);
+
+ return '';
+
+ def generateReportBody(self):
+ """
+ This is overridden by the child class to generate the report.
+ Returns HTML.
+ """
+ return '<h3>Must override generateReportBody!</h3>';
+
+ def show(self):
+ """
+ Generate the report.
+ Returns (sTitle, HTML).
+ """
+
+ sTitle = self._sTitle if self._sTitle is not None else type(self).__name__;
+ sReport = self.generateReportBody();
+ if not self._fSubReport:
+ sReport = self.generateNavigator('top') + sReport + self.generateNavigator('bottom');
+ sTitle = self._oModel.sSubject + ' - ' + sTitle; ## @todo add subject to title in a proper way!
+
+ sReport += '\n\n<!-- HEYYOU: sSubject=%s aidSubjects=%s -->\n\n' % (self._oModel.sSubject, self._oModel.aidSubjects);
+ return (sTitle, sReport);
+
+ #
+ # Utility methods
+ #
+
+ def getPeriodForTestResults(self):
+ """
+ Takes the report period length and count and translates it into a
+ reasonable test result period (value).
+ """
+ from testmanager.webui.wuimain import WuiMain;
+ cHours = self._oModel.cPeriods * self._oModel.cHoursPerPeriod;
+ if cHours > 7*24:
+ cHours = cHours // 2;
+ for sPeriodValue, _, cPeriodHours in WuiMain.kaoResultPeriods:
+ sPeriod = sPeriodValue;
+ if cPeriodHours >= cHours:
+ return sPeriod;
+ return sPeriod;
+
+ @staticmethod
+ def fmtPct(cHits, cTotal):
+ """
+ Formats a percent number.
+ Returns a string.
+ """
+ uPct = cHits * 100 // cTotal;
+ if uPct >= 10 and (uPct > 103 or uPct <= 95):
+ return '%s%%' % (uPct,);
+ return '%.1f%%' % (cHits * 100.0 / cTotal,);
+
+ @staticmethod
+ def fmtPctWithHits(cHits, cTotal):
+ """
+ Formats a percent number with total in parentheses.
+ Returns a string.
+ """
+ return '%s (%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits);
+
+ @staticmethod
+ def fmtPctWithHitsAndTotal(cHits, cTotal):
+ """
+ Formats a percent number with total in parentheses.
+ Returns a string.
+ """
+ return '%s (%s/%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits, cTotal);
+
+
+
+class WuiReportSuccessRate(WuiReportBase):
+ """
+ Generates a report displaying the success rate over time.
+ """
+
+ def generateReportBody(self):
+ self._sTitle = 'Success rate';
+ fTailoredForGoogleCharts = True;
+
+ #
+ # Get the data and check if we have anything in the 'skipped' category.
+ #
+ adPeriods = self._oModel.getSuccessRates();
+
+ cTotalSkipped = 0;
+ for dStatuses in adPeriods:
+ cTotalSkipped += dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ #
+ # Output some general stats before the graphs.
+ #
+ cTotalNow = adPeriods[0][ReportModelBase.ksTestStatus_Success];
+ cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Skipped];
+ cSuccessNow = cTotalNow;
+ cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Failure];
+
+ sReport = '<p>Current success rate: ';
+ if cTotalNow > 0:
+ cSkippedNow = adPeriods[0][ReportModelBase.ksTestStatus_Skipped];
+ if cSkippedNow > 0:
+ sReport += '%s (thereof %s skipped)</p>\n' \
+ % (self.fmtPct(cSuccessNow, cTotalNow), self.fmtPct(cSkippedNow, cTotalNow),);
+ else:
+ sReport += '%s (none skipped)</p>\n' % (self.fmtPct(cSuccessNow, cTotalNow),);
+ else:
+ sReport += 'N/A</p>\n'
+
+ #
+ # Create the data table.
+ #
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]);
+ else:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Failed' ]);
+
+ for i, dStatuses in enumerate(adPeriods):
+ cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success];
+ cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure];
+ cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ cSuccess = cSuccesses + cSkipped;
+ cTotal = cSuccess + cFailures;
+ sPeriod = self._oModel.getPeriodDesc(i);
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal if cTotal else 0,
+ cSkipped * 100 // cTotal if cTotal else 0,
+ cFailures * 100 // cTotal if cTotal else 0, ],
+ [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None,
+ self.fmtPct(cSkipped, cTotal) if cSkipped else None,
+ self.fmtPct(cFailures, cTotal) if cFailures else None, ]);
+ else:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal if cTotal else 0,
+ cFailures * 100 // cTotal if cTotal else 0, ],
+ [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None,
+ self.fmtPct(cFailures, cTotal) if cFailures else None, ]);
+ elif cTotal > 0:
+ if cTotalSkipped > 0:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal,
+ cSkipped * 100 // cTotal,
+ cFailures * 100 // cTotal, ],
+ [ self.fmtPctWithHits(cSuccesses, cTotal),
+ self.fmtPctWithHits(cSkipped, cTotal),
+ self.fmtPctWithHits(cFailures, cTotal), ]);
+ else:
+ oTable.addRow(sPeriod,
+ [ cSuccesses * 100 // cTotal,
+ cFailures * 100 // cTotal, ],
+ [ self.fmtPctWithHits(cSuccesses, cTotal),
+ self.fmtPctWithHits(cFailures, cTotal), ]);
+ elif cTotalSkipped > 0:
+ oTable.addRow(sPeriod, [ 0, 0, 0 ], [ '0%', '0%', '0%' ]);
+ else:
+ oTable.addRow(sPeriod, [ 0, 0 ], [ '0%', '0%' ]);
+
+ #
+ # Render the graph.
+ #
+ oGraph = WuiHlpBarGraph('success-rate', oTable, self._oDisp);
+ oGraph.setRangeMax(100);
+ sReport += oGraph.renderGraph();
+
+ #
+ # Graph with absolute counts.
+ #
+ if fTailoredForGoogleCharts:
+ if cTotalSkipped > 0:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]);
+ else:
+ oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]);
+ for i, dStatuses in enumerate(adPeriods):
+ cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success];
+ cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure];
+ cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped];
+
+ if cTotalSkipped > 0:
+ oTable.addRow(None, #self._oModel.getPeriodDesc(i),
+ [ cSuccesses, cSkipped, cFailures, ],
+ [ str(cSuccesses) if cSuccesses > 0 else None,
+ str(cSkipped) if cSkipped > 0 else None,
+ str(cFailures) if cFailures > 0 else None, ]);
+ else:
+ oTable.addRow(None, #self._oModel.getPeriodDesc(i),
+ [ cSuccesses, cFailures, ],
+ [ str(cSuccesses) if cSuccesses > 0 else None,
+ str(cFailures) if cFailures > 0 else None, ]);
+ oGraph = WuiHlpBarGraph('success-numbers', oTable, self._oDisp);
+ oGraph.invertYDirection();
+ sReport += oGraph.renderGraph();
+
+ return sReport;
+
+
+class WuiReportFailuresBase(WuiReportBase):
+ """
+ Common parent of WuiReportFailureReasons and WuiReportTestCaseFailures.
+ """
+
+ def _splitSeriesIntoMultipleGraphs(self, aidSorted, cMaxSeriesPerGraph = 8):
+ """
+ Splits the ID array into one or more arrays, making sure we don't
+ have too many series per graph.
+ Returns array of ID arrays.
+ """
+ if len(aidSorted) <= cMaxSeriesPerGraph + 2:
+ return [aidSorted,];
+ cGraphs = len(aidSorted) // cMaxSeriesPerGraph + (len(aidSorted) % cMaxSeriesPerGraph != 0);
+ cPerGraph = len(aidSorted) // cGraphs + (len(aidSorted) % cGraphs != 0);
+
+ aaoRet = [];
+ cLeft = len(aidSorted);
+ iSrc = 0;
+ while cLeft > 0:
+ cThis = cPerGraph;
+ if cLeft <= cPerGraph + 2:
+ cThis = cLeft;
+ elif cLeft <= cPerGraph * 2 + 4:
+ cThis = cLeft // 2;
+ aaoRet.append(aidSorted[iSrc : iSrc + cThis]);
+ iSrc += cThis;
+ cLeft -= cThis;
+ return aaoRet;
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ """
+ Worker for _formatEdgeOccurence that child classes overrides to format
+ their type of subject data in the best possible way.
+ """
+ _ = oTransient;
+ assert False;
+ return '';
+
+ def _formatEdgeOccurence(self, oTransient):
+ """
+ Helper for formatting the transients.
+ oTransient is of type ReportFailureReasonTransient or ReportTestCaseFailureTransient.
+ """
+ sHtml = u'<li>';
+ if oTransient.fEnter: sHtml += 'Since ';
+ else: sHtml += 'Until ';
+ sHtml += WuiSvnLinkWithTooltip(oTransient.iRevision, oTransient.sRepository, fBracketed = 'False').toHtml();
+ sHtml += u', %s: ' % (WuiTestSetLink(oTransient.idTestSet, self.formatTsShort(oTransient.tsDone),
+ fBracketed = False).toHtml(), )
+ sHtml += self._formatEdgeOccurenceSubject(oTransient);
+ sHtml += u'</li>\n';
+ return sHtml;
+
+ def _generateTransitionList(self, oSet):
+ """
+ Generates the enter and leave lists.
+ """
+ # Skip this if we're looking at builds.
+ if self._oModel.sSubject in [self._oModel.ksSubBuild,] and len(self._oModel.aidSubjects) in [1, 2]:
+ return u'';
+
+ sHtml = u'<h4>Movements:</h4>\n' \
+ u'<ul>\n';
+ if not oSet.aoEnterInfo and not oSet.aoLeaveInfo:
+ sHtml += u'<li>No changes</li>\n';
+ else:
+ for oTransient in oSet.aoEnterInfo:
+ sHtml += self._formatEdgeOccurence(oTransient);
+ for oTransient in oSet.aoLeaveInfo:
+ sHtml += self._formatEdgeOccurence(oTransient);
+ sHtml += u'</ul>\n';
+
+ return sHtml;
+
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ """ Formats the series name column for the HTML table. """
+ return '<th>Subject Name</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ """ Formats the series name for the HTML table. """
+ _ = oSet;
+ return '<td>%d</td>' % (idKey,);
+
+ def _formatRowValueForTable(self, oRow, oPeriod, cColsPerSeries):
+ """ Formats a row value for the HTML table. """
+ _ = oPeriod;
+ if oRow is None:
+ return u'<td colspan="%d"> </td>' % (cColsPerSeries,);
+ if cColsPerSeries == 2:
+ return u'<td align="right">%u%%</td><td align="center">%u / %u</td>' \
+ % (oRow.cHits * 100 // oRow.cTotal, oRow.cHits, oRow.cTotal);
+ return u'<td align="center">%u</td>' % (oRow.cHits,);
+
+ def _formatSeriesTotalForTable(self, oSet, idKey, cColsPerSeries):
+ """ Formats the totals cell for a data series in the HTML table. """
+ dcTotalPerId = getattr(oSet, 'dcTotalPerId', None);
+ if cColsPerSeries == 2:
+ return u'<td align="right">%u%%</td><td align="center">%u/%u</td>' \
+ % (oSet.dcHitsPerId[idKey] * 100 // dcTotalPerId[idKey], oSet.dcHitsPerId[idKey], dcTotalPerId[idKey]);
+ return u'<td align="center">%u</td>' % (oSet.dcHitsPerId[idKey],);
+
+ def _generateTableForSet(self, oSet, aidSorted = None, iSortColumn = 0,
+ fWithTotals = True, cColsPerSeries = None):
+ """
+ Turns the set into a table.
+
+ Returns raw html.
+ """
+ sHtml = u'<table class="tmtbl-report-set" width="100%%">\n';
+ if cColsPerSeries is None:
+ cColsPerSeries = 2 if hasattr(oSet, 'dcTotalPerId') else 1;
+
+ # Header row.
+ sHtml += u' <tr><thead><th>#</th>';
+ sHtml += self._formatSeriesNameColumnHeadersForTable();
+ for iPeriod, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">%s</a>%s</th>' \
+ % ( cColsPerSeries, self._oDisp.ksParamSortColumns, iPeriod, webutils.escapeElem(oPeriod.sDesc),
+ '&#x25bc;' if iPeriod == iSortColumn else '');
+ if fWithTotals:
+ sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">Total</a>%s</th>' \
+ % ( cColsPerSeries, self._oDisp.ksParamSortColumns, len(oSet.aoPeriods),
+ '&#x25bc;' if iSortColumn == len(oSet.aoPeriods) else '');
+ sHtml += u'</thead></td>\n';
+
+ # Each data series.
+ if aidSorted is None:
+ aidSorted = oSet.dSubjects.keys();
+ sHtml += u' <tbody>\n';
+ for iRow, idKey in enumerate(aidSorted):
+ sHtml += u' <tr class="%s">' % ('tmodd' if iRow & 1 else 'tmeven',);
+ sHtml += u'<td align="left">#%u</td>' % (iRow + 1,);
+ sHtml += self._formatSeriesNameForTable(oSet, idKey);
+ for oPeriod in reversed(oSet.aoPeriods):
+ oRow = oPeriod.dRowsById.get(idKey, None);
+ sHtml += self._formatRowValueForTable(oRow, oPeriod, cColsPerSeries);
+ if fWithTotals:
+ sHtml += self._formatSeriesTotalForTable(oSet, idKey, cColsPerSeries);
+ sHtml += u' </tr>\n';
+ sHtml += u' </tbody>\n';
+ sHtml += u'</table>\n';
+ return sHtml;
+
+
+class WuiReportFailuresWithTotalBase(WuiReportFailuresBase):
+ """
+ For ReportPeriodSetWithTotalBase.
+ """
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ """
+ Format the subject name for the graph.
+ """
+ return str(oSubject);
+
+ def _getSortedIds(self, oSet):
+ """
+ Get default sorted subject IDs and which column.
+ """
+
+ # Figure the sorting column.
+ if self._aiSortColumns is not None \
+ and self._aiSortColumns \
+ and abs(self._aiSortColumns[0]) <= len(oSet.aoPeriods):
+ iSortColumn = abs(self._aiSortColumns[0]);
+ fByTotal = iSortColumn >= len(oSet.aoPeriods); # pylint: disable=unused-variable
+ elif oSet.cMaxTotal < 10:
+ iSortColumn = len(oSet.aoPeriods);
+ else:
+ iSortColumn = 0;
+
+ if iSortColumn >= len(oSet.aoPeriods):
+ # Sort the total.
+ aidSortedRaw = sorted(oSet.dSubjects,
+ key = lambda idKey: oSet.dcHitsPerId[idKey] * 10000 // oSet.dcTotalPerId[idKey],
+ reverse = True);
+ else:
+ # Sort by NOW column.
+ dTmp = {};
+ for idKey in oSet.dSubjects:
+ oRow = oSet.aoPeriods[-1 - iSortColumn].dRowsById.get(idKey, None);
+ if oRow is None: dTmp[idKey] = 0;
+ else: dTmp[idKey] = oRow.cHits * 10000 // max(1, oRow.cTotal);
+ aidSortedRaw = sorted(dTmp, key = lambda idKey: dTmp[idKey], reverse = True);
+ return (aidSortedRaw, iSortColumn);
+
+ def _generateGraph(self, oSet, sIdBase, aidSortedRaw):
+ """
+ Generates graph.
+ """
+ sHtml = u'';
+ fGenerateGraph = len(aidSortedRaw) <= 6 and len(aidSortedRaw) > 0; ## Make this configurable.
+ if fGenerateGraph:
+ # Figure the graph width for all of them.
+ uPctMax = max(oSet.uMaxPct, oSet.cMaxHits * 100 // oSet.cMaxTotal);
+ uPctMax = max(uPctMax + 2, 10);
+
+ for _, aidSorted in enumerate(self._splitSeriesIntoMultipleGraphs(aidSortedRaw, 8)):
+ asNames = [];
+ for idKey in aidSorted:
+ oSubject = oSet.dSubjects[idKey];
+ asNames.append(self._formatSeriedNameForGraph(oSubject));
+
+ oTable = WuiHlpGraphDataTable('Period', asNames);
+
+ for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ aiValues = [];
+ asValues = [];
+
+ for idKey in aidSorted:
+ oRow = oPeriod.dRowsById.get(idKey, None);
+ if oRow is not None:
+ aiValues.append(oRow.cHits * 100 // oRow.cTotal);
+ asValues.append(self.fmtPctWithHitsAndTotal(oRow.cHits, oRow.cTotal));
+ else:
+ aiValues.append(0);
+ asValues.append('0');
+
+ oTable.addRow(oPeriod.sDesc, aiValues, asValues);
+
+ if True: # pylint: disable=using-constant-test
+ aiValues = [];
+ asValues = [];
+ for idKey in aidSorted:
+ uPct = oSet.dcHitsPerId[idKey] * 100 // oSet.dcTotalPerId[idKey];
+ aiValues.append(uPct);
+ asValues.append(self.fmtPctWithHitsAndTotal(oSet.dcHitsPerId[idKey], oSet.dcTotalPerId[idKey]));
+ oTable.addRow('Totals', aiValues, asValues);
+
+ oGraph = WuiHlpBarGraph(sIdBase, oTable, self._oDisp);
+ oGraph.setRangeMax(uPctMax);
+ sHtml += '<br>\n';
+ sHtml += oGraph.renderGraph();
+ return sHtml;
+
+
+
+class WuiReportFailureReasons(WuiReportFailuresBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ return u'%s / %s' % ( webutils.escapeElem(oTransient.oReason.oCategory.sShort),
+ webutils.escapeElem(oTransient.oReason.sShort),);
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Failure Reason</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oReason = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ sHtml += u'%s / %s' % ( webutils.escapeElem(oReason.oCategory.sShort), webutils.escapeElem(oReason.sShort),);
+ sHtml += u'</td>';
+ return sHtml;
+
+
+ def generateReportBody(self):
+ self._sTitle = 'Failure reasons';
+
+ #
+ # Get the data and sort the data series in descending order of badness.
+ #
+ oSet = self._oModel.getFailureReasons();
+ aidSortedRaw = sorted(oSet.dSubjects, key = lambda idReason: oSet.dcHitsPerId[idReason], reverse = True);
+
+ #
+ # Generate table and transition list. These are the most useful ones with the current graph machinery.
+ #
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, len(oSet.aoPeriods));
+ sHtml += self._generateTransitionList(oSet);
+
+ #
+ # Check if most of the stuff is without any assign reason, if so, skip
+ # that part of the graph so it doesn't offset the interesting bits.
+ #
+ fIncludeWithoutReason = True;
+ for oPeriod in reversed(oSet.aoPeriods):
+ if oPeriod.cWithoutReason > oSet.cMaxHits * 4:
+ fIncludeWithoutReason = False;
+ sHtml += '<p>Warning: Many failures without assigned reason!</p>\n';
+ break;
+
+ #
+ # Generate the graph.
+ #
+ fGenerateGraph = len(aidSortedRaw) <= 9 and len(aidSortedRaw) > 0; ## Make this configurable.
+ if fGenerateGraph:
+ aidSorted = aidSortedRaw;
+
+ asNames = [];
+ for idReason in aidSorted:
+ oReason = oSet.dSubjects[idReason];
+ asNames.append('%s / %s' % (oReason.oCategory.sShort, oReason.sShort,) )
+ if fIncludeWithoutReason:
+ asNames.append('No reason');
+
+ oTable = WuiHlpGraphDataTable('Period', asNames);
+
+ cMax = oSet.cMaxHits;
+ for _, oPeriod in enumerate(reversed(oSet.aoPeriods)):
+ aiValues = [];
+
+ for idReason in aidSorted:
+ oRow = oPeriod.dRowsById.get(idReason, None);
+ iValue = oRow.cHits if oRow is not None else 0;
+ aiValues.append(iValue);
+
+ if fIncludeWithoutReason:
+ aiValues.append(oPeriod.cWithoutReason);
+ if oPeriod.cWithoutReason > cMax:
+ cMax = oPeriod.cWithoutReason;
+
+ oTable.addRow(oPeriod.sDesc, aiValues);
+
+ oGraph = WuiHlpBarGraph('failure-reason', oTable, self._oDisp);
+ oGraph.setRangeMax(max(cMax + 1, 3));
+ sHtml += oGraph.renderGraph();
+ return sHtml;
+
+
+class WuiReportTestCaseFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+ sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Test Case</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestCase = oSet.dSubjects[idKey];
+ return u'<td>%s %s %s</td>' % \
+ ( WuiTestResultsForTestCaseLink(idKey, oTestCase.sName, self._dExtraTestResultsParams).toHtml(),
+ WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml(),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase,
+ dExtraParams = self._dExtraParams).toHtml(),);
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return oSubject.sName;
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Case Failures';
+ oSet = self._oModel.getTestCaseFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testcase-graph', aidSortedRaw);
+ return sHtml;
+
+
+class WuiReportTestCaseArgsFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None):
+ WuiReportFailuresWithTotalBase.__init__(self, oModel, dParams, fSubReport = fSubReport,
+ aiSortColumns = aiSortColumns, fnDPrint = fnDPrint, oDisp = oDisp);
+ self.oTestCaseCrit = TestResultFilter().aCriteria[TestResultFilter.kiTestCases] # type: FilterCriterion
+
+ @staticmethod
+ def _formatName(oTestCaseArgs):
+ """ Internal helper for formatting the testcase name. """
+ if oTestCaseArgs.sSubName:
+ sName = u'%s / %s' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.sSubName, );
+ else:
+ sName = u'%s / #%u' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.idTestCaseArgs, );
+ return sName;
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(self._formatName(oTransient.oSubject)),);
+ sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th>Test Case / Variation</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestCaseArgs = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ dParams = dict(self._dExtraTestResultsParams);
+ dParams[self.oTestCaseCrit.sVarNm] = oTestCaseArgs.idTestCase;
+ dParams[self.oTestCaseCrit.oSub.sVarNm] = idKey;
+ sHtml += WuiTestResultsForTestCaseLink(oTestCaseArgs.idTestCase, self._formatName(oTestCaseArgs), dParams).toHtml();
+ sHtml += u' ';
+ sHtml += WuiTestCaseDetailsLink(oTestCaseArgs.idTestCase).toHtml();
+ #sHtml += u' ';
+ #sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCaseArgs, oTestCaseArgs.idTestCaseArgs,
+ # sName = self._formatName(oTestCaseArgs), dExtraParams = self._dExtraParams).toHtml();
+ sHtml += u'</td>';
+ return sHtml;
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return self._formatName(oSubject);
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Case Variation Failures';
+ oSet = self._oModel.getTestCaseVariationFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testcasearg-graph', aidSortedRaw);
+ return sHtml;
+
+
+
+class WuiReportTestBoxFailures(WuiReportFailuresWithTotalBase):
+ """
+ Generates a report displaying the failure reasons over time.
+ """
+
+ def _formatEdgeOccurenceSubject(self, oTransient):
+ sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),);
+ sHtml += WuiTestBoxDetailsLinkShort(oTransient.oSubject).toHtml();
+ return sHtml;
+
+ def _formatSeriesNameColumnHeadersForTable(self):
+ return '<th colspan="5">Test Box</th>';
+
+ def _formatSeriesNameForTable(self, oSet, idKey):
+ oTestBox = oSet.dSubjects[idKey];
+ sHtml = u'<td>';
+ sHtml += WuiTestResultsForTestBoxLink(idKey, oTestBox.sName, self._dExtraTestResultsParams).toHtml()
+ sHtml += u' ';
+ sHtml += WuiTestBoxDetailsLinkShort(oTestBox).toHtml();
+ sHtml += u' ';
+ sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestBox.idTestBox,
+ dExtraParams = self._dExtraParams).toHtml();
+ sHtml += u'</td>';
+ sOsAndVer = '%s %s' % (oTestBox.sOs, oTestBox.sOsVersion.strip(),);
+ if len(sOsAndVer) < 22:
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(sOsAndVer),);
+ else: # wonder if td.title works..
+ sHtml += u'<td title="%s" width="1%%" style="white-space:nowrap;">%s...</td>' \
+ % (webutils.escapeAttr(sOsAndVer), webutils.escapeElem(sOsAndVer[:20]));
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getArchBitString()),);
+ sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getPrettyCpuVendor()),);
+ sHtml += u'<td>%s' % (oTestBox.getPrettyCpuVersion(),);
+ if oTestBox.fCpuNestedPaging: sHtml += u', np';
+ elif oTestBox.fCpuHwVirt: sHtml += u', hw';
+ else: sHtml += u', raw';
+ if oTestBox.fCpu64BitGuest: sHtml += u', 64';
+ sHtml += u'</td>';
+ return sHtml;
+
+ def _formatSeriedNameForGraph(self, oSubject):
+ return oSubject.sName;
+
+ def generateReportBody(self):
+ self._sTitle = 'Test Box Failures';
+ oSet = self._oModel.getTestBoxFailures();
+ (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet);
+
+ sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn);
+ sHtml += self._generateTransitionList(oSet);
+ sHtml += self._generateGraph(oSet, 'testbox-graph', aidSortedRaw);
+ return sHtml;
+
+
+class WuiReportSummary(WuiReportBase):
+ """
+ Summary report.
+ """
+
+ def generateReportBody(self):
+ self._sTitle = 'Summary';
+ sHtml = '<p>This will display several reports and listings useful to get an overview of %s (id=%s).</p>' \
+ % (self._oModel.sSubject, self._oModel.aidSubjects,);
+
+ aoReports = [];
+
+ aoReports.append(WuiReportSuccessRate( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportTestCaseFailures(self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ if self._oModel.sSubject == ReportModelBase.ksSubTestCase:
+ aoReports.append(WuiReportTestCaseArgsFailures(self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportTestBoxFailures( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+ aoReports.append(WuiReportFailureReasons( self._oModel, self._dParams, fSubReport = True,
+ aiSortColumns = self._aiSortColumns,
+ fnDPrint = self._fnDPrint, oDisp = self._oDisp));
+
+ for oReport in aoReports:
+ (sTitle, sContent) = oReport.show();
+ sHtml += '<br>'; # drop this layout hack
+ sHtml += '<div>';
+ sHtml += '<h3>%s</h3>\n' % (webutils.escapeElem(sTitle),);
+ sHtml += sContent;
+ sHtml += '</div>';
+
+ return sHtml;
+
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
new file mode 100755
index 00000000..1edb03d5
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
@@ -0,0 +1,965 @@
+# -*- coding: utf-8 -*-
+# $Id: wuitestresult.py $
+
+"""
+Test Manager WUI - Test Results.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+import datetime;
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentBase, WuiHtmlBase, WuiTmLink, WuiLinkBase, \
+ WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml, \
+ WuiHtmlKeeper;
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuihlpform import WuiHlpForm;
+from testmanager.webui.wuiadminfailurereason import WuiFailureReasonAddLink, WuiFailureReasonDetailsLink;
+from testmanager.webui.wuitestresultfailure import WuiTestResultFailureDetailsLink;
+from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic;
+from testmanager.core.report import ReportGraphModel, ReportModelBase;
+from testmanager.core.testbox import TestBoxData;
+from testmanager.core.testcase import TestCaseData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.testgroup import TestGroupData;
+from testmanager.core.testresultfailures import TestResultFailureData;
+from testmanager.core.build import BuildData;
+from testmanager.core import db;
+from testmanager import config;
+from common import webutils, utils;
+
+
+class WuiTestSetLink(WuiTmLink):
+ """ Test set link. """
+
+ def __init__(self, idTestSet, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False):
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: idTestSet, }, fBracketed = fBracketed);
+ self.idTestSet = idTestSet;
+
+class WuiTestResultsForSomethingLink(WuiTmLink):
+ """ Test results link for a grouping. """
+
+ def __init__(self, sGroupedBy, idGroupMember, sName = WuiContentBase.ksShortTestResultsLink,
+ dExtraParams = None, fBracketed = False):
+ dParams = dict(dExtraParams) if dExtraParams else {};
+ dParams[WuiMain.ksParamAction] = sGroupedBy;
+ dParams[WuiMain.ksParamGroupMemberId] = idGroupMember;
+ WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForTestBoxLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given testbox. """
+
+ def __init__(self, idTestBox, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestBox, idTestBox,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForTestCaseLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given testcase. """
+
+ def __init__(self, idTestCase, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestCase, idTestCase,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResultsForBuildRevLink(WuiTestResultsForSomethingLink):
+ """ Test results link for a given build revision. """
+
+ def __init__(self, iRevision, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False):
+ WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByBuildRev, iRevision,
+ sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed);
+
+
+class WuiTestResult(WuiContentBase):
+ """Display test case result"""
+
+ def __init__(self, fnDPrint = None, oDisp = None):
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+
+ # Cyclic import hacks.
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ self.oWuiAdmin = WuiAdmin;
+
+ def _toHtml(self, oObject):
+ """Translate some object to HTML."""
+ if isinstance(oObject, WuiHtmlBase):
+ return oObject.toHtml();
+ if db.isDbTimestamp(oObject):
+ return webutils.escapeElem(self.formatTsShort(oObject));
+ if db.isDbInterval(oObject):
+ return webutils.escapeElem(self.formatIntervalShort(oObject));
+ if utils.isString(oObject):
+ return webutils.escapeElem(oObject);
+ return webutils.escapeElem(str(oObject));
+
+ def _htmlTable(self, aoTableContent):
+ """Generate HTML code for table"""
+ sHtml = u' <table class="tmtbl-testresult-details" width="100%%">\n';
+
+ for aoSubRows in aoTableContent:
+ if not aoSubRows:
+ continue; # Can happen if there is no testsuit.
+ oCaption = aoSubRows[0];
+ sHtml += u' \n' \
+ u' <tr class="tmtbl-result-details-caption">\n' \
+ u' <td colspan="2">%s</td>\n' \
+ u' </tr>\n' \
+ % (self._toHtml(oCaption),);
+
+ iRow = 0;
+ for aoRow in aoSubRows[1:]:
+ iRow += 1;
+ sHtml += u' <tr class="%s">\n' % ('tmodd' if iRow & 1 else 'tmeven',);
+ if len(aoRow) == 1:
+ sHtml += u' <td class="tmtbl-result-details-subcaption" colspan="2">%s</td>\n' \
+ % (self._toHtml(aoRow[0]),);
+ else:
+ sHtml += u' <th scope="row">%s</th>\n' % (webutils.escapeElem(aoRow[0]),);
+ if len(aoRow) > 2:
+ sHtml += u' <td>%s</td>\n' % (aoRow[2](aoRow[1]),);
+ else:
+ sHtml += u' <td>%s</td>\n' % (self._toHtml(aoRow[1]),);
+ sHtml += u' </tr>\n';
+
+ sHtml += u' </table>\n';
+
+ return sHtml
+
+ def _highlightStatus(self, sStatus):
+ """Return sStatus string surrounded by HTML highlight code """
+ sTmp = '<font color=%s><b>%s</b></font>' \
+ % ('red' if sStatus == 'failure' else 'green', webutils.escapeElem(sStatus.upper()))
+ return sTmp
+
+ def _anchorAndAppendBinaries(self, sBinaries, aoRows):
+ """ Formats each binary (if any) into a row with a download link. """
+ if sBinaries is not None:
+ for sBinary in sBinaries.split(','):
+ if not webutils.hasSchema(sBinary):
+ sBinary = config.g_ksBuildBinUrlPrefix + sBinary;
+ aoRows.append([WuiLinkBase(webutils.getFilename(sBinary), sBinary, fBracketed = False),]);
+ return aoRows;
+
+
+ def _formatEventTimestampHtml(self, tsEvent, tsLog, idEvent, oTestSet):
+ """ Formats an event timestamp with a main log link. """
+ tsEvent = db.dbTimestampToZuluDatetime(tsEvent);
+ #sFormattedTimestamp = u'%04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02uZ' \
+ # % ( tsEvent.year, tsEvent.month, tsEvent.day,
+ # tsEvent.hour, tsEvent.minute, tsEvent.second,);
+ sFormattedTimestamp = u'%02u:%02u:%02uZ' \
+ % ( tsEvent.hour, tsEvent.minute, tsEvent.second,);
+ sTitle = u'#%u - %04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02u.%06uZ' \
+ % ( idEvent, tsEvent.year, tsEvent.month, tsEvent.day,
+ tsEvent.hour, tsEvent.minute, tsEvent.second, tsEvent.microsecond, );
+ tsLog = db.dbTimestampToZuluDatetime(tsLog);
+ sFragment = u'%02u_%02u_%02u_%06u' % ( tsLog.hour, tsLog.minute, tsLog.second, tsLog.microsecond);
+ return WuiTmLink(sFormattedTimestamp, '',
+ { WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: oTestSet.idTestSet, },
+ sFragmentId = sFragment, sTitle = sTitle, fBracketed = False, ).toHtml();
+
+ def _recursivelyGenerateEvents(self, oTestResult, sParentName, sLineage, iRow,
+ iFailure, oTestSet, iDepth): # pylint: disable=too-many-locals
+ """
+ Recursively generate event table rows for the result set.
+
+ oTestResult is an object of the type TestResultDataEx.
+ """
+ # Hack: Replace empty outer test result name with (pretty) command line.
+ if iRow == 1:
+ sName = '';
+ sDisplayName = sParentName;
+ else:
+ sName = oTestResult.sName if sParentName == '' else '%s, %s' % (sParentName, oTestResult.sName,);
+ sDisplayName = webutils.escapeElem(sName);
+
+ # Format error count.
+ sErrCnt = '';
+ if oTestResult.cErrors > 0:
+ sErrCnt = ' (1 error)' if oTestResult.cErrors == 1 else ' (%d errors)' % oTestResult.cErrors;
+
+ # Format bits for adding or editing the failure reason. Level 0 is handled at the top of the page.
+ sChangeReason = '';
+ if oTestResult.cErrors > 0 and iDepth > 0 and self._oDisp is not None and not self._oDisp.isReadOnlyUser():
+ dTmp = {
+ self._oDisp.ksParamAction: self._oDisp.ksActionTestResultFailureAdd if oTestResult.oReason is None else
+ self._oDisp.ksActionTestResultFailureEdit,
+ TestResultFailureData.ksParam_idTestResult: oTestResult.idTestResult,
+ };
+ sChangeReason = ' <a href="?%s" class="tmtbl-edit-reason" onclick="addRedirectToAnchorHref(this)">%s</a> ' \
+ % ( webutils.encodeUrlParams(dTmp), WuiContentBase.ksShortEditLinkHtml );
+
+ # Format the include in graph checkboxes.
+ sLineage += ':%u' % (oTestResult.idStrName,);
+ sResultGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include result in graph."/>' \
+ % (WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeResult, sLineage,);
+ sElapsedGraph = '';
+ if oTestResult.tsElapsed is not None:
+ sElapsedGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include elapsed time in graph."/>' \
+ % ( WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeElapsed, sLineage);
+
+
+ if not oTestResult.aoChildren \
+ and len(oTestResult.aoValues) + len(oTestResult.aoMsgs) + len(oTestResult.aoFiles) == 0:
+ # Leaf - single row.
+ tsEvent = oTestResult.tsCreated;
+ if oTestResult.tsElapsed is not None:
+ tsEvent += oTestResult.tsElapsed;
+ sHtml = ' <tr class="%s tmtbl-events-leaf tmtbl-events-lvl%s tmstatusrow-%s" id="S%u">\n' \
+ ' <td id="E%u">%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2"%s>%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult,
+ oTestResult.idTestResult,
+ self._formatEventTimestampHtml(tsEvent, oTestResult.tsCreated, oTestResult.idTestResult, oTestSet),
+ sElapsedGraph,
+ webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)) if oTestResult.tsElapsed is not None
+ else '',
+ sDisplayName,
+ ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
+ webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
+ sChangeReason if oTestResult.oReason is None else '',
+ sResultGraph );
+ iRow += 1;
+ else:
+ # Multiple rows.
+ sHtml = ' <tr class="%s tmtbl-events-first tmtbl-events-lvl%s ">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2">%s</td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oTestResult.tsCreated, oTestResult.tsCreated,
+ oTestResult.idTestResult, oTestSet),
+ sDisplayName,
+ 'running' if oTestResult.tsElapsed is None else '', );
+ iRow += 1;
+
+ # Depth. Check if our error count is just reflecting the one of our children.
+ cErrorsBelow = 0;
+ for oChild in oTestResult.aoChildren:
+ (sChildHtml, iRow, iFailure) = self._recursivelyGenerateEvents(oChild, sName, sLineage,
+ iRow, iFailure, oTestSet, iDepth + 1);
+ sHtml += sChildHtml;
+ cErrorsBelow += oChild.cErrors;
+
+ # Messages.
+ for oMsg in oTestResult.aoMsgs:
+ sHtml += ' <tr class="%s tmtbl-events-message tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td colspan="3">%s: %s</td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oMsg.tsCreated, oMsg.tsCreated, oMsg.idTestResultMsg, oTestSet),
+ webutils.escapeElem(oMsg.enmLevel),
+ webutils.escapeElem(oMsg.sMsg), );
+ iRow += 1;
+
+ # Values.
+ for oValue in oTestResult.aoValues:
+ sHtml += ' <tr class="%s tmtbl-events-value tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td class="tmtbl-events-number">%s</td>\n' \
+ ' <td class="tmtbl-events-unit">%s</td>\n' \
+ ' <td><input type="checkbox" name="%s" value="%s%s:%u" title="Include value in graph."></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oValue.tsCreated, oValue.tsCreated, oValue.idTestResultValue, oTestSet),
+ webutils.escapeElem(oValue.sName),
+ utils.formatNumber(oValue.lValue).replace(' ', '&nbsp;'),
+ webutils.escapeElem(oValue.sUnit),
+ WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeValue, sLineage, oValue.idStrName, );
+ iRow += 1;
+
+ # Files.
+ for oFile in oTestResult.aoFiles:
+ if oFile.sMime in [ 'text/plain', ]:
+ aoLinks = [
+ WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionViewLog,
+ self._oDisp.ksParamLogSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamLogFileId: oFile.idTestResultFile, },
+ sTitle = oFile.sDescription),
+ WuiTmLink('View Raw', '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: False, },
+ sTitle = oFile.sDescription),
+ ]
+ else:
+ aoLinks = [
+ WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: False, },
+ sTitle = oFile.sDescription),
+ ]
+ aoLinks.append(WuiTmLink('Download', '',
+ { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile,
+ self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet,
+ self._oDisp.ksParamGetFileId: oFile.idTestResultFile,
+ self._oDisp.ksParamGetFileDownloadIt: True, },
+ sTitle = oFile.sDescription));
+
+ sHtml += ' <tr class="%s tmtbl-events-file tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' <td></td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ self._formatEventTimestampHtml(oFile.tsCreated, oFile.tsCreated, oFile.idTestResultFile, oTestSet),
+ '\n'.join(oLink.toHtml() for oLink in aoLinks),);
+ iRow += 1;
+
+ # Done?
+ if oTestResult.tsElapsed is not None:
+ tsEvent = oTestResult.tsCreated + oTestResult.tsElapsed;
+ sHtml += ' <tr class="%s tmtbl-events-final tmtbl-events-lvl%s tmstatusrow-%s" id="E%d">\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2"%s>%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult,
+ self._formatEventTimestampHtml(tsEvent, tsEvent, oTestResult.idTestResult, oTestSet),
+ sElapsedGraph,
+ webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)),
+ sDisplayName,
+ ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '',
+ webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt),
+ sChangeReason if cErrorsBelow < oTestResult.cErrors and oTestResult.oReason is None else '',
+ sResultGraph);
+ iRow += 1;
+
+ # Failure reason.
+ if oTestResult.oReason is not None:
+ sReasonText = '%s / %s' % ( oTestResult.oReason.oFailureReason.oCategory.sShort,
+ oTestResult.oReason.oFailureReason.sShort, );
+ sCommentHtml = '';
+ if oTestResult.oReason.sComment and oTestResult.oReason.sComment.strip():
+ sCommentHtml = '<br>' + webutils.escapeElem(oTestResult.oReason.sComment.strip());
+ sCommentHtml = sCommentHtml.replace('\n', '<br>');
+
+ sDetailedReason = ' <a href="?%s" class="tmtbl-show-reason">%s</a>' \
+ % ( webutils.encodeUrlParams({ self._oDisp.ksParamAction:
+ self._oDisp.ksActionTestResultFailureDetails,
+ TestResultFailureData.ksParam_idTestResult:
+ oTestResult.idTestResult,}),
+ WuiContentBase.ksShortDetailsLinkHtml,);
+
+ sHtml += ' <tr class="%s tmtbl-events-reason tmtbl-events-lvl%s">\n' \
+ ' <td>%s</td>\n' \
+ ' <td colspan="2">%s</td>\n' \
+ ' <td colspan="3">%s%s%s%s</td>\n' \
+ ' <td>%s</td>\n' \
+ ' </tr>\n' \
+ % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth,
+ webutils.escapeElem(self.formatTsShort(oTestResult.oReason.tsEffective)),
+ oTestResult.oReason.oAuthor.sUsername,
+ webutils.escapeElem(sReasonText), sDetailedReason, sChangeReason,
+ sCommentHtml,
+ 'todo');
+ iRow += 1;
+
+ if oTestResult.isFailure():
+ iFailure += 1;
+
+ return (sHtml, iRow, iFailure);
+
+
+ def _generateMainReason(self, oTestResultTree, oTestSet):
+ """
+ Generates the form for displaying and updating the main failure reason.
+
+ oTestResultTree is an instance TestResultDataEx.
+ oTestSet is an instance of TestSetData.
+
+ """
+ _ = oTestSet;
+ sHtml = ' ';
+
+ if oTestResultTree.isFailure() or oTestResultTree.cErrors > 0:
+ sHtml += ' <h2>Failure Reason:</h2>\n';
+ oData = oTestResultTree.oReason;
+
+ # We need the failure reasons for the combobox.
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Test Sheriff, you figure out why!');
+ assert aoFailureReasons;
+
+ # For now we'll use the standard form helper.
+ sFormActionUrl = '%s?%s=%s' % ( self._oDisp.ksScriptName, self._oDisp.ksParamAction,
+ WuiMain.ksActionTestResultFailureAddPost if oData is None else
+ WuiMain.ksActionTestResultFailureEditPost )
+ fReadOnly = not self._oDisp or self._oDisp.isReadOnlyUser();
+ oForm = WuiHlpForm('failure-reason', sFormActionUrl,
+ sOnSubmit = WuiHlpForm.ksOnSubmit_AddReturnToFieldWithCurrentUrl, fReadOnly = fReadOnly);
+ oForm.addTextHidden(TestResultFailureData.ksParam_idTestResult, oTestResultTree.idTestResult);
+ oForm.addTextHidden(TestResultFailureData.ksParam_idTestSet, oTestSet.idTestSet);
+ if oData is not None:
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason, 'Reason',
+ aoFailureReasons,
+ sPostHtml = u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml()
+ + (u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml()
+ if not fReadOnly else u''));
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment')
+
+ oForm.addNonText(u'%s (%s), %s'
+ % ( oData.oAuthor.sUsername, oData.oAuthor.sUsername,
+ self.formatTsShort(oData.tsEffective),),
+ 'Sheriff',
+ sPostHtml = ' ' + WuiTestResultFailureDetailsLink(oData.idTestResult, "Show Details").toHtml() )
+
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective);
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire);
+ oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor);
+ oForm.addSubmit('Change Reason');
+ else:
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, -1, 'Reason', aoFailureReasons,
+ sPostHtml = ' ' + WuiFailureReasonAddLink('New').toHtml() if not fReadOnly else '');
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, '', 'Comment');
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, '');
+ oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, '');
+ oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, '');
+ oForm.addSubmit('Add Reason');
+
+ sHtml += oForm.finalize();
+ return sHtml;
+
+
+ def showTestCaseResultDetails(self, # pylint: disable=too-many-locals,too-many-statements
+ oTestResultTree,
+ oTestSet,
+ oBuildEx,
+ oValidationKitEx,
+ oTestBox,
+ oTestGroup,
+ oTestCaseEx,
+ oTestVarEx):
+ """Show detailed result"""
+ def getTcDepsHtmlList(aoTestCaseData):
+ """Get HTML <ul> list of Test Case name items"""
+ if aoTestCaseData:
+ sTmp = '<ul>'
+ for oTestCaseData in aoTestCaseData:
+ sTmp += '<li>%s</li>' % (webutils.escapeElem(oTestCaseData.sName),);
+ sTmp += '</ul>'
+ else:
+ sTmp = 'No items'
+ return sTmp
+
+ def getGrDepsHtmlList(aoGlobalResourceData):
+ """Get HTML <ul> list of Global Resource name items"""
+ if aoGlobalResourceData:
+ sTmp = '<ul>'
+ for oGlobalResourceData in aoGlobalResourceData:
+ sTmp += '<li>%s</li>' % (webutils.escapeElem(oGlobalResourceData.sName),);
+ sTmp += '</ul>'
+ else:
+ sTmp = 'No items'
+ return sTmp
+
+
+ asHtml = []
+
+ from testmanager.webui.wuireport import WuiReportSummaryLink;
+ tsReportEffectiveDate = None;
+ if oTestSet.tsDone is not None:
+ tsReportEffectiveDate = oTestSet.tsDone + datetime.timedelta(days = 4);
+ if tsReportEffectiveDate >= self.getNowTs():
+ tsReportEffectiveDate = None;
+
+ # Test result + test set details.
+ aoResultRows = [
+ WuiHtmlKeeper([ WuiTmLink(oTestCaseEx.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: oTestCaseEx.idTestCase,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, },
+ fBracketed = False),
+ WuiTestResultsForTestCaseLink(oTestCaseEx.idTestCase),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCaseEx.idTestCase,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]),
+ ];
+ if oTestCaseEx.sDescription:
+ aoResultRows.append([oTestCaseEx.sDescription,]);
+ aoResultRows.append([ 'Status:', WuiRawHtml('<span class="tmspan-status-%s">%s</span>'
+ % (oTestResultTree.enmStatus, oTestResultTree.enmStatus,))]);
+ if oTestResultTree.cErrors > 0:
+ aoResultRows.append(( 'Errors:', oTestResultTree.cErrors ));
+ aoResultRows.append([ 'Elapsed:', oTestResultTree.tsElapsed ]);
+ cSecCfgTimeout = oTestCaseEx.cSecTimeout if oTestVarEx.cSecTimeout is None else oTestVarEx.cSecTimeout;
+ cSecEffTimeout = cSecCfgTimeout * oTestBox.pctScaleTimeout / 100;
+ aoResultRows.append([ 'Timeout:',
+ '%s (%s sec)' % (utils.formatIntervalSeconds2(cSecEffTimeout), cSecEffTimeout,) ]);
+ if cSecEffTimeout != cSecCfgTimeout:
+ aoResultRows.append([ 'Cfg Timeout:',
+ '%s (%s sec)' % (utils.formatIntervalSeconds(cSecCfgTimeout), cSecCfgTimeout,) ]);
+ aoResultRows += [
+ ( 'Started:', WuiTmLink(self.formatTsShort(oTestSet.tsCreated), WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped,
+ WuiMain.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False) ),
+ ];
+ if oTestSet.tsDone is not None:
+ aoResultRows += [ ( 'Done:',
+ WuiTmLink(self.formatTsShort(oTestSet.tsDone), WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped,
+ WuiMain.ksParamEffectiveDate: oTestSet.tsDone, },
+ fBracketed = False) ) ];
+ else:
+ aoResultRows += [( 'Done:', 'Still running...')];
+ aoResultRows += [( 'Config:', oTestSet.tsConfig )];
+ if oTestVarEx.cGangMembers > 1:
+ aoResultRows.append([ 'Member No:', '#%s (of %s)' % (oTestSet.iGangMemberNo, oTestVarEx.cGangMembers) ]);
+
+ aoResultRows += [
+ ( 'Test Group:',
+ WuiHtmlKeeper([ WuiTmLink(oTestGroup.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestGroupDetails,
+ TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestGroup, oTestGroup.idTestGroup,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]), ),
+ ];
+ if oTestVarEx.sTestBoxReqExpr is not None:
+ aoResultRows.append([ 'TestBox reqs:', oTestVarEx.sTestBoxReqExpr ]);
+ elif oTestCaseEx.sTestBoxReqExpr is not None or oTestVarEx.sTestBoxReqExpr is not None:
+ aoResultRows.append([ 'TestBox reqs:', oTestCaseEx.sTestBoxReqExpr ]);
+ if oTestVarEx.sBuildReqExpr is not None:
+ aoResultRows.append([ 'Build reqs:', oTestVarEx.sBuildReqExpr ]);
+ elif oTestCaseEx.sBuildReqExpr is not None or oTestVarEx.sBuildReqExpr is not None:
+ aoResultRows.append([ 'Build reqs:', oTestCaseEx.sBuildReqExpr ]);
+ if oTestCaseEx.sValidationKitZips is not None and oTestCaseEx.sValidationKitZips != '@VALIDATIONKIT_ZIP@':
+ aoResultRows.append([ 'Validation Kit:', oTestCaseEx.sValidationKitZips ]);
+ if oTestCaseEx.aoDepTestCases:
+ aoResultRows.append([ 'Prereq. Test Cases:', oTestCaseEx.aoDepTestCases, getTcDepsHtmlList ]);
+ if oTestCaseEx.aoDepGlobalResources:
+ aoResultRows.append([ 'Global Resources:', oTestCaseEx.aoDepGlobalResources, getGrDepsHtmlList ]);
+
+ # Builds.
+ aoBuildRows = [];
+ if oBuildEx is not None:
+ aoBuildRows += [
+ WuiHtmlKeeper([ WuiTmLink('Build', self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oBuildEx.idBuild,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False),
+ WuiTestResultsForBuildRevLink(oBuildEx.iRevision),
+ WuiReportSummaryLink(ReportModelBase.ksSubBuild, oBuildEx.idBuild,
+ tsNow = tsReportEffectiveDate, fBracketed = False), ]),
+ ];
+ self._anchorAndAppendBinaries(oBuildEx.sBinaries, aoBuildRows);
+ aoBuildRows += [
+ ( 'Revision:', WuiSvnLinkWithTooltip(oBuildEx.iRevision, oBuildEx.oCat.sRepository,
+ fBracketed = False) ),
+ ( 'Product:', oBuildEx.oCat.sProduct ),
+ ( 'Branch:', oBuildEx.oCat.sBranch ),
+ ( 'Type:', oBuildEx.oCat.sType ),
+ ( 'Version:', oBuildEx.sVersion ),
+ ( 'Created:', oBuildEx.tsCreated ),
+ ];
+ if oBuildEx.uidAuthor is not None:
+ aoBuildRows += [ ( 'Author ID:', oBuildEx.uidAuthor ), ];
+ if oBuildEx.sLogUrl is not None:
+ aoBuildRows += [ ( 'Log:', WuiBuildLogLink(oBuildEx.sLogUrl, fBracketed = False) ), ];
+
+ aoValidationKitRows = [];
+ if oValidationKitEx is not None:
+ aoValidationKitRows += [
+ WuiTmLink('Validation Kit', self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oValidationKitEx.idBuild,
+ self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, },
+ fBracketed = False),
+ ];
+ self._anchorAndAppendBinaries(oValidationKitEx.sBinaries, aoValidationKitRows);
+ aoValidationKitRows += [ ( 'Revision:', WuiSvnLink(oValidationKitEx.iRevision, fBracketed = False) ) ];
+ if oValidationKitEx.oCat.sProduct != 'VBox TestSuite':
+ aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sProduct ), ];
+ if oValidationKitEx.oCat.sBranch != 'trunk':
+ aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sBranch ), ];
+ if oValidationKitEx.oCat.sType != 'release':
+ aoValidationKitRows += [ ( 'Type:', oValidationKitEx.oCat.sType), ];
+ if oValidationKitEx.sVersion != '0.0.0':
+ aoValidationKitRows += [ ( 'Version:', oValidationKitEx.sVersion ), ];
+ aoValidationKitRows += [
+ ( 'Created:', oValidationKitEx.tsCreated ),
+ ];
+ if oValidationKitEx.uidAuthor is not None:
+ aoValidationKitRows += [ ( 'Author ID:', oValidationKitEx.uidAuthor ), ];
+ if oValidationKitEx.sLogUrl is not None:
+ aoValidationKitRows += [ ( 'Log:', WuiBuildLogLink(oValidationKitEx.sLogUrl, fBracketed = False) ), ];
+
+ # TestBox.
+ aoTestBoxRows = [
+ WuiHtmlKeeper([ WuiTmLink(oTestBox.sName, self.oWuiAdmin.ksScriptName,
+ { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idGenTestBox: oTestSet.idGenTestBox, },
+ fBracketed = False),
+ WuiTestResultsForTestBoxLink(oTestBox.idTestBox),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestSet.idTestBox,
+ tsNow = tsReportEffectiveDate, fBracketed = False),
+ ]),
+ ];
+ if oTestBox.sDescription:
+ aoTestBoxRows.append([oTestBox.sDescription, ]);
+ aoTestBoxRows += [
+ ( 'IP:', oTestBox.ip ),
+ #( 'UUID:', oTestBox.uuidSystem ),
+ #( 'Enabled:', oTestBox.fEnabled ),
+ #( 'Lom Kind:', oTestBox.enmLomKind ),
+ #( 'Lom IP:', oTestBox.ipLom ),
+ ( 'OS/Arch:', '%s.%s' % (oTestBox.sOs, oTestBox.sCpuArch) ),
+ ( 'OS Version:', oTestBox.sOsVersion ),
+ ( 'CPUs:', oTestBox.cCpus ),
+ ];
+ if oTestBox.sCpuName is not None:
+ aoTestBoxRows.append(['CPU Name', oTestBox.sCpuName.replace(' ', ' ')]);
+ if oTestBox.lCpuRevision is not None:
+ sMarch = oTestBox.queryCpuMicroarch();
+ if sMarch is not None:
+ aoTestBoxRows.append( ('CPU Microarch', sMarch) );
+ uFamily = oTestBox.getCpuFamily();
+ uModel = oTestBox.getCpuModel();
+ uStepping = oTestBox.getCpuStepping();
+ aoTestBoxRows += [
+ ( 'CPU Family', '%u (%#x)' % ( uFamily, uFamily, ) ),
+ ( 'CPU Model', '%u (%#x)' % ( uModel, uModel, ) ),
+ ( 'CPU Stepping', '%u (%#x)' % ( uStepping, uStepping, ) ),
+ ];
+ asFeatures = [ oTestBox.sCpuVendor, ];
+ if oTestBox.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt');
+ if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ aoTestBoxRows += [
+ ( 'Features:', u' '.join(asFeatures) ),
+ ( 'RAM size:', '%s MB' % (oTestBox.cMbMemory,) ),
+ ( 'Scratch Size:', '%s MB' % (oTestBox.cMbScratch,) ),
+ ( 'Scale Timeout:', '%s%%' % (oTestBox.pctScaleTimeout,) ),
+ ( 'Script Rev:', WuiSvnLink(oTestBox.iTestBoxScriptRev, fBracketed = False) ),
+ ( 'Python:', oTestBox.formatPythonVersion() ),
+ ( 'Pending Command:', oTestBox.enmPendingCmd ),
+ ];
+
+ aoRows = [
+ aoResultRows,
+ aoBuildRows,
+ aoValidationKitRows,
+ aoTestBoxRows,
+ ];
+
+ asHtml.append(self._htmlTable(aoRows));
+
+ #
+ # Convert the tree to a list of events, values, message and files.
+ #
+ sHtmlEvents = '';
+ sHtmlEvents += '<table class="tmtbl-events" id="tmtbl-events" width="100%">\n';
+ sHtmlEvents += ' <tr class="tmheader">\n' \
+ ' <th>When</th>\n' \
+ ' <th></th>\n' \
+ ' <th>Elapsed</th>\n' \
+ ' <th>Event name</th>\n' \
+ ' <th colspan="2">Value (status)</th>' \
+ ' <th></th>\n' \
+ ' </tr>\n';
+ sPrettyCmdLine = '&nbsp;\\<br>&nbsp;&nbsp;&nbsp;&nbsp;\n'.join(webutils.escapeElem(oTestCaseEx.sBaseCmd
+ + ' '
+ + oTestVarEx.sArgs).split() );
+ (sTmp, _, cFailures) = self._recursivelyGenerateEvents(oTestResultTree, sPrettyCmdLine, '', 1, 0, oTestSet, 0);
+ sHtmlEvents += sTmp;
+
+ sHtmlEvents += '</table>\n'
+
+ #
+ # Put it all together.
+ #
+ sHtml = '<table class="tmtbl-testresult-details-base" width="100%">\n';
+ sHtml += ' <tr>\n'
+ sHtml += ' <td valign="top" width="20%%">\n%s\n</td>\n' % ' <br>\n'.join(asHtml);
+
+ sHtml += ' <td valign="top" width="80%" style="padding-left:6px">\n';
+ sHtml += self._generateMainReason(oTestResultTree, oTestSet);
+
+ sHtml += ' <h2>Events:</h2>\n';
+ sHtml += ' <form action="#" method="get" id="graph-form">\n' \
+ ' <input type="hidden" name="%s" value="%s"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ ' <input type="hidden" name="%s" value="%u"/>\n' \
+ % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz,
+ WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox,
+ WuiMain.ksParamGraphWizBuildCatIds, oBuildEx.idBuildCategory,
+ WuiMain.ksParamGraphWizTestCaseIds, oTestSet.idTestCase,
+ WuiMain.ksParamGraphWizSrcTestSetId, oTestSet.idTestSet,
+ );
+ if oTestSet.tsDone is not None:
+ sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \
+ % ( WuiMain.ksParamEffectiveDate, oTestSet.tsDone, );
+ sHtml += ' <p>\n';
+ sFormButton = '<button type="submit" onclick="%s">Show graphs</button>' \
+ % ( webutils.escapeAttr('addDynamicGraphInputs("graph-form", "main", "%s", "%s");'
+ % (WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizDpi, )) );
+ sHtml += ' ' + sFormButton + '\n';
+ sHtml += ' %s %s %s\n' \
+ % ( WuiTmLink('Log File', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionViewLog,
+ WuiMain.ksParamLogSetId: oTestSet.idTestSet,
+ }),
+ WuiTmLink('Raw Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: oTestSet.idTestSet,
+ WuiMain.ksParamGetFileDownloadIt: False,
+ }),
+ WuiTmLink('Download Log', '',
+ { WuiMain.ksParamAction: WuiMain.ksActionGetFile,
+ WuiMain.ksParamGetFileSetId: oTestSet.idTestSet,
+ WuiMain.ksParamGetFileDownloadIt: True,
+ }),
+ );
+ sHtml += ' </p>\n';
+ if cFailures == 1:
+ sHtml += ' <p>%s</p>\n' % ( WuiTmLink('Jump to failure', '#failure-0'), )
+ elif cFailures > 1:
+ sHtml += ' <p>Jump to failure: ';
+ if cFailures <= 13:
+ for iFailure in range(0, cFailures):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ else:
+ for iFailure in range(0, 6):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ sHtml += ' ... ';
+ for iFailure in range(cFailures - 6, cFailures):
+ sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml();
+ sHtml += ' </p>\n';
+
+ sHtml += sHtmlEvents;
+ sHtml += ' <p>' + sFormButton + '</p>\n';
+ sHtml += ' </form>\n';
+ sHtml += ' </td>\n';
+
+ sHtml += ' </tr>\n';
+ sHtml += '</table>\n';
+
+ return ('Test Case result details', sHtml)
+
+
+class WuiGroupedResultList(WuiListContentBase):
+ """
+ WUI results content generator.
+ """
+
+ def __init__(self, aoEntries, cEntriesCount, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp,
+ aiSelectedSortColumns = None):
+ """Override initialization"""
+ WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective,
+ sTitle = 'Ungrouped (%d)' % cEntriesCount, sId = 'results',
+ fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
+
+ self._cEntriesCount = cEntriesCount
+
+ self._asColumnHeaders = [
+ 'Start',
+ 'Product Build',
+ 'Kit',
+ 'Box',
+ 'OS.Arch',
+ 'Test Case',
+ 'Elapsed',
+ 'Result',
+ 'Reason',
+ ];
+ self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', 'align="center"', 'align="center"',
+ 'align="center"', ];
+
+
+ # Prepare parameter lists.
+ self._dTestBoxLinkParams = self._oDisp.getParameters();
+ self._dTestBoxLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestBox;
+
+ self._dTestCaseLinkParams = self._oDisp.getParameters();
+ self._dTestCaseLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestCase;
+
+ self._dRevLinkParams = self._oDisp.getParameters();
+ self._dRevLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByBuildRev;
+
+
+
+ def _formatListEntry(self, iEntry):
+ """
+ Format *show all* table entry
+ """
+ oEntry = self._aoEntries[iEntry];
+
+ from testmanager.webui.wuiadmin import WuiAdmin;
+ from testmanager.webui.wuireport import WuiReportSummaryLink;
+
+ oValidationKit = None;
+ if oEntry.idBuildTestSuite is not None:
+ oValidationKit = WuiTmLink('r%s' % (oEntry.iRevisionTestSuite,),
+ WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuildTestSuite },
+ fBracketed = False);
+
+ aoTestSetLinks = [];
+ aoTestSetLinks.append(WuiTmLink(oEntry.enmStatus,
+ WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.idTestSet },
+ fBracketed = False));
+ if oEntry.cErrors > 0:
+ aoTestSetLinks.append(WuiRawHtml('-'));
+ aoTestSetLinks.append(WuiTmLink('%d error%s' % (oEntry.cErrors, '' if oEntry.cErrors == 1 else 's', ),
+ WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails,
+ TestSetData.ksParam_idTestSet: oEntry.idTestSet },
+ sFragmentId = 'failure-0', fBracketed = False));
+
+
+ self._dTestBoxLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestBox;
+ self._dTestCaseLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestCase;
+ self._dRevLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.iRevision;
+
+ sTestBoxTitle = u'';
+ if oEntry.sCpuVendor is not None:
+ sTestBoxTitle += 'CPU vendor:\t%s\n' % ( oEntry.sCpuVendor, );
+ if oEntry.sCpuName is not None:
+ sTestBoxTitle += 'CPU name:\t%s\n' % ( ' '.join(oEntry.sCpuName.split()), );
+ if oEntry.sOsVersion is not None:
+ sTestBoxTitle += 'OS version:\t%s\n' % ( oEntry.sOsVersion, );
+ asFeatures = [];
+ if oEntry.fCpuHwVirt is True:
+ if oEntry.sCpuVendor is None:
+ asFeatures.append(u'HW\u2011Virt');
+ elif oEntry.sCpuVendor in ['AuthenticAMD',]:
+ asFeatures.append(u'HW\u2011Virt(AMD\u2011V)');
+ else:
+ asFeatures.append(u'HW\u2011Virt(VT\u2011x)');
+ if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging');
+ if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest');
+ #if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU');
+ sTestBoxTitle += u'CPU features:\t' + u', '.join(asFeatures);
+
+ # Testcase
+ if oEntry.sSubName:
+ sTestCaseName = '%s / %s' % (oEntry.sTestCaseName, oEntry.sSubName,);
+ else:
+ sTestCaseName = oEntry.sTestCaseName;
+
+ # Reason:
+ aoReasons = [];
+ for oIt in oEntry.aoFailureReasons:
+ sReasonTitle = 'Reason: \t%s\n' % ( oIt.oFailureReason.sShort, );
+ sReasonTitle += 'Category:\t%s\n' % ( oIt.oFailureReason.oCategory.sShort, );
+ sReasonTitle += 'Assigned:\t%s\n' % ( self.formatTsShort(oIt.tsFailureReasonAssigned), );
+ sReasonTitle += 'By User: \t%s\n' % ( oIt.oFailureReasonAssigner.sUsername, );
+ if oIt.sFailureReasonComment:
+ sReasonTitle += 'Comment: \t%s\n' % ( oIt.sFailureReasonComment, );
+ if oIt.oFailureReason.iTicket is not None and oIt.oFailureReason.iTicket > 0:
+ sReasonTitle += 'xTracker:\t#%s\n' % ( oIt.oFailureReason.iTicket, );
+ for i, sUrl in enumerate(oIt.oFailureReason.asUrls):
+ sUrl = sUrl.strip();
+ if sUrl:
+ sReasonTitle += 'URL#%u: \t%s\n' % ( i, sUrl, );
+ aoReasons.append(WuiTmLink(oIt.oFailureReason.sShort, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails,
+ FailureReasonData.ksParam_idFailureReason: oIt.oFailureReason.idFailureReason },
+ sTitle = sReasonTitle));
+
+ return [
+ oEntry.tsCreated,
+ [ WuiTmLink('%s %s (%s)' % (oEntry.sProduct, oEntry.sVersion, oEntry.sType,),
+ WuiMain.ksScriptName, self._dRevLinkParams, sTitle = '%s' % (oEntry.sBranch,), fBracketed = False),
+ WuiSvnLinkWithTooltip(oEntry.iRevision, 'vbox'), ## @todo add sRepository TestResultListingData
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails,
+ BuildData.ksParam_idBuild: oEntry.idBuild },
+ fBracketed = False),
+ ],
+ oValidationKit,
+ [ WuiTmLink(oEntry.sTestBoxName, WuiMain.ksScriptName, self._dTestBoxLinkParams, fBracketed = False,
+ sTitle = sTestBoxTitle),
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails,
+ TestBoxData.ksParam_idTestBox: oEntry.idTestBox },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oEntry.idTestBox, fBracketed = False), ],
+ '%s.%s' % (oEntry.sOs, oEntry.sArch),
+ [ WuiTmLink(sTestCaseName, WuiMain.ksScriptName, self._dTestCaseLinkParams, fBracketed = False,
+ sTitle = (oEntry.sBaseCmd + ' ' + oEntry.sArgs) if oEntry.sArgs else oEntry.sBaseCmd),
+ WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName,
+ { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails,
+ TestCaseData.ksParam_idTestCase: oEntry.idTestCase },
+ fBracketed = False),
+ WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oEntry.idTestCase, fBracketed = False), ],
+ oEntry.tsElapsed,
+ aoTestSetLinks,
+ aoReasons
+ ];
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
new file mode 100755
index 00000000..9ffd5518
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# $Id: wuitestresultfailure.py $
+
+"""
+Test Manager WUI - Dummy Test Result Failure Reason Edit Dialog - just for error handling!
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Validation Kit imports.
+from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiTmLink;
+from testmanager.webui.wuimain import WuiMain;
+from testmanager.webui.wuiadminfailurereason import WuiFailureReasonDetailsLink, WuiFailureReasonAddLink;
+from testmanager.core.testresultfailures import TestResultFailureData;
+from testmanager.core.testset import TestSetData;
+from testmanager.core.failurereason import FailureReasonLogic;
+
+
+
+class WuiTestResultFailureDetailsLink(WuiTmLink):
+ """ Link for adding a failure reason. """
+ def __init__(self, idTestResult, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None):
+ if fBracketed is None:
+ fBracketed = len(sName) > 2;
+ WuiTmLink.__init__(self, sName = sName,
+ sUrlBase = WuiMain.ksScriptName,
+ dParams = { WuiMain.ksParamAction: WuiMain.ksActionTestResultFailureDetails,
+ TestResultFailureData.ksParam_idTestResult: idTestResult, },
+ fBracketed = fBracketed,
+ sTitle = sTitle);
+ self.idTestResult = idTestResult;
+
+
+
+class WuiTestResultFailure(WuiFormContentBase):
+ """
+ WUI test result failure error form generator.
+ """
+ def __init__(self, oData, sMode, oDisp):
+ if sMode == WuiFormContentBase.ksMode_Add:
+ sTitle = 'Add Test Result Failure Reason';
+ elif sMode == WuiFormContentBase.ksMode_Edit:
+ sTitle = 'Modify Test Result Failure Reason';
+ else:
+ assert sMode == WuiFormContentBase.ksMode_Show;
+ sTitle = 'Test Result Failure Reason';
+ ## submit access.
+ WuiFormContentBase.__init__(self, oData, sMode, 'TestResultFailure', oDisp, sTitle);
+
+ def _populateForm(self, oForm, oData):
+
+ aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why');
+ sPostHtml = '';
+ if oData.idFailureReason is not None and oData.idFailureReason >= 0:
+ sPostHtml += u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml();
+ sPostHtml += u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml();
+ oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason,
+ 'Reason', aoFailureReasons, sPostHtml = sPostHtml);
+ oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment');
+ oForm.addIntRO( TestResultFailureData.ksParam_idTestResult, oData.idTestResult, 'Test Result ID');
+ oForm.addIntRO( TestResultFailureData.ksParam_idTestSet, oData.idTestSet, 'Test Set ID');
+ oForm.addTimestampRO(TestResultFailureData.ksParam_tsEffective, oData.tsEffective, 'Effective Date');
+ oForm.addTimestampRO(TestResultFailureData.ksParam_tsExpire, oData.tsExpire, 'Expire (excl)');
+ oForm.addIntRO( TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID');
+ if self._sMode != WuiFormContentBase.ksMode_Show:
+ oForm.addSubmit('Add' if self._sMode == WuiFormContentBase.ksMode_Add else 'Modify');
+ return True;
+
+ def _generateTopRowFormActions(self, oData):
+ """
+ We add a way to get back to the test set to the actions.
+ """
+ aoActions = super(WuiTestResultFailure, self)._generateTopRowFormActions(oData);
+ if oData and oData.idTestResult and int(oData.idTestResult) > 0:
+ aoActions.append(WuiTmLink('Associated Test Set', WuiMain.ksScriptName,
+ { WuiMain.ksParamAction: WuiMain.ksActionTestSetDetailsFromResult,
+ TestSetData.ksParam_idTestResult: oData.idTestResult }
+ ));
+ return aoActions;
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py
new file mode 100755
index 00000000..1df9bbb2
--- /dev/null
+++ b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# $Id: wuivcshistory.py $
+
+"""
+Test Manager WUI - VCS history
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-2023 Oracle and/or its affiliates.
+
+This file is part of VirtualBox base platform packages, as
+available from https://www.virtualbox.org.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, in version 3 of the
+License.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, see <https://www.gnu.org/licenses>.
+
+The contents of this file may alternatively be used under the terms
+of the Common Development and Distribution License Version 1.0
+(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+in the VirtualBox distribution, in which case the provisions of the
+CDDL are applicable instead of those of the GPL.
+
+You may elect to license modified versions of this file under the
+terms and conditions of either the GPL or the CDDL or both.
+
+SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+"""
+__version__ = "$Revision: 155244 $"
+
+# Python imports.
+#import datetime;
+
+# Validation Kit imports.
+from testmanager import config;
+from testmanager.core import db;
+from testmanager.webui.wuicontentbase import WuiContentBase;
+from common import webutils;
+
+class WuiVcsHistoryTooltip(WuiContentBase):
+ """
+ WUI VCS history tooltip generator.
+ """
+
+ def __init__(self, aoEntries, sRepository, iRevision, cEntries, fnDPrint, oDisp):
+ """Override initialization"""
+ WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
+ self.aoEntries = aoEntries;
+ self.sRepository = sRepository;
+ self.iRevision = iRevision;
+ self.cEntries = cEntries;
+
+
+ def show(self):
+ """
+ Generates the tooltip.
+ Returns (sTitle, HTML).
+ """
+ sHtml = '<div class="tmvcstimeline tmvcstimelinetooltip">\n';
+
+ oCurDate = None;
+ for oEntry in self.aoEntries:
+ oTsZulu = db.dbTimestampToZuluDatetime(oEntry.tsCreated);
+ if oCurDate is None or oCurDate != oTsZulu.date():
+ if oCurDate is not None:
+ sHtml += ' </dl>\n'
+ oCurDate = oTsZulu.date();
+ sHtml += ' <h2>%s:</h2>\n' \
+ ' <dl>\n' \
+ % (oTsZulu.strftime('%Y-%m-%d'),);
+
+ sEntry = ' <dt id="r%s">' % (oEntry.iRevision, );
+ sEntry += '<a href="%s" target="_blank">' \
+ % ( webutils.escapeAttr(config.g_ksTracChangsetUrlFmt
+ % { 'iRevision': oEntry.iRevision, 'sRepository': oEntry.sRepository,}), );
+
+ sEntry += '<span class="tmvcstimeline-time">%s</span>' % ( oTsZulu.strftime('%H:%MZ'), );
+ sEntry += ' Changeset <span class="tmvcstimeline-rev">[%s]</span>' % ( oEntry.iRevision, );
+ sEntry += ' by <span class="tmvcstimeline-author">%s</span>' % ( webutils.escapeElem(oEntry.sAuthor), );
+ sEntry += '</a>\n';
+ sEntry += '</dt>\n';
+ sEntry += ' <dd>%s</dd>\n' % ( webutils.escapeElem(oEntry.sMessage), );
+
+ sHtml += sEntry;
+
+ if oCurDate is not None:
+ sHtml += ' </dl>\n';
+ sHtml += '</div>\n';
+
+ return ('VCS History Tooltip', sHtml);
+