diff options
Diffstat (limited to 'testapi/3rd_party/static/testapi-ui/components/results-report')
5 files changed, 1219 insertions, 0 deletions
diff --git a/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html new file mode 100644 index 0000000..583c9b9 --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html @@ -0,0 +1,65 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" aria-hidden="true" ng-click="modal.close()">×</button> + <h4>Edit Test Run Metadata</h4> + <p>Make changes to your test metadata.</p> + </div> + <div class="modal-body"> + <div class="form-group"> + <strong>Publicly Shared:</strong> + <select ng-model="modal.metaCopy.shared" + class="form-control"> + <option value="true">Yes</option> + <option value="">No</option> + </select> + <br /> + <strong>Associated Guideline:</strong> + <select ng-model="modal.metaCopy.guideline" + ng-options="o as o.slice(0, -5) for o in modal.versionList" + class="form-control"> + <option value="">None</option> + </select> + <br /> + <strong>Associated Target Program:</strong> + <select ng-model="modal.metaCopy.target" + class="form-control"> + <option value="">None</option> + <option value="platform">OpenStack Powered Platform</option> + <option value="compute">OpenStack Powered Compute</option> + <option value="object">OpenStack Powered Object Storage</option> + </select> + <hr> + <strong>Associated Product:</strong> + <select ng-options="product as product.name for product in modal.products | arrayConverter | orderBy: 'name' track by product.id" + ng-model="modal.selectedProduct" + ng-change="modal.getProductVersions()" + class="form-control"> + <option value="">-- No Product --</option> + </select> + + <span ng-if="modal.productVersions.length"> + <strong>Product Version:</strong> + <select ng-options="version as version.version for version in modal.productVersions | orderBy: 'version' track by version.id" + ng-model="modal.selectedVersion" + class="form-control"> + </select> + + </span> + + </div> + <div ng-show="modal.showError" class="alert alert-danger" role="alert"> + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> + <span class="sr-only">Error:</span> + {{modal.error}} + </div> + <div ng-show="modal.showSuccess" class="alert alert-success" role="success"> + <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> + <span class="sr-only">Success:</span> + Changes saved successfully. + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-primary" type="button" ng-click="modal.saveChanges()">Save Changes</button> + <button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button> + </div> +</div> diff --git a/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html new file mode 100644 index 0000000..6db198b --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html @@ -0,0 +1,13 @@ +<div class="modal-content"> + <div class="modal-header"> + <h4>All Passed Tests ({{modal.tests.length}})</h4> + </div> + <div class="modal-body tests-modal-content"> + <div class="form-group"> + <textarea class="form-control" rows="20" id="tests" wrap="off">{{modal.getTestListString()}}</textarea> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button> + </div> +</div> diff --git a/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html new file mode 100644 index 0000000..517e569 --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html @@ -0,0 +1,87 @@ +<!-- +HTML for each accordion group that separates the status types on the results +report page. +--> + +<uib-accordion-group is-open="isOpen" is-disabled="ctrl.caps[status].caps.length == 0"> + <uib-accordion-heading> + {{status | capitalize}} + <small> + (<strong>Total:</strong> {{ctrl.caps[status].caps.length}} capabilities, {{ctrl.caps[status].count}} tests) + <span ng-if="ctrl.testStatus !== 'total'"> + (<strong>{{ctrl.testStatus | capitalize}}:</strong> {{ctrl.getStatusTestCount(status)}} tests) + </span> + </small> + <i class="pull-right glyphicon" + ng-class="{'glyphicon-chevron-down': isOpen, 'glyphicon-chevron-right': !isOpen}"> + </i> + </uib-accordion-heading> + <ol class="capabilities"> + <li ng-repeat="capability in ctrl.caps[status].caps | orderBy:'id'" + ng-if="ctrl.isCapabilityShown(capability)"> + + <a ng-click="showTests = !showTests" + title="{{ctrl.guidelineData.capabilities[capability.id].description}}"> + {{capability.id}} + </a> + <span ng-class="{'text-success': ctrl.testStatus === 'passed', + 'text-danger': ctrl.testStatus === 'not passed', + 'text-warning': ctrl.testStatus === 'flagged'}" + ng-if="ctrl.testStatus !== 'total'"> + [{{ctrl.getCapabilityTestCount(capability)}}] + </span> + <span ng-class="{'text-success': (capability.passedTests.length > 0 && + capability.notPassedTests.length == 0), + 'text-danger': (capability.passedTests.length == 0 && + capability.notPassedTests.length > 0), + 'text-warning': (capability.passedTests.length > 0 && + capability.notPassedTests.length > 0)}" + ng-if="ctrl.testStatus === 'total'"> + [{{capability.passedTests.length}}/{{capability.passedTests.length + + capability.notPassedTests.length}}] + </span> + + <ul class="list-unstyled" uib-collapse="!showTests"> + <!-- Start passed test list --> + <li ng-repeat="test in capability.passedTests | orderBy:'toString()'" + ng-if="ctrl.isTestShown(test, capability)"> + + <span class="glyphicon glyphicon-ok text-success" + aria-hidden="true"> + </span> + <span ng-class="{'glyphicon glyphicon-flag text-warning': + ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}" + title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}"> + </span> + {{test}} + <span ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases"> — + <a ng-click="showAliases = !showAliases">[Aliases]</a> + <div class="test-detail-report" ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases && showAliases"> + <ul><li ng-repeat="alias in ctrl.guidelineData.capabilities[capability.id].tests[test].aliases">{{alias}}</li></ul> + </div> + </span> + </li> + <!-- End passed test list --> + + <!-- Start not passed test list --> + <li ng-repeat="test in capability.notPassedTests | orderBy:'toString()'" + ng-if="ctrl.isTestShown(test, capability)"> + + <span class="glyphicon glyphicon-remove text-danger" aria-hidden="true"></span> + <span ng-class="{'glyphicon glyphicon-flag text-warning': + ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}" + title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}"> + </span> + {{test}} + <span ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases"> — + <a ng-click="showAliases = !showAliases">[Aliases]</a> + <div class="test-detail-report" ng-if="ctrl.guidelineData.capabilities[capability.id].tests[test].aliases && showAliases"> + <ul><li ng-repeat="alias in ctrl.guidelineData.capabilities[capability.id].tests[test].aliases">{{alias}}</li></ul> + </div> + </span> + </li> + <!-- End not passed test list --> + </ul> + </li> + </ol> +</uib-accordion-group> diff --git a/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html b/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html new file mode 100644 index 0000000..5527121 --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html @@ -0,0 +1,185 @@ +<h3>Test Run Results</h3> + +<div ng-show="ctrl.resultsData" class="container-fluid"> + <div class="row"> + <div class="pull-left"> + <div class="test-report"> + <strong>Test ID:</strong> {{ctrl.testId}}<br /> + <div ng-if="ctrl.isResultAdmin()"><strong>Cloud ID:</strong> {{ctrl.resultsData.cpid}}<br /></div> + <strong>Upload Date:</strong> {{ctrl.resultsData.created_at}} UTC<br /> + <strong>Duration:</strong> {{ctrl.resultsData.duration_seconds}} seconds<br /> + <strong>Total Number of Passed Tests:</strong> + <a title="See all passed tests" ng-click="ctrl.openFullTestListModal()"> + {{ctrl.resultsData.results.length}} + </a> + </div> + <hr> + <div ng-show="ctrl.isResultAdmin()"> + <strong>Publicly Shared:</strong> + <span ng-if="ctrl.resultsData.meta.shared">Yes</span> + <span ng-if="!ctrl.resultsData.meta.shared">No</span> + <br /> + </div> + <div ng-show="ctrl.resultsData.product_version"> + <strong>Product:</strong> + {{ctrl.resultsData.product_version.product_info.name}} + <span ng-if="ctrl.resultsData.product_version.version"> + ({{ctrl.resultsData.product_version.version}}) + </span><br /> + </div> + <div ng-show="ctrl.resultsData.meta.guideline"> + <strong>Associated Guideline:</strong> + {{ctrl.resultsData.meta.guideline.slice(0, -5)}} + </div> + <div ng-show="ctrl.resultsData.meta.target"> + <strong>Associated Target Program:</strong> + {{ctrl.targetMappings[ctrl.resultsData.meta.target]}} + </div> + <div ng-show="ctrl.resultsData.verification_status"> + <strong>Verified:</strong> + <span class="yes">YES</span> + </div> + <hr> + </div> + + <div class="pull-right"> + <div ng-show="ctrl.isResultAdmin() && !ctrl.resultsData.verification_status"> + <button class="btn btn-info" ng-click="ctrl.openEditTestModal()">Edit</button> + <button type="button" class="btn btn-danger" ng-click="ctrl.deleteTestRun()" confirm="Are you sure you want to delete these test run results?">Delete</button> + </div> + <div ng-show="ctrl.resultsData.user_role === 'foundation'"> + <hr> + <div class="checkbox checkbox-verified"> + <label><input type="checkbox" + ng-model="ctrl.isVerified" + ng-change="ctrl.updateVerificationStatus()" + ng-true-value="1" + ng-false-value="0"> + <strong>Verified</strong> + </label> + </div> + </div> + </div> + </div> +</div> + +<div ng-show="ctrl.resultsData"> + <p>See how these results stack up against Interop Working Group capabilities and OpenStack + <a target="_blank" href="http://www.openstack.org/brand/interop/">target marketing programs.</a> + </p> + + <!-- User Options --> + <div class="row"> + <div class="col-md-3"> + <strong>Guideline Version:</strong> + <!-- Slicing the version file name here gets rid of the '.json' file extension --> + <select ng-model="ctrl.version" + ng-change="ctrl.updateGuidelines()" + class="form-control" + ng-options="versionFile.slice(0,-5) for versionFile in ctrl.versionList"> + </select> + </div> + <div class="col-md-4"> + <strong>Target Program:</strong> + <select ng-model="ctrl.target" class="form-control" ng-change="ctrl.buildCapabilitiesObject()"> + <option value="platform">OpenStack Powered Platform</option> + <option value="compute">OpenStack Powered Compute</option> + <option value="object">OpenStack Powered Object Storage</option> + </select> + </div> + </div> + <!-- End User Options --> + + <br /> + <div ng-if="ctrl.guidelineData"> + <strong>Guideline Status:</strong> + {{ctrl.guidelineData.status | capitalize}} + </div> + + <strong>Corresponding OpenStack Releases:</strong> + <ul class="list-inline"> + <li ng-repeat="release in ctrl.guidelineData.releases"> + {{release | capitalize}} + </li> + </ul> + <hr > + + <div ng-show="ctrl.guidelineData"> + <strong>Status:</strong> + <p>This cloud passes <strong>{{ctrl.requiredPassPercent | number:1}}% </strong> + ({{ctrl.caps.required.passedCount}}/{{ctrl.caps.required.count}}) + of the tests in the <strong>{{ctrl.version.slice(0, -5)}}</strong> <em>required</em> capabilities for the + <strong>{{ctrl.targetMappings[target]}}</strong> program. <br /> + Excluding flagged tests, this cloud passes + <strong>{{ctrl.nonFlagRequiredPassPercent | number:1}}%</strong> + ({{ctrl.nonFlagPassCount}}/{{ctrl.totalNonFlagCount}}) + of the <em>required</em> tests. + </p> + + <p>Compliance with <strong>{{ctrl.version.slice(0, -5)}}</strong>: + <strong> + <span ng-if="ctrl.nonFlagPassCount === ctrl.totalNonFlagCount" class="yes">YES</span> + <span ng-if="ctrl.nonFlagPassCount !== ctrl.totalNonFlagCount" class="no">NO</span> + </strong> + </p> + + <hr> + <h4>Capability Overview</h4> + + Test Filters:<br /> + <div class="btn-group button-margin" data-toggle="buttons"> + <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'total'}"> + <input type="radio" ng-model="ctrl.testStatus" value="total"> + <span class="text-primary">All</span> + </label> + <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'passed'}"> + <input type="radio" ng-model="ctrl.testStatus" value="passed"> + <span class="text-success">Passed</span> + </label> + <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'not passed'}"> + <input type="radio" ng-model="ctrl.testStatus" value="not passed"> + <span class="text-danger">Not Passed</span> + </label> + <label class="btn btn-default" ng-class="{'active': ctrl.testStatus === 'flagged'}"> + <input type="radio" ng-model="ctrl.testStatus" value="flagged"> + <span class="text-warning">Flagged</span> + </label> + </div> + + <uib-accordion close-others=false> + <!-- The ng-repeat is used to pass in a local variable to the template. --> + <ng-include + ng-repeat="status in ['required']" + src="ctrl.detailsTemplate" + onload="isOpen = true"> + </ng-include> + <br /> + <ng-include + ng-repeat="status in ['advisory']" + src="ctrl.detailsTemplate"> + </ng-include> + <br /> + <ng-include + ng-repeat="status in ['deprecated']" + src="ctrl.detailsTemplate"> + </ng-include> + <br /> + <ng-include + ng-repeat="status in ['removed']" + src="ctrl.detailsTemplate"> + </ng-include> + </uib-accordion> + </div> +</div> + +<div class="loading"> + <div cg-busy="{promise:versionsRequest,message:'Loading versions'}"></div> + <div cg-busy="{promise:capsRequest,message:'Loading capabilities'}"></div> + <div cg-busy="{promise:resultsRequest,message:'Loading results'}"></div> +</div> + +<div ng-show="ctrl.showError" class="alert alert-danger" role="alert"> + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> + <span class="sr-only">Error:</span> + {{ctrl.error}} +</div> diff --git a/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js b/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js new file mode 100644 index 0000000..591ad40 --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js @@ -0,0 +1,869 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function () { + 'use strict'; + + angular + .module('testapiApp') + .controller('ResultsReportController', ResultsReportController); + + ResultsReportController.$inject = [ + '$http', '$stateParams', '$window', + '$uibModal', 'testapiApiUrl', 'raiseAlert' + ]; + + /** + * TestAPI Results Report Controller + * This controller is for the '/results/<test run ID>' page where a user can + * view details for a specific test run. + */ + function ResultsReportController($http, $stateParams, $window, + $uibModal, testapiApiUrl, raiseAlert) { + + var ctrl = this; + + ctrl.getVersionList = getVersionList; + ctrl.getResults = getResults; + ctrl.isResultAdmin = isResultAdmin; + ctrl.isShared = isShared; + ctrl.shareTestRun = shareTestRun; + ctrl.deleteTestRun = deleteTestRun; + ctrl.updateVerificationStatus = updateVerificationStatus; + ctrl.updateGuidelines = updateGuidelines; + ctrl.getTargetCapabilities = getTargetCapabilities; + ctrl.buildCapabilityV1_2 = buildCapabilityV1_2; + ctrl.buildCapabilityV1_3 = buildCapabilityV1_3; + ctrl.buildCapabilitiesObject = buildCapabilitiesObject; + ctrl.isTestFlagged = isTestFlagged; + ctrl.getFlaggedReason = getFlaggedReason; + ctrl.isCapabilityShown = isCapabilityShown; + ctrl.isTestShown = isTestShown; + ctrl.getCapabilityTestCount = getCapabilityTestCount; + ctrl.getStatusTestCount = getStatusTestCount; + ctrl.openFullTestListModal = openFullTestListModal; + ctrl.openEditTestModal = openEditTestModal; + + /** The testID extracted from the URL route. */ + ctrl.testId = $stateParams.testID; + + /** The target OpenStack marketing program to compare against. */ + ctrl.target = 'platform'; + + /** Mappings of Interop WG components to marketing program names. */ + ctrl.targetMappings = { + 'platform': 'Openstack Powered Platform', + 'compute': 'OpenStack Powered Compute', + 'object': 'OpenStack Powered Object Storage' + }; + + /** The schema version of the currently selected guideline data. */ + ctrl.schemaVersion = null; + + /** The selected test status used for test filtering. */ + ctrl.testStatus = 'total'; + + /** The HTML template that all accordian groups will use. */ + ctrl.detailsTemplate = 'components/results-report/partials/' + + 'reportDetails.html'; + + /** + * Retrieve an array of available guideline files from the TestAPI + * API server, sort this array reverse-alphabetically, and store it in + * a scoped variable. The scope's selected version is initialized to + * the latest (i.e. first) version here as well. After a successful API + * call, the function to update the capabilities is called. + * Sample API return array: ["2015.03.json", "2015.04.json"] + */ + function getVersionList() { + var content_url = testapiApiUrl + '/guidelines'; + ctrl.versionsRequest = + $http.get(content_url).success(function (data) { + ctrl.versionList = data.sort().reverse(); + if (!ctrl.version) { + // Default to the first approved guideline which is + // expected to be at index 1. + ctrl.version = ctrl.versionList[1]; + } + ctrl.updateGuidelines(); + }).error(function (error) { + ctrl.showError = true; + ctrl.error = 'Error retrieving version list: ' + + angular.toJson(error); + }); + } + + /** + * Retrieve results from the TestAPI API server based on the test + * run id in the URL. This function is the first function that will + * be called from the controller. Upon successful retrieval of results, + * the function that gets the version list will be called. + */ + function getResults() { + var content_url = testapiApiUrl + '/results/' + ctrl.testId; + ctrl.resultsRequest = + $http.get(content_url).success(function (data) { + ctrl.resultsData = data; + ctrl.version = ctrl.resultsData.meta.guideline; + ctrl.isVerified = ctrl.resultsData.verification_status; + if (ctrl.resultsData.meta.target) { + ctrl.target = ctrl.resultsData.meta.target; + } + getVersionList(); + }).error(function (error) { + ctrl.showError = true; + ctrl.resultsData = null; + ctrl.error = 'Error retrieving results from server: ' + + angular.toJson(error); + }); + } + + /** + * This tells you whether the current user has administrative + * privileges for the test result. + * @returns {Boolean} true if the user has admin privileges. + */ + function isResultAdmin() { + return Boolean(ctrl.resultsData && + (ctrl.resultsData.user_role === 'owner' || + ctrl.resultsData.user_role === 'foundation')); + } + /** + * This tells you whether the current results are shared with the + * community or not. + * @returns {Boolean} true if the results are shared + */ + function isShared() { + return Boolean(ctrl.resultsData && + 'shared' in ctrl.resultsData.meta); + } + + /** + * This will send an API request in order to share or unshare the + * current results based on the passed in shareState. + * @param {Boolean} shareState - Whether to share or unshare results. + */ + function shareTestRun(shareState) { + var content_url = [ + testapiApiUrl, '/results/', ctrl.testId, '/meta/shared' + ].join(''); + if (shareState) { + ctrl.shareRequest = + $http.post(content_url, 'true').success(function () { + ctrl.resultsData.meta.shared = 'true'; + raiseAlert('success', '', 'Test run shared!'); + }).error(function (error) { + raiseAlert('danger', error.title, error.detail); + }); + } else { + ctrl.shareRequest = + $http.delete(content_url).success(function () { + delete ctrl.resultsData.meta.shared; + raiseAlert('success', '', 'Test run unshared!'); + }).error(function (error) { + raiseAlert('danger', error.title, error.detail); + }); + } + } + + /** + * This will send a request to the API to delete the current + * test results set. + */ + function deleteTestRun() { + var content_url = [ + testapiApiUrl, '/results/', ctrl.testId + ].join(''); + ctrl.deleteRequest = + $http.delete(content_url).success(function () { + $window.history.back(); + }).error(function (error) { + raiseAlert('danger', error.title, error.detail); + }); + } + + /** + * This will send a request to the API to delete the current + * test results set. + */ + function updateVerificationStatus() { + var content_url = [ + testapiApiUrl, '/results/', ctrl.testId + ].join(''); + var data = {'verification_status': ctrl.isVerified}; + ctrl.updateRequest = + $http.put(content_url, data).success( + function () { + ctrl.resultsData.verification_status = ctrl.isVerified; + raiseAlert('success', '', + 'Verification status changed!'); + }).error(function (error) { + ctrl.isVerified = ctrl.resultsData.verification_status; + raiseAlert('danger', error.title, error.detail); + }); + } + + /** + * This will contact the TestAPI API server to retrieve the JSON + * content of the guideline file corresponding to the selected + * version. A function to construct an object from the capability + * data will be called upon successful retrieval. + */ + function updateGuidelines() { + ctrl.guidelineData = null; + ctrl.showError = false; + var content_url = testapiApiUrl + '/guidelines/' + + ctrl.version; + ctrl.capsRequest = + $http.get(content_url).success(function (data) { + ctrl.guidelineData = data; + ctrl.schemaVersion = data.schema; + ctrl.buildCapabilitiesObject(); + }).error(function (error) { + ctrl.showError = true; + ctrl.guidelineData = null; + ctrl.error = 'Error retrieving guideline date: ' + + angular.toJson(error); + }); + } + + /** + * This will get all the capabilities relevant to the target and + * their corresponding statuses. + * @returns {Object} Object containing each capability and their status + */ + function getTargetCapabilities() { + var components = ctrl.guidelineData.components; + var targetCaps = {}; + + // The 'platform' target is comprised of multiple components, so + // we need to get the capabilities belonging to each of its + // components. + if (ctrl.target === 'platform') { + var platform_components = + ctrl.guidelineData.platform.required; + + // This will contain status priority values, where lower + // values mean higher priorities. + var statusMap = { + required: 1, + advisory: 2, + deprecated: 3, + removed: 4 + }; + + // For each component required for the platform program. + angular.forEach(platform_components, function (component) { + // Get each capability list belonging to each status. + angular.forEach(components[component], + function (caps, status) { + // For each capability. + angular.forEach(caps, function(cap) { + // If the capability has already been added. + if (cap in targetCaps) { + // If the status priority value is less + // than the saved priority value, update + // the value. + if (statusMap[status] < + statusMap[targetCaps[cap]]) { + targetCaps[cap] = status; + } + } + else { + targetCaps[cap] = status; + } + }); + }); + }); + } + else { + angular.forEach(components[ctrl.target], + function (caps, status) { + angular.forEach(caps, function(cap) { + targetCaps[cap] = status; + }); + }); + } + return targetCaps; + } + + /** + * This will build the a capability object for schema version 1.2. + * This object will contain the information needed to form a report in + * the HTML template. + * @param {String} capId capability ID + */ + function buildCapabilityV1_2(capId) { + var cap = { + 'id': capId, + 'passedTests': [], + 'notPassedTests': [], + 'passedFlagged': [], + 'notPassedFlagged': [] + }; + var capDetails = ctrl.guidelineData.capabilities[capId]; + // Loop through each test belonging to the capability. + angular.forEach(capDetails.tests, + function (testId) { + // If the test ID is in the results' test list, add + // it to the passedTests array. + if (ctrl.resultsData.results.indexOf(testId) > -1) { + cap.passedTests.push(testId); + if (capDetails.flagged.indexOf(testId) > -1) { + cap.passedFlagged.push(testId); + } + } + else { + cap.notPassedTests.push(testId); + if (capDetails.flagged.indexOf(testId) > -1) { + cap.notPassedFlagged.push(testId); + } + } + }); + return cap; + } + + /** + * This will build the a capability object for schema version 1.3 and + * above. This object will contain the information needed to form a + * report in the HTML template. + * @param {String} capId capability ID + */ + function buildCapabilityV1_3(capId) { + var cap = { + 'id': capId, + 'passedTests': [], + 'notPassedTests': [], + 'passedFlagged': [], + 'notPassedFlagged': [] + }; + + // For cases where a capability listed in components is not + // in the capabilities object. + if (!(capId in ctrl.guidelineData.capabilities)) { + return cap; + } + + // Loop through each test belonging to the capability. + angular.forEach(ctrl.guidelineData.capabilities[capId].tests, + function (details, testId) { + var passed = false; + + // If the test ID is in the results' test list. + if (ctrl.resultsData.results.indexOf(testId) > -1) { + passed = true; + } + else if ('aliases' in details) { + var len = details.aliases.length; + for (var i = 0; i < len; i++) { + var alias = details.aliases[i]; + if (ctrl.resultsData.results.indexOf(alias) > -1) { + passed = true; + break; + } + } + } + + // Add to correct array based on whether the test was + // passed or not. + if (passed) { + cap.passedTests.push(testId); + if ('flagged' in details) { + cap.passedFlagged.push(testId); + } + } + else { + cap.notPassedTests.push(testId); + if ('flagged' in details) { + cap.notPassedFlagged.push(testId); + } + } + }); + return cap; + } + + /** + * This will check the schema version of the current capabilities file, + * and will call the correct method to build an object based on the + * capability data retrieved from the TestAPI API server. + */ + function buildCapabilitiesObject() { + // This is the object template where 'count' is the number of + // total tests that fall under the given status, and 'passedCount' + // is the number of tests passed. The 'caps' array will contain + // objects with details regarding each capability. + ctrl.caps = { + 'required': {'caps': [], 'count': 0, 'passedCount': 0, + 'flagFailCount': 0, 'flagPassCount': 0}, + 'advisory': {'caps': [], 'count': 0, 'passedCount': 0, + 'flagFailCount': 0, 'flagPassCount': 0}, + 'deprecated': {'caps': [], 'count': 0, 'passedCount': 0, + 'flagFailCount': 0, 'flagPassCount': 0}, + 'removed': {'caps': [], 'count': 0, 'passedCount': 0, + 'flagFailCount': 0, 'flagPassCount': 0} + }; + + switch (ctrl.schemaVersion) { + case '1.2': + var capMethod = 'buildCapabilityV1_2'; + break; + case '1.3': + case '1.4': + case '1.5': + case '1.6': + capMethod = 'buildCapabilityV1_3'; + break; + default: + ctrl.showError = true; + ctrl.guidelineData = null; + ctrl.error = 'The schema version for the guideline ' + + 'file selected (' + ctrl.schemaVersion + + ') is currently not supported.'; + return; + } + + // Get test details for each relevant capability and store + // them in the scope's 'caps' object. + var targetCaps = ctrl.getTargetCapabilities(); + angular.forEach(targetCaps, function(status, capId) { + var cap = ctrl[capMethod](capId); + ctrl.caps[status].count += + cap.passedTests.length + cap.notPassedTests.length; + ctrl.caps[status].passedCount += cap.passedTests.length; + ctrl.caps[status].flagPassCount += cap.passedFlagged.length; + ctrl.caps[status].flagFailCount += + cap.notPassedFlagged.length; + ctrl.caps[status].caps.push(cap); + }); + + ctrl.requiredPassPercent = (ctrl.caps.required.passedCount * + 100 / ctrl.caps.required.count); + + ctrl.totalRequiredFailCount = ctrl.caps.required.count - + ctrl.caps.required.passedCount; + ctrl.totalRequiredFlagCount = + ctrl.caps.required.flagFailCount + + ctrl.caps.required.flagPassCount; + ctrl.totalNonFlagCount = ctrl.caps.required.count - + ctrl.totalRequiredFlagCount; + ctrl.nonFlagPassCount = ctrl.totalNonFlagCount - + (ctrl.totalRequiredFailCount - + ctrl.caps.required.flagFailCount); + + ctrl.nonFlagRequiredPassPercent = (ctrl.nonFlagPassCount * + 100 / ctrl.totalNonFlagCount); + } + + /** + * This will check if a given test is flagged. + * @param {String} test ID of the test to check + * @param {Object} capObj capability that test is under + * @returns {Boolean} truthy value if test is flagged + */ + function isTestFlagged(test, capObj) { + if (!capObj) { + return false; + } + return (((ctrl.schemaVersion === '1.2') && + (capObj.flagged.indexOf(test) > -1)) || + ((ctrl.schemaVersion >= '1.3') && + (capObj.tests[test].flagged))); + } + + /** + * This will return the reason a test is flagged. An empty string + * will be returned if the passed in test is not flagged. + * @param {String} test ID of the test to check + * @param {String} capObj capability that test is under + * @returns {String} reason + */ + function getFlaggedReason(test, capObj) { + if ((ctrl.schemaVersion === '1.2') && + (ctrl.isTestFlagged(test, capObj))) { + + // Return a generic message since schema 1.2 does not + // provide flag reasons. + return 'Interop Working Group has flagged this test.'; + } + else if ((ctrl.schemaVersion >= '1.3') && + (ctrl.isTestFlagged(test, capObj))) { + + return capObj.tests[test].flagged.reason; + } + else { + return ''; + } + } + + /** + * This will check the if a capability should be shown based on the + * test filter selected. If a capability does not have any tests + * belonging under the given filter, it should not be shown. + * @param {Object} capability Built object for capability + * @returns {Boolean} true if capability should be shown + */ + function isCapabilityShown(capability) { + return ((ctrl.testStatus === 'total') || + (ctrl.testStatus === 'passed' && + capability.passedTests.length > 0) || + (ctrl.testStatus === 'not passed' && + capability.notPassedTests.length > 0) || + (ctrl.testStatus === 'flagged' && + (capability.passedFlagged.length + + capability.notPassedFlagged.length > 0))); + } + + /** + * This will check the if a test should be shown based on the test + * filter selected. + * @param {String} test ID of the test + * @param {Object} capability Built object for capability + * @return {Boolean} true if test should be shown + */ + function isTestShown(test, capability) { + return ((ctrl.testStatus === 'total') || + (ctrl.testStatus === 'passed' && + capability.passedTests.indexOf(test) > -1) || + (ctrl.testStatus === 'not passed' && + capability.notPassedTests.indexOf(test) > -1) || + (ctrl.testStatus === 'flagged' && + (capability.passedFlagged.indexOf(test) > -1 || + capability.notPassedFlagged.indexOf(test) > -1))); + } + + /** + * This will give the number of tests belonging under the selected + * test filter for a given capability. + * @param {Object} capability Built object for capability + * @returns {Number} number of tests under filter + */ + function getCapabilityTestCount(capability) { + if (ctrl.testStatus === 'total') { + return capability.passedTests.length + + capability.notPassedTests.length; + } + else if (ctrl.testStatus === 'passed') { + return capability.passedTests.length; + } + else if (ctrl.testStatus === 'not passed') { + return capability.notPassedTests.length; + } + else if (ctrl.testStatus === 'flagged') { + return capability.passedFlagged.length + + capability.notPassedFlagged.length; + } + else { + return 0; + } + } + + /** + * This will give the number of tests belonging under the selected + * test filter for a given status. + * @param {String} capability status + * @returns {Number} number of tests for status under filter + */ + function getStatusTestCount(status) { + if (!ctrl.caps) { + return -1; + } + else if (ctrl.testStatus === 'total') { + return ctrl.caps[status].count; + } + else if (ctrl.testStatus === 'passed') { + return ctrl.caps[status].passedCount; + } + else if (ctrl.testStatus === 'not passed') { + return ctrl.caps[status].count - + ctrl.caps[status].passedCount; + } + else if (ctrl.testStatus === 'flagged') { + return ctrl.caps[status].flagFailCount + + ctrl.caps[status].flagPassCount; + } + else { + return -1; + } + } + + /** + * This will open the modal that will show the full list of passed + * tests for the current results. + */ + function openFullTestListModal() { + $uibModal.open({ + templateUrl: '/components/results-report/partials' + + '/fullTestListModal.html', + backdrop: true, + windowClass: 'modal', + animation: true, + controller: 'FullTestListModalController as modal', + size: 'lg', + resolve: { + tests: function () { + return ctrl.resultsData.results; + } + } + }); + } + + /** + * This will open the modal that will all a user to edit test run + * metadata. + */ + function openEditTestModal() { + $uibModal.open({ + templateUrl: '/components/results-report/partials' + + '/editTestModal.html', + backdrop: true, + windowClass: 'modal', + animation: true, + controller: 'EditTestModalController as modal', + size: 'lg', + resolve: { + resultsData: function () { + return ctrl.resultsData; + } + } + }); + } + + getResults(); + } + + angular + .module('testapiApp') + .controller('FullTestListModalController', FullTestListModalController); + + FullTestListModalController.$inject = ['$uibModalInstance', 'tests']; + + /** + * Full Test List Modal Controller + * This controller is for the modal that appears if a user wants to see the + * full list of passed tests on a report page. + */ + function FullTestListModalController($uibModalInstance, tests) { + var ctrl = this; + + ctrl.tests = tests; + + /** + * This function will close/dismiss the modal. + */ + ctrl.close = function () { + $uibModalInstance.dismiss('exit'); + }; + + /** + * This function will return a string representing the sorted + * tests list separated by newlines. + */ + ctrl.getTestListString = function () { + return ctrl.tests.sort().join('\n'); + }; + } + + angular + .module('testapiApp') + .controller('EditTestModalController', EditTestModalController); + + EditTestModalController.$inject = [ + '$uibModalInstance', '$http', '$state', 'raiseAlert', + 'testapiApiUrl', 'resultsData' + ]; + + /** + * Edit Test Modal Controller + * This controller is for the modal that appears if a user wants to edit + * test run metadata. + */ + function EditTestModalController($uibModalInstance, $http, $state, + raiseAlert, testapiApiUrl, resultsData) { + + var ctrl = this; + + ctrl.getVersionList = getVersionList; + ctrl.getUserProducts = getUserProducts; + ctrl.associateProductVersion = associateProductVersion; + ctrl.getProductVersions = getProductVersions; + ctrl.saveChanges = saveChanges; + + ctrl.resultsData = resultsData; + ctrl.metaCopy = angular.copy(resultsData.meta); + ctrl.prodVersionCopy = angular.copy(resultsData.product_version); + + ctrl.getVersionList(); + ctrl.getUserProducts(); + + /** + * Retrieve an array of available capability files from the TestAPI + * API server, sort this array reverse-alphabetically, and store it in + * a scoped variable. + * Sample API return array: ["2015.03.json", "2015.04.json"] + */ + function getVersionList() { + if (ctrl.versionList) { + return; + } + var content_url = testapiApiUrl + '/guidelines'; + ctrl.versionsRequest = + $http.get(content_url).success(function (data) { + ctrl.versionList = data.sort().reverse(); + }).error(function (error) { + raiseAlert('danger', error.title, + 'Unable to retrieve version list'); + }); + } + + /** + * Get products user has management rights to or all products depending + * on the passed in parameter value. + */ + function getUserProducts() { + var contentUrl = testapiApiUrl + '/products'; + ctrl.productsRequest = + $http.get(contentUrl).success(function (data) { + ctrl.products = {}; + angular.forEach(data.products, function(prod) { + if (prod.can_manage) { + ctrl.products[prod.id] = prod; + } + }); + if (ctrl.prodVersionCopy) { + ctrl.selectedProduct = ctrl.products[ + ctrl.prodVersionCopy.product_info.id + ]; + } + ctrl.getProductVersions(); + }).error(function (error) { + ctrl.products = null; + ctrl.showError = true; + ctrl.error = + 'Error retrieving Products listing from server: ' + + angular.toJson(error); + }); + } + + /** + * Send a PUT request to the API server to associate a product with + * a test result. + */ + function associateProductVersion() { + var verId = (ctrl.selectedVersion ? + ctrl.selectedVersion.id : null); + var testId = resultsData.id; + var url = testapiApiUrl + '/results/' + testId; + ctrl.associateRequest = $http.put(url, {'product_version_id': + verId}) + .error(function (error) { + ctrl.showError = true; + ctrl.showSuccess = false; + ctrl.error = + 'Error associating product version with test run: ' + + angular.toJson(error); + }); + } + + /** + * Get all versions for a product. + */ + function getProductVersions() { + if (!ctrl.selectedProduct) { + ctrl.productVersions = []; + ctrl.selectedVersion = null; + return; + } + + var url = testapiApiUrl + '/products/' + + ctrl.selectedProduct.id + '/versions'; + ctrl.getVersionsRequest = $http.get(url) + .success(function (data) { + ctrl.productVersions = data; + if (ctrl.prodVersionCopy && + ctrl.prodVersionCopy.product_info.id == + ctrl.selectedProduct.id) { + ctrl.selectedVersion = ctrl.prodVersionCopy; + } + else { + angular.forEach(data, function(ver) { + if (!ver.version) { + ctrl.selectedVersion = ver; + } + }); + } + }).error(function (error) { + raiseAlert('danger', error.title, error.detail); + }); + } + + /** + * Send a PUT request to the server with the changes. + */ + function saveChanges() { + ctrl.showError = false; + ctrl.showSuccess = false; + var metaBaseUrl = [ + testapiApiUrl, '/results/', resultsData.id, '/meta/' + ].join(''); + var metaFields = ['target', 'guideline', 'shared']; + var meta = ctrl.metaCopy; + angular.forEach(metaFields, function(field) { + var oldMetaValue = (field in ctrl.resultsData.meta) ? + ctrl.resultsData.meta[field] : ''; + if (field in meta && oldMetaValue != meta[field]) { + var metaUrl = metaBaseUrl + field; + if (meta[field]) { + ctrl.assocRequest = $http.post(metaUrl, meta[field]) + .success(function(data) { + ctrl.resultsData.meta[field] = meta[field]; + }) + .error(function (error) { + ctrl.showError = true; + ctrl.showSuccess = false; + ctrl.error = + 'Error associating metadata with ' + + 'test run: ' + angular.toJson(error); + }); + } + else { + ctrl.unassocRequest = $http.delete(metaUrl) + .success(function (data) { + delete ctrl.resultsData.meta[field]; + delete meta[field]; + }) + .error(function (error) { + ctrl.showError = true; + ctrl.showSuccess = false; + ctrl.error = + 'Error associating metadata with ' + + 'test run: ' + angular.toJson(error); + }); + } + } + }); + ctrl.associateProductVersion(); + if (!ctrl.showError) { + ctrl.showSuccess = true; + $state.reload(); + } + } + + /** + * This function will close/dismiss the modal. + */ + ctrl.close = function () { + $uibModalInstance.dismiss('exit'); + }; + } +})(); |