diff options
Diffstat (limited to 'src/VBox/ValidationKit/testmanager/webui')
36 files changed, 12396 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..4a9fbedc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk @@ -0,0 +1,37 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-2019 Oracle Corporation +# +# This file is part of VirtualBox Open Source Edition (OSE), as +# available from http://www.virtualbox.org. This file is free software; +# you can redistribute it and/or modify it under the terms of the GNU +# General Public License (GPL) as published by the Free Software +# Foundation, in version 2 as it comes in the "COPYING" file of the +# VirtualBox OSE distribution. VirtualBox OSE is distributed in the +# hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL) only, as it comes in the "COPYING.CDDL" file of the +# VirtualBox OSE 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. +# + +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..1c2f4fe7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# $Id: __init__.py $ + +""" +TestBox Script - WUI Presentation. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + 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..84629b7e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template-details.html @@ -0,0 +1,44 @@ +<!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/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..3c347ad6 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.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/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..ac9f8ad9 --- /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"> +@@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..58712418 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template.html @@ -0,0 +1,64 @@ +<!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" /> + <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 © 2012-2017 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..5ee3785b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py @@ -0,0 +1,1172 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmin.py $ + +""" +Test Manager Core - WUI - Admin Main page. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# Standard python imports. +import cgitb; +import sys; + +# Validation Kit imports. +from common import 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' + + 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'; + ## @} + + def __init__(self, oSrvGlue): # pylint: disable=too-many-locals,too-many-statements + WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName); + self._sTemplate = 'template.html'; + + + # + # System Log actions. + # + self._dDispatch[self.ksActionSystemChangelogList] = self._actionSystemChangelogList; + self._dDispatch[self.ksActionSystemLogList] = self._actionSystemLogList; + + # + # 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 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; + + + # + # 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 ], + [ '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 ], + [ '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 wrapper. """ + 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 = 300); + 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) + + # 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 _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),); + 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=C0103 + """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..d2a37aa4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuild.py $ + +""" +Test Manager WUI - Builds. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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. + """ + + 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..ee1859d4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildblacklist.py $ + +""" +Test Manager WUI - Build Blacklist. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..798db2f7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildcategory.py $ + +""" +Test Manager WUI - Build categories. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..27a4830d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildsource.py $ + +""" +Test Manager WUI - Build Sources. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..8a3300f0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminfailurecategory.py $ + +""" +Test Manager WUI - Failure Categories Web content generator. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..b9914b19 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminfailurereason.py $ + +""" +Test Manager WUI - Failure Reasons Web content generator. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..1fc7039f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminglobalrsrc.py $ + +""" +Test Manager WUI - Global resources. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 dict()) + + 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..0adfdd72 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminschedgroup.py $ + +""" +Test Manager WUI - Scheduling groups. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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 TestBoxData; +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink + + +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. + self._aoAllTestGroups = TestGroupLogic(oDisp.getDb()).getAll(); + + + def _populateForm(self, oForm, oData): + """ + 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._aoAllTestGroups, 'Test groups', + 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] + + 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 oTestBox in oEntry.aoTestBoxes: + aoTestBoxes.append(WuiTmLink(oTestBox.sName, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails, + TestBoxData.ksParam_idTestBox: oTestBox.idTestBox, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }, + sTitle = '#%s - %s / %s - %s.%s (%s)' + % (oTestBox.idTestBox, oTestBox.ip, oTestBox.uuidSystem, oTestBox.sOs, + oTestBox.sCpuArch, oTestBox.sOsVersion,))); + + + # 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/wuiadminsystemchangelog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py new file mode 100755 index 00000000..648d26e0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminsystemchangelog.py $ + +""" +Test Manager WUI - Admin - System changelog. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +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/wuiadminsystemlog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py new file mode 100755 index 00000000..23901218 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminsystemlog.py $ + +""" +Test Manager WUI - Admin - System Log. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..949c1e2a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py @@ -0,0 +1,401 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestbox.py $ + +""" +Test Manager WUI - TestBox. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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 WuiTestBoxDetailsLink(WuiTmLink): + """ Test box details link by ID. """ + + def __init__(self, idTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = 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); + self.idTestBox = idTestBox; + + + +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'); + # 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=R0914 + 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=R0204 + { 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 == 'linux' or oEntry.sOs == '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..6ec397ab --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestcase.py $ + +""" +Test Manager WUI - Test Cases. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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..17fa87cd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestgroup.py $ + +""" +Test Manager WUI - Test Groups. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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..8485ff5e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminuseraccount.py $ + +""" +Test Manager WUI - User accounts. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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..25e49898 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuibase.py @@ -0,0 +1,1214 @@ +# -*- coding: utf-8 -*- +# $Id: wuibase.py $ + +""" +Test Manager Web-UI - Base Classes. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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; + + +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'; + + ## @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 = dict(); + for sKey, sValue in oSrvGlue.getParameters().iteritems(): + 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) \ + or False: + # 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) \ + or False: + # 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) \ + or False: + ## 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/>' \ + + (unicode(self._sDebug, errors='ignore') if isinstance(self._sDebug, str) + else self._sDebug) \ + + u'</div>\n'; + + # + # Load the template. + # + oFile = open(os.path.join(self._oSrvGlue.pathTmWebUI(), self._sTemplate)); + sTmpl = oFile.read(); + oFile.close(); + + # + # 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 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 == 'True' or sValue == 'true' or sValue == '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=R0914 + """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 = True if idTestCaseArgs in aiCheckedTestCaseArgs else False + + # 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, + True if idTestCase in aiSelectedTestCaseIds else False, + sTestCaseName, + oListEntryTestCaseArgs) + + aoListOfTestCases.append(oListEntryTestCase) + + if 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:]); + 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" type="get" action="#">\n'; + + for sKey, oValue in self._dParams.iteritems(): + 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 = 300); + 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(); + + 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=R0914 + """ + 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] = dParams[sKey][idxItem].decode('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 and True: + 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..e9737cb6 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py @@ -0,0 +1,1119 @@ +# -*- coding: utf-8 -*- +# $Id: wuicontentbase.py $ + +""" +Test Manager Web-UI - Content Base Classes. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# Standard python imports. +import copy; +import sys; + +# Validation Kit imports. +from common import 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=R0903 + """ + Base class for HTML objects. + """ + + def __init__(self): + """Dummy init to shut up pylint.""" + 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=R0903 + """ + 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)); + + +class WuiTmLink(WuiLinkBase): # pylint: disable=R0903 + """ Local link to the test manager. """ + + kdDbgParams = None; + + 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=R0903 + """ 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 = dict(); + 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=R0903 + """ 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 = dict(); + 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=R0903 + """ + 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=R0903 + """ + 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=R0903 + """ + 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=R0903 + """ + 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=R0903 + """ + 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=R0903 + """ + Outputs the given element text. + """ + def __init__(self, sText): + WuiRawHtml.__init__(self, webutils.escapeElem(sText)); + + +class WuiContentBase(object): # pylint: disable=R0903 + """ + Base for the content classes. + """ + + ## The text/symbol for a very short add link. + ksShortAddLink = u'\u2795' + ## HTML hex entity string for ksShortAddLink. + ksShortAddLinkHtml = '➕;' + ## The text/symbol for a very short edit link. + ksShortEditLink = u'\u270D' + ## HTML hex entity string for ksShortDetailsLink. + ksShortEditLinkHtml = '✍' + ## The text/symbol for a very short details link. + ksShortDetailsLink = u'\u2318' + ## HTML hex entity string for ksShortDetailsLink. + ksShortDetailsLinkHtml = '⌘' + ## The text/symbol for a very short change log / details / previous page link. + ksShortChangeLogLink = u'\u2397' + ## HTML hex entity string for ksShortDetailsLink. + ksShortChangeLogLinkHtml = '⎗' + ## The text/symbol for a very short reports link. + ksShortReportLink = u'\u2397' + ## HTML hex entity string for ksShortReportLink. + ksShortReportLinkHtml = '⎗' + + + 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 « and » are too tiny). + if iCurItem > 0: + sHtml += '%s ' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '<<'); + else: + sHtml += '<< '; + + # 1 2 3 4... + if iStart > 0: + sHtml += '%s ... \n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),); + + sHtml += ' \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 += ' ... %s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase)); + + # Next page. + if iCurItem + 1 < cItems: + sHtml += ' %s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '>>'); + else: + sHtml += ' >>'; + + return sHtml; + +class WuiSingleContentBase(WuiContentBase): # pylint: disable=R0903 + """ + 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=R0903 + """ + 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): + """ + Formats one change log entry into one or more HTML table rows. + + 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]; + + # 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, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)), + 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"><pre>%s</pre></div></td>' \ + '<td><div class="tdpre"><pre>%s</pre></div></td></tr>\n' \ + % ( sRowClass, 'odd' if j & 1 else 'even', + webutils.escapeElem(oChange.sAttr), + webutils.escapeElem(oChange.sOldText), + webutils.escapeElem(oChange.sNewText), ); + 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 align="left">\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 += ' \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 align="right"><a href="?%s#tmchangelog">Next</a></td>\n' \ + % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td align="right">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'; + + for iEntry, _ in enumerate(aoEntries): + sContent += self.formatChangeLogEntry(aoEntries, iEntry); + + 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. + dParams = self._oDisp.getParameters(); + if hasattr(oData, 'ksIdGenAttr'): + sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); + if sIdGenParam in dParams: + del dParams[sIdGenParam]; + if WuiDispatcherBase.ksParamEffectiveDate in dParams: + del dParams[WuiDispatcherBase.ksParamEffectiveDate]; + 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. + if hasattr(self._oDisp, self._sActionBase + 'Clone'): + dParams = self._oDisp.getParameters(); + 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 dict(), + 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 = True if sListItem in asSelectedItems else False + 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): + 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._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'; + + def _generateTimeNavigation(self, sWhere): + """ + Returns HTML for time navigation. + + Note! Views without a need for a timescale just stubs this method. + """ + _ = sWhere; + sNavigation = ''; + + dParams = self._oDisp.getParameters(); + dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage; + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage; + + if WuiDispatcherBase.ksParamEffectiveDate in dParams: + del dParams[WuiDispatcherBase.ksParamEffectiveDate]; + sNavigation += ' [<a href="?%s">Now</a>]' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 01:00:00.00'; + sNavigation += ' [<a href="?%s">1</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 02:00:00.00'; + sNavigation += ', <a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 06:00:00.00'; + sNavigation += ', <a href="?%s">6</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 12:00:00.00'; + sNavigation += ', <a href="?%s">12</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-01 00:00:00.00'; + sNavigation += ', or <a href="?%s">24</a> hours ago]' % (webutils.encodeUrlParams(dParams),); + + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-02 00:00:00.00'; + sNavigation += ' [<a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-03 00:00:00.00'; + sNavigation += ', <a href="?%s">3</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-05 00:00:00.00'; + sNavigation += ', <a href="?%s">5</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-07 00:00:00.00'; + sNavigation += ', <a href="?%s">7</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-14 00:00:00.00'; + sNavigation += ', <a href="?%s">14</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-21 00:00:00.00'; + sNavigation += ', <a href="?%s">21</a>' % (webutils.encodeUrlParams(dParams),); + + dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-28 00:00:00.00'; + sNavigation += ', or <a href="?%s">28</a> days ago]' % (webutils.encodeUrlParams(dParams),); + + return sNavigation; + + + 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. + sNavigation += '<td align="center" class="tmtimenav">'; + sNavigation += self._generateTimeNavigation(sWhere); + sNavigation += '</td>'; + + # Next + if len(self._aoEntries) > self._cItemsPerPage: + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; + sNavigation += ' <td align="right"><a href="?%s">Next</a></td>\n' % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td></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]); + 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 = ' ▴' if iSorting == 1 else '<small> ▵</small>'; + sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]); + else: + sDirection = ''; + if iSorting < 0: + sDirection = ' ▾' if iSorting == -1 else '<small> ▿</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._tsEffectiveDate); ## @todo shorten this. + 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..18d57728 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- +# $Id: wuigraphwiz.py $ + +""" +Test Manager WUI - Graph Wizard +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 len(aoSeries) < 1: + return []; + return [aoSeries,]; + + # Split on unit. + dUnitSeries = dict(); + 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 iUnit in dUnitSeries: + aoUnitSeries = dUnitSeries[iUnit]; + 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> </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 in range(len(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], + ); + oRevInfo = oSeries.aoRevInfo[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;"> </span> %s</th>\n' \ + ' </tr>\n' \ + ' <tr class="graphwiz-tab graphwiz-tab-col-hdr-row">\n' \ + ' <th>Revision</th><th>Value (%s)</th><th>Δmax</th><th>Δmin</th>' \ + '<th>Samples</th>\n' \ + ' </tr>\n' \ + '</thead>\n' \ + % ( sColor, + self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs), + sUnit ); + + for i in range(len(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', + oSeries.aiRevisions[i], 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..16432e4d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py @@ -0,0 +1,976 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpform.py $ + +""" +Test Manager Web-UI - Form Helpers. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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; + +# 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 dict(); + + 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 += unicode(sText, errors='ignore') if isinstance(sText, str) else sText; + 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, escapeElem(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, escapeElem(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=R0915 + """ + 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=R0914 + fReadOnly = True): + """ + For WuiTestGroup. + """ + assert len(aoTestGroupMembers) <= len(aoAllTestCases); + self._addLabel(sName, sLabel); + if not aoAllTestCases: + return self._add('<li>No testcases available.</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 in range(len(oTestCase.aoTestCaseArgs)): + oVar = oTestCase.aoTestCaseArgs[iVar]; + 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, aoAllTestGroups, # pylint: disable=R0914 + sLabel, fReadOnly = True): + """ + For WuiAdminSchedGroup. + """ + if fReadOnly is None or self._fReadOnly: + fReadOnly = self._fReadOnly; + assert len(aoSchedGroupMembers) <= len(aoAllTestGroups); + self._addLabel(sName, sLabel); + if not aoAllTestGroups: + return self._add(u'<li>No test groups available.</li>\n') + + self._add(u'<input name="%s" type="hidden" value="%s">\n' + % ( SchedGroupDataEx.ksParam_aidTestGroups, + ','.join([unicode(oTestGroup.idTestGroup) for oTestGroup in aoAllTestGroups]), )); + + self._add(u'<table class="tmformtbl">\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(aoAllTestGroups): + oTestGroup = aoAllTestGroups[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, -1 if oMember is None else oMember.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 align="left">%s</td>\n' % ( escapeElem(oTestGroup.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, SchedGroupMemberData.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', )); + + self._add(u' <td align="center">\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 aoAllTestGroups: + 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 align="left">\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 addListOfSchedGroupsForTestBox(self, sName, aoInSchedGroups, aoAllSchedGroups, sLabel, # pylint: disable=R0914 + 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 available.</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, -1 if oMember is None else oMember.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> </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..e822a077 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraph.py $ + +""" +Test Manager Web-UI - Graph Helpers. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +class WuiHlpGraphDataTable(object): # pylint: disable=R0903 + """ + Data table container. + """ + + class Row(object): # pylint: disable=R0903 + """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), ]; + + def addRow(self, sGroup, aoValues, asValues = None): + """Adds a row to the data table.""" + 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=R0903 + """ + Data container for an table/graph with optional error bars on the Y values. + """ + + class DataSeries(object): # pylint: disable=R0903 + """ + 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=W0125 + from testmanager.webui import wuihlpgraphgooglechart as GraphImplementation; +else: + try: + import matplotlib; # pylint: disable=W0611,F0401,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=C0103 +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..5112e67a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphbase.py $ + +""" +Test Manager Web-UI - Graph Helpers - Base Class. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +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..c11345da --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphgooglechart.py $ + +""" +Test Manager Web-UI - Graph Helpers - Implemented using Google Charts. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# Validation Kit imports. +from common import webutils; +from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase; +from testmanager.webui import wuihlpgraphsimple; + + +#******************************************************************************* +#* Global Variables * +#******************************************************************************* +g_cGraphs = 0; + +class WuiHlpGraphGoogleChartsBase(WuiHlpGraphBase): + """ Base class for the Google Charts graphs. """ + pass; + + +## @todo bar graphs later. +WuiHlpBarGraph = wuihlpgraphsimple.WuiHlpBarGraph; + + +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=R0914 + 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..80615c06 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphmatplotlib.py $ + +""" +Test Manager Web-UI - Graph Helpers - Implemented using matplotlib. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 +else: + from StringIO import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module + +import matplotlib; # pylint: disable=F0401 +matplotlib.use('Agg'); # Force backend. +import matplotlib.pyplot; # pylint: disable=F0401 +from numpy import arange as numpy_arange; # pylint: disable=E0611,E0401,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=E1101 + 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 renderGraph(self): # pylint: disable=R0914 + aoTable = self._oData.aoTable; + + # + # Extract/structure the required data. + # + aoSeries = list(); + for j in range(len(aoTable[1].aoValues)): + aoSeries.append(list()); + asNames = list(); + 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 in range(len(aoTable[i].aoValues)): + fpValue = float(aoTable[i].aoValues[j]); + 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 = list(); + 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=R0914 + 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=W0125 + # 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=R0914 + 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=E0401 + 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..7892ce9c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphsimple.py $ + +""" +Test Manager Web-UI - Graph Helpers - Simple/Stub Implementation. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 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 in range(len(oRow.aoValues)): + oValue = oRow.aoValues[j]; + 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 </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 </td>\n' \ + ' <td width="%spx" nowrap><div> </div></td>\n' \ + % (cxBar, sColor, sInvColor, sValue, self.cxMaxBar - cxBar); + else: + sReport += ' <td width="%spx" nowrap bgcolor="%s"></td>\n' \ + ' <td width="%spx" nowrap> %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 in range(len(aoTable[0].asValues)): + sColor = self.kasColors[j % len(self.kasColors)]; + sReport += ' <font color="%s">■ %s</font>\n' \ + % (sColor, escapeElem(aoTable[0].asValues[j])); + 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; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py new file mode 100755 index 00000000..c90a00ab --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +# $Id: wuilogviewer.py $ + +""" +Test Manager WUI - Log viewer +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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; + if offEnd > cbFile: + 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..e02fc719 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuimain.py @@ -0,0 +1,1400 @@ +# -*- coding: utf-8 -*- +# $Id: wuimain.py $ + +""" +Test Manager Core - WUI - The Main page. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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'; + ## @} + + ## @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 _generateTimeSelector(self, dParams, sPreamble, sPostamble): + """ + Generate HTML code for time selector. + """ + + if WuiDispatcherBase.ksParamEffectiveDate in dParams: + tsEffective = dParams[WuiDispatcherBase.ksParamEffectiveDate] + del dParams[WuiDispatcherBase.ksParamEffectiveDate] + else: + tsEffective = '' + + # Forget about page No when changing a period + if WuiDispatcherBase.ksParamPageNo in dParams: + del dParams[WuiDispatcherBase.ksParamPageNo] + + sHtmlTimeSelector = '<form name="TimeForm" method="GET">\n' + sHtmlTimeSelector += sPreamble; + sHtmlTimeSelector += '\n <select name="%s" onchange="window.location=' % WuiDispatcherBase.ksParamEffectiveDate + sHtmlTimeSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), WuiDispatcherBase.ksParamEffectiveDate) + sHtmlTimeSelector += '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; + sHtmlTimeSelector += ' <option value="%s"%s%s>%s</option>\n' \ + % (webutils.quoteUrl(sTimestamp), + ' selected="selected"' if sTimestamp == tsEffective else '', + sExtraAttrs, sWayBackPointCaption) + if not fSelected and tsEffective != '': + sHtmlTimeSelector += ' <option value="%s" selected>%s</option>\n' \ + % (webutils.quoteUrl(tsEffective), tsEffective) + + sHtmlTimeSelector += ' </select>\n'; + sHtmlTimeSelector += sPostamble; + sHtmlTimeSelector += '\n</form>\n' + + return sHtmlTimeSelector + + 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"><<</a> ' \ + % (webutils.encodeUrlParams(dParams),); + + if tsNext is not None: + dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsNext); + sNext = ' <a href="?%s" title="One period later">>></a>' \ + % (webutils.encodeUrlParams(dParams),); + else: + sNext = ' >>'; + + return self._generateTimeSelector(self.getParameters(), sPrev, sNext); + + 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 = ' \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 ... \n' % (sHrefPtr % (0, str(1))) + sHtmlPager + if cPagesRangeEnd < cNumOfPages: + sHtmlPager += ' ... %s\n' % (sHrefPtr % (cNumOfPages, str(cNumOfPages + 1))) + + # Prev/Next (using << >> because « and » are too tiny). + if iPage > 0: + dParams[WuiDispatcherBase.ksParamPageNo] = iPage - 1 + sHtmlPager = ('<a title="Previous page" href="?%s"><<</a> \n' + % (webutils.encodeUrlParams(dParams), )) \ + + sHtmlPager; + else: + sHtmlPager = '<< \n' + sHtmlPager + + if iPage + 1 < cNumOfPages: + dParams[WuiDispatcherBase.ksParamPageNo] = iPage + 1 + sHtmlPager += '\n <a title="Next page" href="?%s">>></a>\n' % (webutils.encodeUrlParams(dParams),) + else: + sHtmlPager += '\n >>\n' + + return sHtmlPager + + def _generateItemPerPageSelector(self, dParams, cItemsPerPage): + """ + Generate HTML code for items per page selector + """ + + if WuiDispatcherBase.ksParamItemsPerPage in dParams: + del dParams[WuiDispatcherBase.ksParamItemsPerPage] + + # Forced reset of the page number + dParams[WuiDispatcherBase.ksParamPageNo] = 0 + sHtmlItemsPerPageSelector = '<form name="AgesPerPageForm" method="GET">\n' \ + ' Max <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \ + % (WuiDispatcherBase.ksParamItemsPerPage, + webutils.encodeUrlParams(dParams), + WuiDispatcherBase.ksParamItemsPerPage) + + aiItemsPerPage = [16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]; + for iItemsPerPage in aiItemsPerPage: + sHtmlItemsPerPageSelector += ' <option value="%d" %s>%d</option>\n' \ + % (iItemsPerPage, + 'selected="selected"' if iItemsPerPage == cItemsPerPage else '', + iItemsPerPage) + sHtmlItemsPerPageSelector += ' </select> items per page\n' \ + '</form>\n' + + return sHtmlItemsPerPageSelector + + 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=R0914 + 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 == TestResultLogic.ksResultsGroupingTypeNone \ + or enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestBox: + aoTmp = oTrLogic.getTestBoxes(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list(set([ (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(set([ (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(set([ (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(set([ ( 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(set([ (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(set([ (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().iteritems(): + 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">»»</a></span></p>\n'; + sHtml += u' <dl>\n'; + for oCrit in oFilter.aCriteria: + if oCrit.aoPossible: + if ( oCrit.oSub is None \ + and ( oCrit.sState == oCrit.ksState_Selected \ + or len(oCrit.aoPossible) <= 2)) \ + 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 = '▼'; + else: + sClass = 'sf-expandable'; + sChar = '▶'; + + 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); + + 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..9562df77 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuireport.py @@ -0,0 +1,692 @@ +# -*- coding: utf-8 -*- +# $Id: wuireport.py $ + +""" +Test Manager WUI - Reports. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + + +# 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; +from testmanager.webui.wuiadmintestcase import WuiTestCaseDetailsLink; +from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink; +from testmanager.core.report import ReportModelBase; + + +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 + self._dExtraParams = {}; + dCurParams = None if oDisp is None else oDisp.getParameters(); + if dCurParams is not None: + from testmanager.webui.wuimain import WuiMain; + for sExtraParam in [ WuiMain.ksParamReportPeriods, WuiMain.ksParamReportPeriodInHours, + WuiMain.ksParamEffectiveDate, ]: + if sExtraParam in dCurParams: + self._dExtraParams[sExtraParam] = dCurParams[sExtraParam]; + + + def generateNavigator(self, sWhere): + """ + Generates the navigator (manipulate _dParams). + Returns HTML. + """ + assert sWhere == 'top' or sWhere == '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); + + @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'; + + adPeriods = self._oModel.getSuccessRates(); + + sReport = ''; + + oTable = WuiHlpGraphDataTable('Period', [ 'Succeeded', 'Skipped', 'Failed' ]); + + #for i in range(len(adPeriods) - 1, -1, -1): + for i, dStatuses in enumerate(adPeriods): + cSuccess = dStatuses[ReportModelBase.ksTestStatus_Success] + dStatuses[ReportModelBase.ksTestStatus_Skipped]; + cTotal = cSuccess + dStatuses[ReportModelBase.ksTestStatus_Failure]; + sPeriod = self._oModel.getPeriodDesc(i); + if cTotal > 0: + oTable.addRow(sPeriod, + [ dStatuses[ReportModelBase.ksTestStatus_Success] * 100 / cTotal, + dStatuses[ReportModelBase.ksTestStatus_Skipped] * 100 / cTotal, + dStatuses[ReportModelBase.ksTestStatus_Failure] * 100 / cTotal, ], + [ self.fmtPctWithHits(dStatuses[ReportModelBase.ksTestStatus_Success], cTotal), + self.fmtPctWithHits(dStatuses[ReportModelBase.ksTestStatus_Skipped], cTotal), + self.fmtPctWithHits(dStatuses[ReportModelBase.ksTestStatus_Failure], cTotal), ]); + else: + oTable.addRow(sPeriod, [ 0, 0, 0 ], [ '0%', '0%', '0%' ]); + + 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: + sReport += '%s%% (thereof %s%% skipped)</p>\n' \ + % ( cSuccessNow * 100 / cTotalNow, adPeriods[0][ReportModelBase.ksTestStatus_Skipped] * 100 / cTotalNow); + else: + sReport += 'N/A</p>\n' + + oGraph = WuiHlpBarGraph('success-rate', oTable, self._oDisp); + oGraph.setRangeMax(100); + 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), + '▼' 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), + '▼' 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=W0125 + 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]; + sHtml = u'<td>'; + sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase, sName = oTestCase.sName, + dExtraParams = self._dExtraParams).toHtml(); + sHtml += u' '; + sHtml += WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml(); + sHtml += u'</td>'; + return sHtml; + + 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. + """ + + @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>'; + sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCaseArgs, oTestCaseArgs.idTestCaseArgs, + sName = self._formatName(oTestCaseArgs), dExtraParams = self._dExtraParams).toHtml(); + sHtml += u' '; + sHtml += WuiTestCaseDetailsLink(oTestCaseArgs.idTestCase).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 += WuiTestBoxDetailsLink(oTransient.oSubject.idTestBox, fBracketed = False).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 += WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestBox.idTestBox, sName = oTestBox.sName, + dExtraParams = self._dExtraParams).toHtml(); + sHtml += u' '; + sHtml += WuiTestBoxDetailsLink(oTestBox.idTestBox).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..0506bfb5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py @@ -0,0 +1,912 @@ +# -*- coding: utf-8 -*- +# $Id: wuitestresult.py $ + +""" +Test Manager WUI - Test Results. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 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=R0914 + """ + 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(' ', ' '), + 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=R0914,R0915 + 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), + 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.formatIntervalSeconds(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), + 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), + 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 = ' \\<br> \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: 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'); + 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..cfbdad7e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# $Id: wuitestresultfailure.py $ + +""" +Test Manager WUI - Dummy Test Result Failure Reason Edit Dialog - just for error handling! +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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 is not None and 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..7b4e12a4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# $Id: wuivcshistory.py $ + +""" +Test Manager WUI - VCS history +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE 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. +""" +__version__ = "$Revision: 127855 $" + +# 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">' \ + % ( 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); + |