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:
parent
0e8721e4ad
commit
39a111ce7d
@ -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>
|
@ -14,6 +14,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<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">
|
<div ng-show="ctrl.resultsData.product_version">
|
||||||
<strong>Product:</strong>
|
<strong>Product:</strong>
|
||||||
{{ctrl.resultsData.product_version.product_info.name}}
|
{{ctrl.resultsData.product_version.product_info.name}}
|
||||||
@ -29,12 +35,16 @@
|
|||||||
<strong>Associated Target Program:</strong>
|
<strong>Associated Target Program:</strong>
|
||||||
{{ctrl.targetMappings[ctrl.resultsData.meta.target]}}
|
{{ctrl.targetMappings[ctrl.resultsData.meta.target]}}
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-show="ctrl.resultsData.verification_status">
|
||||||
|
<strong>Verified:</strong>
|
||||||
|
<span class="yes">YES</span>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<div ng-show="ctrl.isResultAdmin() && !ctrl.resultsData.verification_status">
|
<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-info" ng-click="ctrl.openEditTestModal()">Edit</button>
|
||||||
<button class="btn btn-success" ng-show="ctrl.isShared()" ng-click="ctrl.shareTestRun(false)">Unshare</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>
|
<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>
|
||||||
<div ng-show="ctrl.resultsData.user_role === 'foundation'">
|
<div ng-show="ctrl.resultsData.user_role === 'foundation'">
|
||||||
@ -112,8 +122,6 @@
|
|||||||
<span ng-if="ctrl.nonFlagPassCount !== ctrl.totalNonFlagCount" class="no">NO</span>
|
<span ng-if="ctrl.nonFlagPassCount !== ctrl.totalNonFlagCount" class="no">NO</span>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<p ng-if="ctrl.resultsData.verification_status"><strong>Verified:</strong>
|
|
||||||
<span class="yes">YES</span>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h4>Capability Overview</h4>
|
<h4>Capability Overview</h4>
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
ctrl.getCapabilityTestCount = getCapabilityTestCount;
|
ctrl.getCapabilityTestCount = getCapabilityTestCount;
|
||||||
ctrl.getStatusTestCount = getStatusTestCount;
|
ctrl.getStatusTestCount = getStatusTestCount;
|
||||||
ctrl.openFullTestListModal = openFullTestListModal;
|
ctrl.openFullTestListModal = openFullTestListModal;
|
||||||
|
ctrl.openEditTestModal = openEditTestModal;
|
||||||
|
|
||||||
/** The testID extracted from the URL route. */
|
/** The testID extracted from the URL route. */
|
||||||
ctrl.testId = $stateParams.testID;
|
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();
|
getResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,4 +666,197 @@
|
|||||||
return ctrl.tests.sort().join('\n');
|
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');
|
||||||
|
};
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -734,6 +734,17 @@ describe('Refstack controllers', function () {
|
|||||||
ctrl.openFullTestListModal();
|
ctrl.openFullTestListModal();
|
||||||
expect(modal.open).toHaveBeenCalled();
|
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 () {
|
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() {
|
describe('TestRaiseAlertModalController', function() {
|
||||||
var data, modalInstance, ctrl;
|
var data, modalInstance, ctrl;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user