Add ability to edit test meta on report page

The ability to edit test metadata on the results report
page might be a bit more intuitive for people, so this
patch adds that. This also better allows Foundation admins
to manage any specific test result.

Change-Id: I629408c0f1b05654742aad02129a3872827a8004
This commit is contained in:
Paul Van Eck 2016-12-05 17:25:18 -08:00
parent 0e8721e4ad
commit 39a111ce7d
4 changed files with 369 additions and 4 deletions

View File

@ -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>

View File

@ -14,6 +14,12 @@
</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}}
@ -29,12 +35,16 @@
<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-warning" ng-hide="ctrl.isShared()" ng-click="ctrl.shareTestRun(true)" confirm="Are you sure you want to share these test run results with the community?">Share</button>
<button class="btn btn-success" ng-show="ctrl.isShared()" ng-click="ctrl.shareTestRun(false)">Unshare</button>
<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'">
@ -112,8 +122,6 @@
<span ng-if="ctrl.nonFlagPassCount !== ctrl.totalNonFlagCount" class="no">NO</span>
</strong>
</p>
<p ng-if="ctrl.resultsData.verification_status"><strong>Verified:</strong>
<span class="yes">YES</span>
<hr>
<h4>Capability Overview</h4>

View File

@ -53,6 +53,7 @@
ctrl.getCapabilityTestCount = getCapabilityTestCount;
ctrl.getStatusTestCount = getStatusTestCount;
ctrl.openFullTestListModal = openFullTestListModal;
ctrl.openEditTestModal = openEditTestModal;
/** The testID extracted from the URL route. */
ctrl.testId = $stateParams.testID;
@ -610,6 +611,27 @@
});
}
/**
* 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();
}
@ -644,4 +666,197 @@
return ctrl.tests.sort().join('\n');
};
}
angular
.module('refstackApp')
.controller('EditTestModalController', EditTestModalController);
EditTestModalController.$inject = [
'$uibModalInstance', '$http', '$state', 'raiseAlert',
'refstackApiUrl', '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, refstackApiUrl, 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 Refstack
* 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 = refstackApiUrl + '/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 = refstackApiUrl + '/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 = refstackApiUrl + '/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 = refstackApiUrl + '/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 = [
refstackApiUrl, '/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');
};
}
})();

View File

@ -734,6 +734,17 @@ describe('Refstack controllers', function () {
ctrl.openFullTestListModal();
expect(modal.open).toHaveBeenCalled();
});
it('should have a method to open a modal for editing test metadata',
function () {
var modal;
inject(function ($uibModal) {
modal = $uibModal;
});
spyOn(modal, 'open');
ctrl.openEditTestModal();
expect(modal.open).toHaveBeenCalled();
});
});
describe('FullTestListModalController', function () {
@ -766,6 +777,72 @@ describe('Refstack controllers', function () {
});
});
describe('EditTestModalController', function () {
var modalInstance, ctrl, state;
var fakeResultsData = {
'results': ['test_id_1'],
'id': 'some-id',
'meta': {
'public_key': 'ssh-rsa', 'guideline': '2015.04.json',
'target': 'object'
}
};
var fakeProdResp = {'products': [{'id': 1234}]};
var fakeVersionResp = [{'id': 'ver1', 'version': '1.0'},
{'id': 'ver2', 'version': null}];
beforeEach(inject(function ($controller) {
modalInstance = {
dismiss: jasmine.createSpy('modalInstance.dismiss')
};
state = {
reload: jasmine.createSpy('state.reload')
};
ctrl = $controller('EditTestModalController',
{$uibModalInstance: modalInstance, $state: state,
resultsData: fakeResultsData}
);
$httpBackend.when('GET', fakeApiUrl +
'/guidelines').respond(['2015.03.json', '2015.04.json']);
$httpBackend.when('GET', fakeApiUrl + '/products')
.respond(200, fakeResultsData);
$httpBackend.when('GET', fakeApiUrl +
'/products/1234/versions').respond(fakeVersionResp);
}));
it('should be able to get product versions', function () {
ctrl.selectedProduct = {'id': '1234'};
ctrl.products = null;
$httpBackend.expectGET(fakeApiUrl + '/products/1234/versions')
.respond(200, fakeVersionResp);
ctrl.getProductVersions();
$httpBackend.flush();
expect(ctrl.productVersions).toEqual(fakeVersionResp);
var expected = {'id': 'ver2', 'version': null};
expect(ctrl.selectedVersion).toEqual(expected);
});
it('should have a method to save all changes made.', function () {
ctrl.metaCopy.target = 'platform';
ctrl.metaCopy.shared = 'true';
ctrl.selectedVersion = {'id': 'ver2', 'version': null};
ctrl.saveChanges();
// Only meta changed should send a POST request.
$httpBackend.expectPOST(
fakeApiUrl + '/results/some-id/meta/target',
'platform')
.respond(201, '');
$httpBackend.expectPOST(
fakeApiUrl + '/results/some-id/meta/shared',
'true')
.respond(201, '');
$httpBackend.expectPUT(fakeApiUrl + '/results/some-id',
{'product_version_id': 'ver2'})
.respond(201);
$httpBackend.flush();
});
});
describe('TestRaiseAlertModalController', function() {
var data, modalInstance, ctrl;