From 55d74f86440a5a804d4551e04f7fd39518af0723 Mon Sep 17 00:00:00 2001
From: SerenaFeng <feng.xiaowei@zte.com.cn>
Date: Fri, 12 May 2017 01:49:57 +0800
Subject: add web portal framework for TestAPI

Change-Id: I62cea8b59ffe6a6cde98051c130f4502c07d3557
Signed-off-by: SerenaFeng <feng.xiaowei@zte.com.cn>
---
 .../results-report/partials/editTestModal.html     |  65 ++
 .../results-report/partials/fullTestListModal.html |  13 +
 .../results-report/partials/reportDetails.html     |  87 +++
 .../components/results-report/resultsReport.html   | 185 +++++
 .../results-report/resultsReportController.js      | 869 +++++++++++++++++++++
 5 files changed, 1219 insertions(+)
 create mode 100644 testapi/3rd_party/static/testapi-ui/components/results-report/partials/editTestModal.html
 create mode 100644 testapi/3rd_party/static/testapi-ui/components/results-report/partials/fullTestListModal.html
 create mode 100644 testapi/3rd_party/static/testapi-ui/components/results-report/partials/reportDetails.html
 create mode 100644 testapi/3rd_party/static/testapi-ui/components/results-report/resultsReport.html
 create mode 100644 testapi/3rd_party/static/testapi-ui/components/results-report/resultsReportController.js

(limited to 'testapi/3rd_party/static/testapi-ui/components/results-report')

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()">&times;</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"> &mdash;
+                        <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"> &mdash;
+                        <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');
+        };
+    }
+})();
-- 
cgit