Merge feature/vendor branch into master branch

Change-Id: I9b86d0797a50635af440e3aa0068db8860248e98
This commit is contained in:
Paul Van Eck 2016-10-16 11:52:16 -07:00
commit ac472fe494
41 changed files with 2680 additions and 88 deletions

View File

@ -93,6 +93,26 @@
url: '/vendor/:vendorID',
templateUrl: '/components/vendors/vendor.html',
controller: 'VendorController as ctrl'
}).
state('userProducts', {
url: '/user_products',
templateUrl: '/components/products/products.html',
controller: 'ProductsController as ctrl'
}).
state('publicProducts', {
url: '/public_products',
templateUrl: '/components/products/products.html',
controller: 'ProductsController as ctrl'
}).
state('cloud', {
url: '/cloud/:id',
templateUrl: '/components/products/cloud.html',
controller: 'ProductController as ctrl'
}).
state('distro', {
url: '/distro/:id',
templateUrl: '/components/products/distro.html',
controller: 'ProductController as ctrl'
});
}

View File

@ -0,0 +1,26 @@
<h3>Cloud Product</h3>
<div cg-busy="{promise:ctrl.productRequest,message:'Loading'}"></div>
<div ng-show="ctrl.product" class="container-fluid">
<div class="row">
<div class="pull-left">
<div class="test-report">
<strong>Name:</strong> {{ctrl.product.name}}<br />
<strong>Product ID:</strong> {{ctrl.id}}<br />
<strong>Description:</strong> {{ctrl.product.description}}<br />
<strong>Publicity:</strong> {{ctrl.product.public ? 'Public' : 'Private'}}<br />
<strong>Vendor Name:</strong> {{ctrl.vendor.name}}<br />
<strong>Vendor Description:</strong> {{ctrl.vendor.description || '-'}}<br />
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div>
<hr>
<div ng-include src="'components/products/partials/testsTable.html'"></div>
</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>

View File

@ -0,0 +1,26 @@
<h3>Distro Product</h3>
<div cg-busy="{promise:ctrl.productRequest,message:'Loading'}"></div>
<div ng-show="ctrl.product" class="container-fluid">
<div class="row">
<div class="pull-left">
<div class="test-report">
<strong>Name:</strong> {{ctrl.product.name}}<br />
<strong>Product ID:</strong> {{ctrl.id}}<br />
<strong>Description:</strong> {{ctrl.product.description}}<br />
<strong>Publicity:</strong> {{ctrl.product.public ? 'Public' : 'Private'}}<br />
<strong>Vendor Name:</strong> {{ctrl.vendor.name}}<br />
<strong>Vendor Description:</strong> {{ctrl.vendor.description || '-'}}<br />
</div>
</div>
<div ng-include src="'components/products/partials/management.html'"></div>
<div class="clearfix"></div>
<div ng-include src="'components/products/partials/versions.html'"></div>
<hr>
<div ng-include src="'components/products/partials/testsTable.html'"></div>
</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>

View File

@ -0,0 +1,13 @@
<div class="pull-right">
<a ng-if="ctrl.product.can_manage"
href="javascript:void(0)"
ng-click="ctrl.deleteProduct()"
confirm="Are you sure you want to delete {{ctrl.product.name}}?">
Delete
</a><br />
<a ng-if="ctrl.product.can_manage && (ctrl.vendor.type == 0 || ctrl.vendor.type == 3)"
href="javascript:void(0)" ng-click="ctrl.switchProductPublicity()"
confirm="Are you sure you want to switch publicity of this product?">
Make Product <span ng-if="ctrl.product.public">Private</span><span ng-if="!ctrl.product.public">Public</span>
</a><br />
</div>

View File

@ -0,0 +1,137 @@
<h4><strong>Test Runs on Product</strong></h4>
<div cg-busy="{promise:ctrl.testsRequest,message:'Loading'}"></div>
<table class="table table-striped table-hover">
<thead>
<tr>
<th></th>
<th>Upload Date</th>
<th>Test Run ID</th>
<th>Product Version</th>
<th>Shared</th>
</tr>
</thead>
<tbody>
<tr ng-repeat-start="(index, result) in ctrl.testsData">
<td>
<a ng-if="!result.expanded"
class="glyphicon glyphicon-plus"
ng-click="result.expanded = true">
</a>
<a ng-if="result.expanded"
class="glyphicon glyphicon-minus"
ng-click="result.expanded = false">
</a>
</td>
<td>{{result.created_at}}</td>
<td><a ui-sref="resultsDetail({testID: result.id})">{{result.id}}</a></td>
<td>{{result.product_version.version}}</td>
<td>
<span ng-show="result.meta.shared" class="glyphicon glyphicon-share"></span>
</td>
</tr>
<tr ng-if="result.expanded" ng-repeat-end>
<td></td>
<td colspan="4">
<strong>Publicly Shared:</strong>
<span ng-if="result.meta.shared == 'true' && !result.sharedEdit">Yes</span>
<span ng-if="!result.meta.shared && !result.sharedEdit">
<em>No</em>
</span>
<select ng-if="result.sharedEdit"
ng-model="result.meta.shared"
class="form-inline">
<option value="true">Yes</option>
<option value="">No</option>
</select>
<a ng-if="!result.sharedEdit"
ng-click="result.sharedEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil"></a>
<a ng-if="result.sharedEdit"
ng-click="ctrl.associateTestMeta(index,'shared',result.meta.shared)"
title="Save"
class="glyphicon glyphicon-floppy-disk"></a>
<br />
<strong>Associated Guideline:</strong>
<span ng-if="!result.meta.guideline && !result.guidelineEdit">
<em>None</em>
</span>
<span ng-if="result.meta.guideline && !result.guidelineEdit">
{{result.meta.guideline.slice(0, -5)}}
</span>
<select ng-if="result.guidelineEdit"
ng-model="result.meta.guideline"
ng-options="o as o.slice(0, -5) for o in ctrl.versionList"
class="form-inline">
<option value="">None</option>
</select>
<a ng-if="!result.guidelineEdit"
ng-click="ctrl.getGuidelineVersionList();result.guidelineEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil"></a>
<a ng-if="result.guidelineEdit"
ng-click="ctrl.associateTestMeta(index, 'guideline', result.meta.guideline)"
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
<strong>Associated Target Program:</strong>
<span ng-if="!result.meta.target && !result.targetEdit">
<em>None</em>
</span>
<span ng-if="result.meta.target && !result.targetEdit">
{{ctrl.targetMappings[result.meta.target]}}</span>
<select ng-if="result.targetEdit"
ng-model="result.meta.target"
class="form-inline">
<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>
<a ng-if="!result.targetEdit"
ng-click="result.targetEdit = true"
title="Edit"
class="glyphicon glyphicon-pencil">
</a>
<a ng-if="result.targetEdit"
ng-click="ctrl.associateTestMeta(index, 'target', result.meta.target)"
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
<br />
<small>
<a ng-click="ctrl.unassociateTest(index)"
confirm="Are you sure you want to unassociate this test result with product: {{ctrl.product.name}}? Test result ownership will be given back to the original owner only.">
<span class="glyphicon glyphicon-remove-circle" ></span> Unassociate test result from product
</a>
</small>
</td>
</tr>
</tbody>
</table>
<div class="pages">
<uib-pagination
total-items="ctrl.totalItems"
ng-model="ctrl.currentPage"
items-per-page="ctrl.itemsPerPage"
max-size="ctrl.maxSize"
class="pagination-sm"
boundary-links="true"
rotate="false"
num-pages="ctrl.numPages"
ng-change="ctrl.getProductTests()">
</uib-pagination>
</div>
<div ng-show="ctrl.showTestsError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{ctrl.testsError}}
</div>

View File

@ -0,0 +1,29 @@
<strong>Version(s) Available:</strong>
<span ng-repeat="item in ctrl.productVersions | orderBy:'version'">
<a ng-show="item.version && ctrl.product.can_manage" class="label label-info" ng-click="ctrl.openVersionModal(item)">
{{item.version}}
</a>
<span ng-hide="ctrl.product.can_manage" class="label label-info">{{item.version}}</span>
</span>
&nbsp;
<a ng-if="ctrl.product.can_manage"
title="Add a new product version."
ng-click="ctrl.showNewVersionInput = true">
<small><span class="glyphicon glyphicon-plus"></span></small>
</a>
<div ng-if="ctrl.showNewVersionInput" class="row" style="margin-top: 5px;">
<div class="col-md-2">
<div class="input-group">
<input ng-model="ctrl.newProductVersion"
type="text" class="form-control" placeholder="New Version">
<span class="input-group-btn">
<button
class="btn btn-default"
type="button"
ng-click="ctrl.addProductVersion()">
Add
</button>
</span>
</div>
</div>
</div>

View File

@ -0,0 +1,51 @@
<div class="modal-content">
<div class="modal-header">
<h4>Manage Version</h4>
</div>
<div class="modal-body">
<div class="pull-left">
<strong>Version:</strong> {{modal.version.version}}<br />
</div>
<div class="pull-right">
<a class="glyphicon glyphicon-trash"
ng-click="modal.deleteProductVersion()"
confirm="Are you sure you want to delete product version {{modal.version.version}}?">
</a>
</div>
<div class="clearfix"></div>
<br />
(Optional) Associate cloud provider ID (CPID) with product version for easier
test run associating.
<br />
<br />
<div class="row">
<div class="col-md-8">
<strong>CPID:</strong><br />
<div class="input-group">
<input type="text" class="form-control" ng-model="modal.version.cpid" />
<span class="input-group-btn">
<button
class="btn btn-default"
type="button"
ng-click="modal.saveChanges()">
Save
</button>
</span>
</div>
</div>
</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>
Updated Successfully.
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="button" ng-click="modal.close()">Close</button>
</div>
</div>

View File

@ -0,0 +1,357 @@
/*
* 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('refstackApp')
.controller('ProductController', ProductController);
ProductController.$inject = [
'$scope', '$http', '$state', '$stateParams', '$window', '$uibModal',
'refstackApiUrl', 'raiseAlert'
];
/**
* RefStack Product Controller
* This controller is for the '/product/' details page where owner can
* view details of the product.
*/
function ProductController($scope, $http, $state, $stateParams,
$window, $uibModal, refstackApiUrl, raiseAlert) {
var ctrl = this;
ctrl.getProduct = getProduct;
ctrl.getProductVersions = getProductVersions;
ctrl.deleteProduct = deleteProduct;
ctrl.deleteProductVersion = deleteProductVersion;
ctrl.getProductTests = getProductTests;
ctrl.switchProductPublicity = switchProductPublicity;
ctrl.associateTestMeta = associateTestMeta;
ctrl.getGuidelineVersionList = getGuidelineVersionList;
ctrl.addProductVersion = addProductVersion;
ctrl.unassociateTest = unassociateTest;
ctrl.openVersionModal = openVersionModal;
/** The product id extracted from the URL route. */
ctrl.id = $stateParams.id;
ctrl.productVersions = [];
if (!$scope.auth.isAuthenticated) {
$state.go('home');
}
/** Mappings of DefCore components to marketing program names. */
ctrl.targetMappings = {
'platform': 'Openstack Powered Platform',
'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Object Storage'
};
// Pagination controls.
ctrl.currentPage = 1;
ctrl.itemsPerPage = 20;
ctrl.maxSize = 5;
ctrl.getProduct();
ctrl.getProductVersions();
ctrl.getProductTests();
/**
* This will contact the Refstack API to get a product information.
*/
function getProduct() {
ctrl.showError = false;
ctrl.product = null;
var content_url = refstackApiUrl + '/products/' + ctrl.id;
ctrl.productRequest = $http.get(content_url).success(
function(data) {
ctrl.product = data;
ctrl.product_properties =
angular.fromJson(data.properties);
}
).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving from server: ' +
angular.toJson(error);
}).then(function() {
var url = refstackApiUrl + '/vendors/' +
ctrl.product.organization_id;
$http.get(url).success(function(data) {
ctrl.vendor = data;
}).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving from server: ' +
angular.toJson(error);
});
});
}
/**
* This will contact the Refstack API to get product versions.
*/
function getProductVersions() {
ctrl.showError = false;
var content_url = refstackApiUrl + '/products/' + ctrl.id +
'/versions';
ctrl.productVersionsRequest = $http.get(content_url).success(
function(data) {
ctrl.productVersions = data;
}
).error(function(error) {
ctrl.showError = true;
ctrl.error =
'Error retrieving versions from server: ' +
angular.toJson(error);
});
}
/**
* This will delete the product.
*/
function deleteProduct() {
var url = [refstackApiUrl, '/products/', ctrl.id].join('');
$http.delete(url).success(function () {
$window.location.href = '/';
}).error(function (error) {
raiseAlert('danger', 'Error: ', error.detail);
});
}
/**
* This will delete the given product versions.
*/
function deleteProductVersion(versionId) {
var url = [
refstackApiUrl, '/products/', ctrl.id,
'/versions/', versionId ].join('');
$http.delete(url).success(function () {
ctrl.getProductVersions();
}).error(function (error) {
raiseAlert('danger', 'Error: ', error.detail);
});
}
/**
* Set a POST request to the API server to add a new version for
* the product.
*/
function addProductVersion() {
var url = [refstackApiUrl, '/products/', ctrl.id,
'/versions'].join('');
ctrl.addVersionRequest = $http.post(url,
{'version': ctrl.newProductVersion})
.success(function (data) {
ctrl.productVersions.push(data);
ctrl.newProductVersion = '';
ctrl.showNewVersionInput = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/**
* Get tests runs associated with the current product.
*/
function getProductTests() {
ctrl.showTestsError = false;
var content_url = refstackApiUrl + '/results' +
'?page=' + ctrl.currentPage + '&product_id='
+ ctrl.id;
ctrl.testsRequest = $http.get(content_url).success(
function(data) {
ctrl.testsData = data.results;
ctrl.totalItems = data.pagination.total_pages *
ctrl.itemsPerPage;
ctrl.currentPage = data.pagination.current_page;
}
).error(function(error) {
ctrl.showTestsError = true;
ctrl.testsError =
'Error retrieving tests from server: ' +
angular.toJson(error);
});
}
/**
* This will switch public/private property of the product.
*/
function switchProductPublicity() {
var url = [refstackApiUrl, '/products/', ctrl.id].join('');
$http.put(url, {public: !ctrl.product.public}).success(
function (data) {
ctrl.product = data;
ctrl.product_properties = angular.fromJson(data.properties);
}).error(function (error) {
raiseAlert('danger', 'Error: ', error.detail);
});
}
/**
* This will send an API request in order to associate a metadata
* key-value pair with the given testId
* @param {Number} index - index of the test object in the results list
* @param {String} key - metadata key
* @param {String} value - metadata value
*/
function associateTestMeta(index, key, value) {
var testId = ctrl.testsData[index].id;
var metaUrl = [
refstackApiUrl, '/results/', testId, '/meta/', key
].join('');
var editFlag = key + 'Edit';
if (value) {
ctrl.associateRequest = $http.post(metaUrl, value)
.success(function () {
ctrl.testsData[index][editFlag] = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
else {
ctrl.unassociateRequest = $http.delete(metaUrl)
.success(function () {
ctrl.testsData[index][editFlag] = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
}
/**
* 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 getGuidelineVersionList() {
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');
});
}
/**
* Send a PUT request to the API server to unassociate a product with
* a test result.
*/
function unassociateTest(index) {
var testId = ctrl.testsData[index].id;
var url = refstackApiUrl + '/results/' + testId;
ctrl.associateRequest = $http.put(url, {'product_version_id': null})
.success(function () {
ctrl.testsData.splice(index, 1);
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/**
* This will open the modal that will allow a product version
* to be managed.
*/
function openVersionModal(version) {
$uibModal.open({
templateUrl: '/components/products/partials' +
'/versionsModal.html',
backdrop: true,
windowClass: 'modal',
animation: true,
controller: 'ProductVersionModalController as modal',
size: 'lg',
resolve: {
version: function () {
return version;
},
parent: function () {
return ctrl;
}
}
});
}
}
angular
.module('refstackApp')
.controller('ProductVersionModalController',
ProductVersionModalController);
ProductVersionModalController.$inject = [
'$uibModalInstance', '$http', 'refstackApiUrl', 'version', 'parent'
];
/**
* Product Version Modal Controller
* This controller is for the modal that appears if a user wants to
* manage a product version.
*/
function ProductVersionModalController($uibModalInstance, $http,
refstackApiUrl, version, parent) {
var ctrl = this;
ctrl.version = version;
ctrl.parent = parent;
ctrl.close = close;
ctrl.deleteProductVersion = deleteProductVersion;
ctrl.saveChanges = saveChanges;
/**
* This function will close/dismiss the modal.
*/
function close() {
$uibModalInstance.dismiss('exit');
}
/**
* Call the parent function to delete a version, then close the modal.
*/
function deleteProductVersion() {
ctrl.parent.deleteProductVersion(ctrl.version.id);
ctrl.close();
}
/**
* This will update the current version, saving changes.
*/
function saveChanges() {
ctrl.showSuccess = false;
ctrl.showError = false;
var url = [
refstackApiUrl, '/products/', ctrl.version.product_id,
'/versions/', ctrl.version.id ].join('');
var content = {'cpid': ctrl.version.cpid};
$http.put(url, content).success(function() {
ctrl.showSuccess = true;
}).error(function(error) {
ctrl.showError = true;
ctrl.error = error.detail;
});
}
}
})();

View File

@ -0,0 +1,79 @@
<h3>{{ctrl.pageHeader}}</h3>
<p>{{ctrl.pageParagraph}}</p>
<div ng-show="ctrl.data" class="products-table">
<label ng-if="ctrl.isAdminView && ctrl.isUserProducts">
<input type="checkbox" ng-model="ctrl.withPrivate" ng-change="ctrl.updateData();">&nbsp;Show private
</label>
<br />
<table ng-show="ctrl.data" class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Product Type</th>
<th>Description</th>
<th>Vendor</th>
<th>Visibility</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="product in ctrl.data.products">
<td ng-if="ctrl.isUserProducts && product.product_type == 0"><a ui-sref="distro({id: product.id})">{{product.name}}</a></td>
<td ng-if="ctrl.isUserProducts && product.product_type != 0"><a ui-sref="cloud({id: product.id})">{{product.name}}</a></td>
<td ng-if="!ctrl.isUserProducts">{{product.name}}</td>
<td>{{ctrl.getProductTypeDescription(product.product_type)}}</td>
<td>{{product.description}}</td>
<td>{{ctrl.allVendors[product.organization_id].name}}</td>
<td>{{product.public ? 'Public' : 'Private'}}</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="ctrl.isUserProducts">
<hr />
<h4>Add new Product</h4>
<div class="row">
<div class="col-md-2">
<label>Name</label>
<p class="input-group">
<input type="text" class="form-control" ng-model="ctrl.name" />
</p>
</div>
<div class="col-md-4">
<label>Description</label>
<p class="input-group">
<input type="text" class="form-control" size="80"
ng-model="ctrl.description" />
</p>
</div>
<div class="col-md-2">
<label>Product type:</label>
<select ng-model="ctrl.productType" class="form-control">
<option value="{{0}}">{{ctrl.getProductTypeDescription(0)}}</option>
<option value="{{1}}">{{ctrl.getProductTypeDescription(1)}}</option>
<option value="{{2}}">{{ctrl.getProductTypeDescription(2)}}</option>
</select>
</div>
<div class="col-md-2">
<label>Vendor:</label>
<select ng-model="ctrl.organizationId" class="form-control">
<option ng-repeat="vendor in ctrl.vendors" value="{{vendor.id}}">{{vendor.name}}</option>
</select>
</div>
<div class="col-md-2" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" ng-click="ctrl.addProduct()">Add Product</button>
</div>
</div>
</div>
<div cg-busy="{promise:ctrl.authRequest,message:'Loading'}"></div>
<div cg-busy="{promise:ctrl.productsRequest,message:'Loading'}"></div>
<div cg-busy="{promise:ctrl.vendorsRequest,message:'Loading'}"></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>

View File

@ -0,0 +1,204 @@
/*
* 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('refstackApp')
.controller('ProductsController', ProductsController);
ProductsController.$inject = [
'$rootScope', '$scope', '$http', '$state',
'refstackApiUrl','raiseAlert'
];
/**
* RefStack Products Controller
*/
function ProductsController($rootScope, $scope, $http, $state,
refstackApiUrl, raiseAlert) {
var ctrl = this;
ctrl.update = update;
ctrl.updateData = updateData;
ctrl._filterProduct = _filterProduct;
ctrl.addProduct = addProduct;
ctrl.updateVendors = updateVendors;
ctrl.getProductTypeDescription = getProductTypeDescription;
/** Check to see if this page should display user-specific products. */
ctrl.isUserProducts = $state.current.name === 'userProducts';
/** Show private products in list for foundation admin */
ctrl.withPrivate = false;
/** Properties for adding new products */
ctrl.name = '';
ctrl.description = '';
ctrl.organizationId = '';
// Should only be on user-products-page if authenticated.
if (ctrl.isUserProducts && !$scope.auth.isAuthenticated) {
$state.go('home');
}
ctrl.pageHeader = ctrl.isUserProducts ?
'My Products' : 'Public Products';
ctrl.pageParagraph = ctrl.isUserProducts ?
'Your added products are listed here.' :
'Public products are listed here.';
if (ctrl.isUserProducts) {
ctrl.authRequest = $scope.auth.doSignCheck()
.then(ctrl.updateVendors)
.then(ctrl.update);
} else {
ctrl.updateVendors();
ctrl.update();
}
ctrl.rawData = null;
ctrl.allVendors = {};
ctrl.isAdminView = $rootScope.auth
&& $rootScope.auth.currentUser
&& $rootScope.auth.currentUser.is_admin;
/**
* This will contact the Refstack API to get a listing of products.
*/
function update() {
ctrl.showError = false;
// Construct the API URL based on user-specified filters.
var contentUrl = refstackApiUrl + '/products';
if (typeof ctrl.rawData == 'undefined'
|| ctrl.rawData === null) {
ctrl.productsRequest =
$http.get(contentUrl).success(function (data) {
ctrl.rawData = data;
ctrl.updateData();
}).error(function (error) {
ctrl.rawData = null;
ctrl.showError = true;
ctrl.error =
'Error retrieving Products listing from server: ' +
angular.toJson(error);
});
} else {
ctrl.updateData();
}
}
/**
* This will update data for view with current settings on page.
*/
function updateData() {
ctrl.data = {};
ctrl.data.products = ctrl.rawData.products.filter(function(s) {
return ctrl._filterProduct(s); });
ctrl.data.products.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
}
/**
* Returns true if a specific product can be displayed on this page.
*/
function _filterProduct(product) {
if (!ctrl.isUserProducts) {
return product.public;
}
if ($rootScope.auth.currentUser.is_admin) {
// TO-DO: filter out non-admin's items
// because public is not a correct flag for this
return product.public || ctrl.withPrivate;
}
return product.can_manage;
}
/**
* Get the product type description given the type integer.
*/
function getProductTypeDescription(product_type) {
switch (product_type) {
case 0:
return 'Distro';
case 1:
return 'Public Cloud';
case 2:
return 'Hosted Private Cloud';
default:
return 'Unknown';
}
}
/**
* This will contact the Refstack API to get a listing of
* available vendors that can be used to associate with products.
*/
function updateVendors() {
// Construct the API URL based on user-specified filters.
var contentUrl = refstackApiUrl + '/vendors';
ctrl.vendorsRequest =
$http.get(contentUrl).success(function (data) {
ctrl.vendors = Array();
ctrl.allVendors = {};
data.vendors.forEach(function(vendor) {
ctrl.allVendors[vendor.id] = vendor;
if (vendor.can_manage) {
ctrl.vendors.push(vendor);
}
});
ctrl.vendors.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
if (ctrl.vendors.length == 0) {
ctrl.vendors.push({name: 'Create New...', id: ''});
}
ctrl.organizationId = ctrl.vendors[0].id;
}).error(function (error) {
ctrl.vendors = null;
ctrl.showError = true;
ctrl.error =
'Error retrieving vendor listing from server: ' +
angular.toJson(error);
});
}
/**
* This will add new Product record.
*/
function addProduct() {
var url = refstackApiUrl + '/products';
var data = {
name: ctrl.name,
description: ctrl.description,
organization_id: ctrl.organizationId,
product_type: parseInt(ctrl.productType)
};
ctrl.name = '';
ctrl.description = '';
$http.post(url, data).success(function (data) {
ctrl.rawData = null;
ctrl.update();
}).error(function (error) {
ctrl.showError = true;
ctrl.error =
'Error adding new Product: ' + angular.toJson(error);
});
}
}
})();

View File

@ -133,7 +133,8 @@
*/
function isEditingAllowed() {
return Boolean(ctrl.resultsData &&
ctrl.resultsData.user_role === 'owner');
(ctrl.resultsData.user_role === 'owner' ||
ctrl.resultsData.user_role == 'foundation'));
}
/**
* This tells you whether the current results are shared with the

View File

@ -134,7 +134,7 @@
<option value="object">OpenStack Powered Object Storage</option>
</select>
<a ng-if="!result.targetEdit"
ng-click="result.targetEdit = true"
ng-click="result.targetEdit = true;"
title="Edit"
class="glyphicon glyphicon-pencil">
</a>
@ -143,6 +143,60 @@
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
<strong>Associated Product:</strong>
<span ng-if="!result.product_version && !result.productEdit">
<em>None</em>
</span>
<span ng-if="result.product_version && !result.productEdit">
<span ng-if="ctrl.products[result.product_version.product_info.id].product_type == 0">
<a ui-sref="distro({id: result.product_version.product_info.id})">
{{ctrl.products[result.product_version.product_info.id].name}}
<small ng-if="result.product_version.version">
({{result.product_version.version}})
</small>
</a>
</span>
<span ng-if="ctrl.products[result.product_version.product_info.id].product_type != 0">
<a ui-sref="cloud({id: result.product_version.product_info.id})">
{{ctrl.products[result.product_version.product_info.id].name}}
<small ng-if="result.product_version.version">
({{result.product_version.version}})
</small>
</a>
</span>
</span>
<select ng-if="result.productEdit"
ng-options="product as product.name for product in ctrl.products | arrayConverter | orderBy: 'name' track by product.id"
ng-model="result.selectedProduct"
ng-change="ctrl.getProductVersions(result)">
<option value="">-- No Product --</option>
</select>
<span ng-if="result.productVersions.length && result.productEdit">
<span class="glyphicon glyphicon-arrow-right" style="padding-right:3px;color:#303030;"></span>
Version:
<select ng-options="version as version.version for version in result.productVersions | orderBy: 'version' track by version.id"
ng-model="result.selectedVersion">
</select>
</span>
<a ng-if="!result.productEdit"
ng-click="ctrl.prepVersionEdit(result)"
title="Edit"
class="glyphicon glyphicon-pencil">
</a>
<a ng-if="result.productEdit"
ng-click="ctrl.associateProductVersion(result)"
confirm="Once you associate this test to this product, ownership
will be transferred to the product's vendor admins.
Continue?"
title="Save"
class="glyphicon glyphicon-floppy-disk">
</a>
<br />
</td>
</tr>
</tbody>

View File

@ -37,6 +37,10 @@
ctrl.clearFilters = clearFilters;
ctrl.associateMeta = associateMeta;
ctrl.getVersionList = getVersionList;
ctrl.getUserProducts = getUserProducts;
ctrl.associateProductVersion = associateProductVersion;
ctrl.getProductVersions = getProductVersions;
ctrl.prepVersionEdit = prepVersionEdit;
/** Mappings of DefCore components to marketing program names. */
ctrl.targetMappings = {
@ -90,6 +94,7 @@
if (ctrl.isUserResults) {
ctrl.authRequest = $scope.auth.doSignCheck()
.then(ctrl.update);
ctrl.getUserProducts();
} else {
ctrl.update();
}
@ -206,5 +211,98 @@
});
}
/**
* Get products user has management rights to or all products depending
* on the passed in parameter value.
*/
function getUserProducts() {
if (ctrl.products) {
return;
}
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;
}
});
}).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(result) {
var verId = (result.selectedVersion ?
result.selectedVersion.id : null);
var testId = result.id;
var url = refstackApiUrl + '/results/' + testId;
ctrl.associateRequest = $http.put(url, {'product_version_id':
verId})
.success(function (data) {
result.product_version = result.selectedVersion;
if (result.selectedVersion) {
result.product_version.product_info =
result.selectedProduct;
}
result.productEdit = false;
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/**
* Get all versions for a product.
*/
function getProductVersions(result) {
if (!result.selectedProduct) {
result.productVersions = [];
result.selectedVersion = null;
return;
}
var url = refstackApiUrl + '/products/' +
result.selectedProduct.id + '/versions';
ctrl.getVersionsRequest = $http.get(url)
.success(function (data) {
result.productVersions = data;
// If the test result isn't already associated to a
// version, default it to the null version.
if (!result.product_version) {
angular.forEach(data, function(ver) {
if (!ver.version) {
result.selectedVersion = ver;
}
});
}
}).error(function (error) {
raiseAlert('danger', error.title, error.detail);
});
}
/**
* Instantiate variables needed for editing product/version
* associations.
*/
function prepVersionEdit(result) {
result.productEdit = true;
if (result.product_version) {
result.selectedProduct =
ctrl.products[result.product_version.product_info.id];
}
result.selectedVersion = result.product_version;
ctrl.getProductVersions(result);
}
}
})();

View File

@ -0,0 +1,61 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" aria-hidden="true" ng-click="modal.close()">&times;</button>
<h4>Edit Vendor</h4>
<p>Make changes to your vendor.</p>
</div>
<div class="modal-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text"
class="form-control"
id="name"
ng-model="modal.vendor.name">
<br />
<label for="description">Description</label>
<textarea type="text"
class="form-control"
id="description"
ng-model="modal.vendor.description"
rows="4"
wrap="off">
</textarea>
<br />
<label for="properties">Properties</label>
<small><span class="text-muted glyphicon glyphicon-info-sign" title="Add arbitrary custom properties to your vendor."></span></small>
<div class="row" ng-repeat="(index, prop) in modal.vendorProperties">
<div class="col-md-2">
<input type="text"
class="form-control"
ng-model="prop.key">
</div>
<div class="col-md-6">
<input type="text"
class="form-control"
ng-model="prop.value">
</div>
<div class="col-md-2">
<a class="text-danger glyphicon glyphicon-remove"
title="Delete this property?"
ng-click="modal.removeProperty(index)"
style='top:8px'></a>
</div>
</div>
<div><small><a ng-click="modal.addField()"><span class="glyphicon glyphicon-plus"></span>&nbsp;Add new property</a></small></div>
</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

@ -10,12 +10,21 @@
<span ng-show="ctrl.vendor.type == 3" class="text-success">Official</span>
<br />
<strong>Name:</strong> {{ctrl.vendor.name}}<br />
<strong>Description:</strong> {{ctrl.vendor.description}}<br />
<strong>Description:</strong> {{ctrl.vendor.description || '-'}}<br />
<div ng-if="ctrl.vendorProperties">
<strong>Properties:</strong>
<ul>
<li ng-repeat="(key, value) in ctrl.vendorProperties">
<em>{{key}}</em>: {{value}}
</li>
</ul>
</div>
</div>
</div>
<div class="pull-right">
<a ng-if="ctrl.vendor.canDelete" href="javascript:void(0)" ng-click="ctrl.deleteVendor()" confirm="Are you sure you want to delete this vendor?">Delete</a><br />
<a ng-if="ctrl.vendor.canEdit" href="javascript:void(0)" ng-click="ctrl.openVendorEditModal()">Edit</a><br />
<a ng-if="ctrl.vendor.canRegister" href="javascript:void(0)" ng-click="ctrl.registerVendor()">Register with Foundation</a><br />
<a ng-if="ctrl.vendor.canApprove && ctrl.vendor.type == 2" href="javascript:void(0)" ng-click="ctrl.approveVendor()"
confirm="Are you sure you want to approve this vendor?">Approve registration</a><br />

View File

@ -21,7 +21,7 @@
VendorController.$inject = [
'$rootScope', '$scope', '$http', '$state', '$stateParams', '$window',
'refstackApiUrl', 'raiseAlert', 'confirmModal'
'$uibModal', 'refstackApiUrl', 'raiseAlert', 'confirmModal'
];
/**
@ -30,7 +30,7 @@
* view details of the Vendor and manage users.
*/
function VendorController($rootScope, $scope, $http, $state, $stateParams,
$window, refstackApiUrl, raiseAlert, confirmModal) {
$window, $uibModal, refstackApiUrl, raiseAlert, confirmModal) {
var ctrl = this;
ctrl.getVendor = getVendor;
@ -41,6 +41,7 @@
ctrl.deleteVendor = deleteVendor;
ctrl.removeUserFromVendor = removeUserFromVendor;
ctrl.addUserToVendor = addUserToVendor;
ctrl.openVendorEditModal = openVendorEditModal;
/** The vendor id extracted from the URL route. */
ctrl.vendorId = $stateParams.vendorID;
@ -62,7 +63,8 @@
$http.get(contentUrl).success(function(data) {
ctrl.vendor = data;
var isAdmin = $rootScope.auth.currentUser.is_admin;
ctrl.vendor.canDelete = ctrl.vendor.type != 0
ctrl.vendor.canDelete = ctrl.vendor.canEdit =
ctrl.vendor.type != 0
&& (ctrl.vendor.can_manage || isAdmin);
ctrl.vendor.canRegister =
ctrl.vendor.type == 1;
@ -181,5 +183,121 @@
error.detail);
});
}
/**
* This will open the modal that will allow a user to edit
*/
function openVendorEditModal() {
$uibModal.open({
templateUrl: '/components/vendors/partials' +
'/vendorEditModal.html',
backdrop: true,
windowClass: 'modal',
animation: true,
controller: 'VendorEditModalController as modal',
size: 'lg',
resolve: {
vendor: function () {
return ctrl.vendor;
}
}
});
}
}
angular
.module('refstackApp')
.controller('VendorEditModalController', VendorEditModalController);
VendorEditModalController.$inject = [
'$uibModalInstance', '$http', '$state', 'vendor', 'refstackApiUrl'
];
/**
* Vendor Edit Modal Controller
* This controls the modal that allows editing a vendor.
*/
function VendorEditModalController($uibModalInstance, $http, $state,
vendor, refstackApiUrl) {
var ctrl = this;
ctrl.close = close;
ctrl.addField = addField;
ctrl.saveChanges = saveChanges;
ctrl.removeProperty = removeProperty;
ctrl.vendor = vendor;
ctrl.vendorProperties = [];
parseVendorProperties();
/**
* Close the vendor edit modal.
*/
function close() {
$uibModalInstance.dismiss('exit');
}
/**
* Push a blank property key-value pair into the vendorProperties
* array. This will spawn new input boxes.
*/
function addField() {
ctrl.vendorProperties.push({'key': '', 'value': ''});
}
/**
* Send a PUT request to the server with the changes.
*/
function saveChanges() {
ctrl.showError = false;
ctrl.showSuccess = false;
var url = [refstackApiUrl, '/vendors/', ctrl.vendor.id].join('');
var properties = propertiesToJson();
var content = {'name': ctrl.vendor.name,
'description': ctrl.vendor.description,
'properties': properties};
$http.put(url, content).success(function() {
ctrl.showSuccess = true;
$state.reload();
}).error(function(error) {
ctrl.showError = true;
ctrl.error = error.detail;
});
}
/**
* Remove a property from the vendorProperties array at the given index.
*/
function removeProperty(index) {
ctrl.vendorProperties.splice(index, 1);
}
/**
* Parse the vendor properties and put them in a format more suitable
* for forms.
*/
function parseVendorProperties() {
var props = angular.fromJson(ctrl.vendor.properties);
angular.forEach(props, function(value, key) {
ctrl.vendorProperties.push({'key': key, 'value': value});
});
}
/**
* Convert the list of property objects to a dict containing the
* each key-value pair..
*/
function propertiesToJson() {
var properties = {};
for (var i = 0, len = ctrl.vendorProperties.length; i < len; i++) {
var prop = ctrl.vendorProperties[i];
if (prop.key && prop.value) {
properties[prop.key] = prop.value;
}
}
return properties;
}
}
})();

View File

@ -74,8 +74,7 @@
&& $rootScope.auth.currentUser.is_admin;
/**
* This will contact the Refstack API to get a listing of test run
* results.
* This will contact the Refstack API to get a listing of vendors
*/
function update() {
ctrl.showError = false;

View File

@ -47,6 +47,8 @@
<script src="components/logout/logoutController.js"></script>
<script src="components/vendors/vendorController.js"></script>
<script src="components/vendors/vendorsController.js"></script>
<script src="components/products/productController.js"></script>
<script src="components/products/productsController.js"></script>
<!-- Filters -->
<script src="shared/filters.js"></script>

View File

@ -31,7 +31,9 @@
return function (objects) {
var array = [];
angular.forEach(objects, function (object, key) {
object.id = key;
if (!('id' in object)) {
object.id = key;
}
array.push(object);
});
return array;

View File

@ -19,29 +19,27 @@ RefStack
<li ng-class="{ active: header.isActive('/about')}"><a ui-sref="about">About</a></li>
<li ng-class="{ active: header.isActive('/guidelines')}"><a ui-sref="guidelines">DefCore Guidelines</a></li>
<li ng-class="{ active: header.isActive('/community_results')}"><a ui-sref="communityResults">Community Results</a></li>
<!---
<li ng-class="{ active: header.isCatalogActive('public')}" class="dropdown" uib-dropdown>
<a role="button" class="dropdown-toggle" uib-dropdown-toggle>
Catalog <strong class="caret"></strong>
</a>
<ul class="dropdown-menu">
<li><a ui-sref="publicVendors">Vendors</a></li>
<li><a ui-sref="publicProducts">Products</a></li>
</ul>
</li>
--->
</ul>
<ul class="nav navbar-nav navbar-right">
<li ng-class="{ active: header.isActive('/user_results')}" ng-if="auth.isAuthenticated"><a ui-sref="userResults">My Results</a></li>
<!---
<li ng-if="auth.isAuthenticated" ng-class="{ active: header.isCatalogActive('user')}" class="dropdown" uib-dropdown>
<a role="button" class="dropdown-toggle" uib-dropdown-toggle>
My Catalog <strong class="caret"></strong>
</a>
<ul class="dropdown-menu">
<li><a ui-sref="userVendors">My Vendors</a></li>
<li><a ui-sref="userProducts">My Products</a></li>
</ul>
</li>
--->
<li ng-class="{ active: header.isActive('/profile')}" ng-if="auth.isAuthenticated"><a ui-sref="profile">Profile</a></li>
<li ng-if="auth.isAuthenticated"><a href="" ng-click="auth.doSignOut()">Sign Out</a></li>
<li ng-if="!auth.isAuthenticated"><a href="" ng-click="auth.doSignIn()">Sign In / Sign Up</a></li>

View File

@ -56,7 +56,8 @@
* public or user one.
*/
function isCatalogActive(type) {
return ctrl.isActive('/' + type + '_vendors');
return ctrl.isActive('/' + type + '_vendors')
|| ctrl.isActive('/' + type + '_products');
}
}
})();

View File

@ -215,6 +215,8 @@ describe('Refstack controllers', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('ResultsController', {$scope: scope});
$httpBackend.when('GET', fakeApiUrl +
'/results?page=1').respond(fakeResponse);
}));
it('should fetch the first page of results with proper URL args',
@ -301,6 +303,51 @@ describe('Refstack controllers', function () {
expect(ctrl.versionList).toEqual(['2015.04.json',
'2015.03.json']);
});
it('should have a function to get products manageable by a user',
function () {
var prodResp = {'products': [{'id': 'abc',
'can_manage': true},
{'id': 'foo',
'can_manage': false}]};
ctrl.products = null;
$httpBackend.expectGET(fakeApiUrl + '/products')
.respond(200, prodResp);
ctrl.getUserProducts();
$httpBackend.flush();
var expected = {'abc': {'id': 'abc', 'can_manage': true}};
expect(ctrl.products).toEqual(expected);
});
it('should have a function to associate a product version to a test',
function () {
var result = {'id': 'bar',
'selectedVersion': {'id': 'foo'},
'selectedProduct': {'id': 'prod'}};
ctrl.products = null;
$httpBackend.expectPUT(fakeApiUrl + '/results/bar')
.respond(201);
ctrl.associateProductVersion(result);
$httpBackend.flush();
var expected = {'id': 'foo', 'product_info': {'id': 'prod'}};
expect(result.product_version).toEqual(expected);
});
it('should have a function to get product versions',
function () {
var result = {'id': 'bar',
'selectedProduct': {'id': 'prod'}};
var verResp = [{'id': 'ver1', 'version': '1.0'},
{'id': 'ver2', 'version': null}];
ctrl.products = null;
$httpBackend.expectGET(fakeApiUrl + '/products/prod/versions')
.respond(200, verResp);
ctrl.getProductVersions(result);
$httpBackend.flush();
expect(result.productVersions).toEqual(verResp);
var expected = {'id': 'ver2', 'version': null};
expect(result.selectedVersion).toEqual(expected);
});
});
describe('ResultsReportController', function () {
@ -883,7 +930,59 @@ describe('Refstack controllers', function () {
});
});
describe('VendorsController', function() {
describe('VendorEditModalController', function() {
var ctrl, modalInstance, state;
var fakeVendor = {'name': 'Foo', 'description': 'Bar', 'id': '1234',
'properties': {'key1': 'value1', 'key2': 'value2'}};
beforeEach(inject(function ($controller) {
modalInstance = {
dismiss: jasmine.createSpy('modalInstance.dismiss')
};
state = {
reload: jasmine.createSpy('state.reload')
};
ctrl = $controller('VendorEditModalController',
{$uibModalInstance: modalInstance, $state: state,
vendor: fakeVendor}
);
}));
it('should be able to add/remove properties',
function () {
var expected = [{'key': 'key1', 'value': 'value1'},
{'key': 'key2', 'value': 'value2'}];
expect(ctrl.vendorProperties).toEqual(expected);
ctrl.removeProperty(0);
expected = [{'key': 'key2', 'value': 'value2'}];
expect(ctrl.vendorProperties).toEqual(expected);
ctrl.addField();
expected = [{'key': 'key2', 'value': 'value2'},
{'key': '', 'value': ''}];
expect(ctrl.vendorProperties).toEqual(expected);
});
it('should have a function to save changes',
function () {
var expectedContent = {
'name': 'Foo', 'description': 'Bar',
'properties': {'key1': 'value1', 'key2': 'value2'}
};
$httpBackend.expectPUT(
fakeApiUrl + '/vendors/1234', expectedContent)
.respond(200, '');
ctrl.saveChanges();
$httpBackend.flush();
});
it('should have a function to exit the modal',
function () {
ctrl.close();
expect(modalInstance.dismiss).toHaveBeenCalledWith('exit');
});
});
describe('VendorsController', function () {
var rootScope, scope, ctrl;
var fakeResp = {'vendors': [{'can_manage': true,
'type': 3,
@ -949,4 +1048,277 @@ describe('Refstack controllers', function () {
$httpBackend.flush();
});
});
describe('ProductsController', function() {
var rootScope, scope, ctrl;
var vendResp = {'vendors': [{'can_manage': true,
'type': 3,
'name': 'Foo',
'id': '123'}]};
var prodResp = {'products': [{'id': 'abc',
'product_type': 1,
'public': 1,
'name': 'Foo Product',
'organization_id': '123'}]};
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
rootScope = $rootScope.$new();
rootScope.auth = {'currentUser' : {'is_admin': false,
'openid': 'foo'}
};
ctrl = $controller('ProductsController',
{$rootScope: rootScope, $scope: scope}
);
$httpBackend.when('GET', fakeApiUrl +
'/vendors').respond(vendResp);
$httpBackend.when('GET', fakeApiUrl +
'/products').respond(prodResp);
}));
it('should have a function to get/update vendors',
function () {
$httpBackend.flush();
var newVendResp = {'vendors': [{'name': 'Foo',
'id': '123',
'can_manage': true},
{'name': 'Bar',
'id': '345',
'can_manage': false}]};
$httpBackend.expectGET(fakeApiUrl + '/vendors')
.respond(200, newVendResp);
ctrl.updateVendors();
$httpBackend.flush();
expect(ctrl.allVendors).toEqual({'123': {'name': 'Foo',
'id': '123',
'can_manage': true},
'345': {'name': 'Bar',
'id': '345',
'can_manage': false}});
expect(ctrl.vendors).toEqual([{'name': 'Foo',
'id': '123',
'can_manage': true}]);
});
it('should have a function to get products',
function () {
$httpBackend.expectGET(fakeApiUrl + '/products')
.respond(200, prodResp);
ctrl.update();
$httpBackend.flush();
expect(ctrl.rawData).toEqual(prodResp);
});
it('should have a function to update the view',
function () {
$httpBackend.flush();
ctrl.allVendors = {'123': {'name': 'Foo',
'id': '123',
'can_manage': true}};
ctrl.updateData();
var expectedData = {'products': [{'id': 'abc',
'product_type': 1,
'public': 1,
'name': 'Foo Product',
'organization_id': '123'}]};
expect(ctrl.data).toEqual(expectedData);
});
it('should have a function to map product types with descriptions',
function () {
expect(ctrl.getProductTypeDescription(0)).toEqual('Distro');
expect(ctrl.getProductTypeDescription(1))
.toEqual('Public Cloud');
expect(ctrl.getProductTypeDescription(2))
.toEqual('Hosted Private Cloud');
expect(ctrl.getProductTypeDescription(5)).toEqual('Unknown');
});
});
describe('ProductController', function() {
var rootScope, scope, stateParams, ctrl;
var fakeProdResp = {'product_type': 1,
'product_ref_id': null,
'name': 'Good Stuff',
'created_at': '2016-01-01 01:02:03',
'updated_at': '2016-06-15 01:02:04',
'properties': null,
'organization_id': 'fake-org-id',
'public': true,
'can_manage': true,
'created_by_user': 'fake-open-id',
'type': 0,
'id': '1234',
'description': 'some description'};
var fakeVersionResp = [{'id': 'asdf',
'cpid': null,
'version': '1.0',
'product_id': '1234'}];
var fakeTestsResp = {'pagination': {'current_page': 1,
'total_pages': 1},
'results':[{'id': 'foo-test'}]};
var fakeVendorResp = {'id': 'fake-org-id',
'type': 3,
'can_manage': true,
'properties' : {},
'name': 'Foo Vendor',
'description': 'foo bar'};
var fakeWindow = {
location: {
href: ''
}
};
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
rootScope = $rootScope.$new();
stateParams = {id: 1234};
rootScope.auth = {'currentUser' : {'is_admin': false,
'openid': 'foo'}
};
ctrl = $controller('ProductController',
{$rootScope: rootScope, $scope: scope,
$stateParams: stateParams, $window: fakeWindow}
);
$httpBackend.when('GET', fakeApiUrl +
'/products/1234').respond(fakeProdResp);
$httpBackend.when('GET', fakeApiUrl +
'/products/1234/versions').respond(fakeVersionResp);
$httpBackend.when('GET', fakeApiUrl +
'/results?page=1&product_id=1234').respond(fakeTestsResp);
$httpBackend.when('GET', fakeApiUrl +
'/vendors/fake-org-id').respond(fakeVendorResp);
}));
it('should have a function to get product information',
function () {
$httpBackend.expectGET(fakeApiUrl + '/products/1234')
.respond(200, fakeProdResp);
$httpBackend.expectGET(fakeApiUrl + '/vendors/fake-org-id')
.respond(200, fakeVendorResp);
ctrl.getProduct();
$httpBackend.flush();
expect(ctrl.product).toEqual(fakeProdResp);
expect(ctrl.vendor).toEqual(fakeVendorResp);
});
it('should have a function to get a list of product versions',
function () {
$httpBackend
.expectGET(fakeApiUrl + '/products/1234/versions')
.respond(200, fakeVersionResp);
ctrl.getProductVersions();
$httpBackend.flush();
expect(ctrl.productVersions).toEqual(fakeVersionResp);
});
it('should have a function to delete a product',
function () {
$httpBackend.expectDELETE(fakeApiUrl + '/products/1234')
.respond(202, '');
ctrl.deleteProduct();
$httpBackend.flush();
expect(fakeWindow.location.href).toEqual('/');
});
it('should have a function to delete a product version',
function () {
$httpBackend
.expectDELETE(fakeApiUrl + '/products/1234/versions/abc')
.respond(204, '');
ctrl.deleteProductVersion('abc');
$httpBackend.flush();
});
it('should have a function to add a product version',
function () {
ctrl.newProductVersion = 'abc';
$httpBackend.expectPOST(
fakeApiUrl + '/products/1234/versions',
{version: 'abc'})
.respond(200, {'id': 'foo'});
ctrl.addProductVersion();
$httpBackend.flush();
});
it('should have a function to get tests on a product',
function () {
ctrl.getProductTests();
$httpBackend.flush();
expect(ctrl.testsData).toEqual(fakeTestsResp.results);
expect(ctrl.currentPage).toEqual(1);
});
it('should have a function to unassociate a test from a product',
function () {
ctrl.testsData = [{'id': 'foo-test'}];
$httpBackend.expectPUT(
fakeApiUrl + '/results/foo-test',
{product_version_id: null})
.respond(200, {'id': 'foo-test'});
ctrl.unassociateTest(0);
$httpBackend.flush();
expect(ctrl.testsData).toEqual([]);
});
it('should have a function to switch the publicity of a project',
function () {
ctrl.product = {'public': true};
$httpBackend.expectPUT(fakeApiUrl + '/products/1234',
{'public': false})
.respond(200, fakeProdResp);
ctrl.switchProductPublicity();
$httpBackend.flush();
});
it('should have a method to open a modal for version management',
function () {
var modal;
inject(function ($uibModal) {
modal = $uibModal;
});
spyOn(modal, 'open');
ctrl.openVersionModal();
expect(modal.open).toHaveBeenCalled();
});
});
describe('ProductVersionModalController', function() {
var ctrl, modalInstance, state, parent;
var fakeVersion = {'id': 'asdf', 'cpid': null,
'version': '1.0','product_id': '1234'};
beforeEach(inject(function ($controller) {
modalInstance = {
dismiss: jasmine.createSpy('modalInstance.dismiss')
};
parent = {
deleteProductVersion: jasmine.createSpy('deleteProductVersion')
};
ctrl = $controller('ProductVersionModalController',
{$uibModalInstance: modalInstance, $state: state,
version: fakeVersion, parent: parent}
);
}));
it('should have a function to prompt a version deletion',
function () {
ctrl.deleteProductVersion();
expect(parent.deleteProductVersion)
.toHaveBeenCalledWith('asdf');
expect(modalInstance.dismiss).toHaveBeenCalledWith('exit');
});
it('should have a function to save changes',
function () {
ctrl.version.cpid = 'some-cpid';
var expectedContent = { 'cpid': 'some-cpid'};
$httpBackend.expectPUT(
fakeApiUrl + '/products/1234/versions/asdf',
expectedContent).respond(200, '');
ctrl.saveChanges();
$httpBackend.flush();
});
});
});

View File

@ -20,6 +20,9 @@ END_DATE = 'end_date'
CPID = 'cpid'
PAGE = 'page'
SIGNED = 'signed'
VERIFICATION_STATUS = 'verification_status'
PRODUCT_ID = 'product_id'
ALL_PRODUCT_TESTS = 'all_product_tests'
OPENID = 'openid'
USER_PUBKEYS = 'pubkeys'
@ -50,6 +53,10 @@ USER_OPENID = 'user_openid'
USER = 'user'
SHARED_TEST_RUN = 'shared'
# Test verification statuses
TEST_NOT_VERIFIED = 0
TEST_VERIFIED = 1
# Roles
ROLE_USER = 'user'
ROLE_OWNER = 'owner'

View File

@ -19,6 +19,7 @@ import json
import uuid
from oslo_config import cfg
from oslo_db.exception import DBReferenceError
from oslo_log import log
import pecan
from pecan.secure import secure
@ -35,6 +36,92 @@ LOG = log.getLogger(__name__)
CONF = cfg.CONF
class VersionsController(validation.BaseRestControllerWithValidation):
"""/v1/products/<product_id>/versions handler."""
__validator__ = validators.ProductVersionValidator
@pecan.expose('json')
def get(self, id):
"""Get all versions for a product."""
product = db.get_product(id)
vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id))
if not product['public'] and not is_admin:
pecan.abort(403, 'Forbidden.')
allowed_keys = ['id', 'product_id', 'version', 'cpid']
return db.get_product_versions(id, allowed_keys=allowed_keys)
@pecan.expose('json')
def get_one(self, id, version_id):
"""Get specific version information."""
product = db.get_product(id)
vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id))
if not product['public'] and not is_admin:
pecan.abort(403, 'Forbidden.')
allowed_keys = ['id', 'product_id', 'version', 'cpid']
return db.get_product_version(version_id, allowed_keys=allowed_keys)
@secure(api_utils.is_authenticated)
@pecan.expose('json')
def post(self, id):
"""'secure' decorator doesn't work at store_item. it must be here."""
self.product_id = id
return super(VersionsController, self).post()
@pecan.expose('json')
def store_item(self, version_info):
"""Add a new version for the product."""
if (not api_utils.check_user_is_product_admin(self.product_id) and
not api_utils.check_user_is_foundation_admin()):
pecan.abort(403, 'Forbidden.')
creator = api_utils.get_user_id()
pecan.response.status = 201
return db.add_product_version(self.product_id, version_info['version'],
creator, version_info.get('cpid'))
@secure(api_utils.is_authenticated)
@pecan.expose('json', method='PUT')
def put(self, id, version_id, **kw):
"""Update details for a specific version.
Endpoint: /v1/products/<product_id>/versions/<version_id>
"""
if (not api_utils.check_user_is_product_admin(id) and
not api_utils.check_user_is_foundation_admin()):
pecan.abort(403, 'Forbidden.')
version_info = {'id': version_id}
if 'cpid' in kw:
version_info['cpid'] = kw['cpid']
version = db.update_product_version(version_info)
pecan.response.status = 200
return version
@secure(api_utils.is_authenticated)
@pecan.expose('json')
def delete(self, id, version_id):
"""Delete a product version.
Endpoint: /v1/products/<product_id>/versions/<version_id>
"""
if (not api_utils.check_user_is_product_admin(id) and
not api_utils.check_user_is_foundation_admin()):
pecan.abort(403, 'Forbidden.')
try:
db.delete_product_version(version_id)
except DBReferenceError:
pecan.abort(400, 'Unable to delete. There are still tests '
'associated to this product version.')
pecan.response.status = 204
class ProductsController(validation.BaseRestControllerWithValidation):
"""/v1/products handler."""
@ -44,10 +131,12 @@ class ProductsController(validation.BaseRestControllerWithValidation):
"action": ["POST"],
}
versions = VersionsController()
@pecan.expose('json')
def get(self):
"""Get information of all products."""
allowed_keys = ['id', 'name', 'description', 'product_id', 'type',
allowed_keys = ['id', 'name', 'description', 'product_ref_id', 'type',
'product_type', 'public', 'organization_id']
user = api_utils.get_user_id()
is_admin = user in db.get_foundation_users()
@ -83,18 +172,21 @@ class ProductsController(validation.BaseRestControllerWithValidation):
@pecan.expose('json')
def get_one(self, id):
"""Get information about product."""
product = db.get_product(id)
allowed_keys = ['id', 'name', 'description',
'product_ref_id', 'product_type',
'public', 'properties', 'created_at', 'updated_at',
'organization_id', 'created_by_user', 'type']
product = db.get_product(id, allowed_keys=allowed_keys)
vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id))
if not is_admin and not product['public']:
pecan.abort(403, 'Forbidden.')
if not is_admin:
allowed_keys = ['id', 'name', 'description', 'product_id', 'type',
'product_type', 'public', 'organization_id']
admin_only_keys = ['created_by_user', 'created_at', 'updated_at',
'properties']
for key in product.keys():
if key not in allowed_keys:
if key in admin_only_keys:
product.pop(key)
product['can_manage'] = is_admin
@ -114,7 +206,7 @@ class ProductsController(validation.BaseRestControllerWithValidation):
if product['product_type'] == const.DISTRO
else const.CLOUD)
if product['type'] == const.SOFTWARE:
product['product_id'] = six.text_type(uuid.uuid4())
product['product_ref_id'] = six.text_type(uuid.uuid4())
vendor_id = product.pop('organization_id', None)
if not vendor_id:
# find or create default vendor for new product
@ -151,8 +243,8 @@ class ProductsController(validation.BaseRestControllerWithValidation):
product_info['name'] = kw['name']
if 'description' in kw:
product_info['description'] = kw['description']
if 'product_id' in kw:
product_info['product_id'] = kw['product_id']
if 'product_ref_id' in kw:
product_info['product_ref_id'] = kw['product_ref_id']
if 'public' in kw:
# user can mark product as public only if
# his/her vendor is public(official)
@ -174,11 +266,12 @@ class ProductsController(validation.BaseRestControllerWithValidation):
@pecan.expose('json')
def delete(self, id):
"""Delete product."""
product = db.get_product(id)
vendor_id = product['organization_id']
if (not api_utils.check_user_is_foundation_admin() and
not api_utils.check_user_is_vendor_admin(vendor_id)):
not api_utils.check_user_is_product_admin(id)):
pecan.abort(403, 'Forbidden.')
db.delete_product(id)
try:
db.delete_product(id)
except DBReferenceError:
pecan.abort(400, 'Unable to delete. There are still tests '
'associated to versions of this product.')
pecan.response.status = 204

View File

@ -76,6 +76,10 @@ class MetadataController(rest.RestController):
@pecan.expose('json')
def post(self, test_id, key):
"""Save value for key in test run metadata."""
test = db.get_test(test_id)
if test['verification_status'] == const.TEST_VERIFIED:
pecan.abort(403, 'Can not add/alter a new metadata key for a '
'verified test run.')
db.save_test_meta_item(test_id, key, pecan.request.body)
pecan.response.status = 201
@ -84,6 +88,10 @@ class MetadataController(rest.RestController):
@pecan.expose('json')
def delete(self, test_id, key):
"""Delete key from test run metadata."""
test = db.get_test(test_id)
if test['verification_status'] == const.TEST_VERIFIED:
pecan.abort(403, 'Can not delete a metadata key for a '
'verified test run.')
db.delete_test_meta_item(test_id, key)
pecan.response.status = 204
@ -103,7 +111,9 @@ class ResultsController(validation.BaseRestControllerWithValidation):
if user_role in (const.ROLE_FOUNDATION, const.ROLE_OWNER):
test_info = db.get_test(
test_id, allowed_keys=['id', 'cpid', 'created_at',
'duration_seconds', 'meta']
'duration_seconds', 'meta',
'product_version',
'verification_status']
)
else:
test_info = db.get_test(test_id)
@ -113,6 +123,12 @@ class ResultsController(validation.BaseRestControllerWithValidation):
'user_role': user_role})
if user_role not in (const.ROLE_FOUNDATION, const.ROLE_OWNER):
# Don't expose product information if product is not public.
if (test_info.get('product_version') and
not test_info['product_version']['product_info']['public']):
test_info['product_version'] = None
test_info['meta'] = {
k: v for k, v in six.iteritems(test_info['meta'])
if k in MetadataController.rw_access_keys
@ -142,6 +158,10 @@ class ResultsController(validation.BaseRestControllerWithValidation):
@api_utils.check_permissions(level=const.ROLE_OWNER)
def delete(self, test_id):
"""Delete test run."""
test = db.get_test(test_id)
if test['verification_status'] == const.TEST_VERIFIED:
pecan.abort(403, 'Can not delete a verified test run.')
db.delete_test(test_id)
pecan.response.status = 204
@ -161,10 +181,23 @@ class ResultsController(validation.BaseRestControllerWithValidation):
const.START_DATE,
const.END_DATE,
const.CPID,
const.SIGNED
const.SIGNED,
const.VERIFICATION_STATUS,
const.PRODUCT_ID
]
filters = api_utils.parse_input_params(expected_input_params)
if const.PRODUCT_ID in filters:
product = db.get_product(filters[const.PRODUCT_ID])
vendor_id = product['organization_id']
is_admin = (api_utils.check_user_is_foundation_admin() or
api_utils.check_user_is_vendor_admin(vendor_id))
if is_admin:
filters[const.ALL_PRODUCT_TESTS] = True
elif not product['public']:
pecan.abort(403, 'Forbidden.')
records_count = db.get_test_records_count(filters)
page_number, total_pages_number = \
api_utils.get_page_number(records_count)
@ -172,13 +205,18 @@ class ResultsController(validation.BaseRestControllerWithValidation):
try:
per_page = CONF.api.results_per_page
results = db.get_test_records(page_number, per_page, filters)
is_foundation = api_utils.check_user_is_foundation_admin()
for result in results:
# Only show all metadata if the user is the owner or a member
# of the Foundation group.
if (not api_utils.check_owner(result['id']) and
not api_utils.check_user_is_foundation_admin()):
if not (api_utils.check_owner(result['id']) or is_foundation):
# Don't expose product info if the product is not public.
if (result.get('product_version') and not
result['product_version']['product_info']['public']):
result['product_version'] = None
# Only show all metadata if the user is the owner or a
# member of the Foundation group.
result['meta'] = {
k: v for k, v in six.iteritems(result['meta'])
if k in MetadataController.rw_access_keys
@ -194,7 +232,65 @@ class ResultsController(validation.BaseRestControllerWithValidation):
}}
except Exception as ex:
LOG.debug('An error occurred during '
'operation with database: %s' % ex)
pecan.abort(400)
'operation with database: %s' % str(ex))
pecan.abort(500)
return page
@api_utils.check_permissions(level=const.ROLE_OWNER)
@pecan.expose('json')
def put(self, test_id, **kw):
"""Update a test result."""
test_info = {'id': test_id}
is_foundation_admin = api_utils.check_user_is_foundation_admin()
if 'product_version_id' in kw:
test = db.get_test(test_id)
if test['verification_status'] == const.TEST_VERIFIED:
pecan.abort(403, 'Can not update product_version_id for a '
'verified test run.')
if kw['product_version_id']:
# Verify that the user is a member of the product's vendor.
version = db.get_product_version(kw['product_version_id'],
allowed_keys=['product_id'])
is_vendor_admin = (
api_utils
.check_user_is_product_admin(version['product_id'])
)
else:
# No product vendor to check membership for, so just set
# is_vendor_admin to True.
is_vendor_admin = True
kw['product_version_id'] = None
if not is_vendor_admin and not is_foundation_admin:
pecan.abort(403, 'Forbidden.')
test_info['product_version_id'] = kw['product_version_id']
if 'verification_status' in kw:
if not is_foundation_admin:
pecan.abort(403, 'You do not have permission to change a '
'verification status.')
if kw['verification_status'] not in (0, 1):
pecan.abort(400, 'Invalid verification_status value: %d' %
kw['verification_status'])
# Check pre-conditions are met to mark a test verified.
if (kw['verification_status'] == 1 and
not (db.get_test_meta_key(test_id, 'target') and
db.get_test_meta_key(test_id, 'guideline') and
db.get_test_meta_key(test_id, const.SHARED_TEST_RUN))):
pecan.abort(403, 'In order to mark a test verified, the '
'test must be shared and have been '
'associated to a guideline and target '
'program.')
test_info['verification_status'] = kw['verification_status']
test = db.update_test(test_info)
pecan.response.status = 201
return test

View File

@ -19,6 +19,7 @@ import json
import six
from oslo_config import cfg
from oslo_db.exception import DBReferenceError
from oslo_log import log
import pecan
from pecan import rest
@ -196,7 +197,11 @@ class VendorsController(validation.BaseRestControllerWithValidation):
pecan.abort(403, 'Forbidden.')
_check_is_not_foundation(vendor_id)
db.delete_organization(vendor_id)
try:
db.delete_organization(vendor_id)
except DBReferenceError:
pecan.abort(400, 'Unable to delete. There are still tests '
'associated to products for this vendor.')
pecan.response.status = 204
@secure(api_utils.is_authenticated)
@ -204,65 +209,99 @@ class VendorsController(validation.BaseRestControllerWithValidation):
def action(self, vendor_id, **kw):
"""Handler for action on Vendor object."""
params = list()
for param in ('register', 'approve', 'deny'):
for param in ('register', 'approve', 'deny', 'cancel'):
if param in kw:
params.append(param)
if len(params) != 1:
raise api_exc.ValidationError('Invalid actions in the body: ')
raise api_exc.ValidationError(
'Invalid actions in the body: ' + str(params))
vendor = db.get_organization(vendor_id)
if 'register' in params:
self.register(vendor_id)
self.register(vendor)
elif 'approve' in params:
self.approve(vendor_id)
self.approve(vendor)
elif 'cancel' in params:
self.cancel(vendor)
else:
self.deny(vendor_id, kw.get('reason'))
self.deny(vendor, kw.get('reason'))
def register(self, vendor_id):
def register(self, vendor):
"""Handler for applying for registration with Foundation."""
if not api_utils.check_user_is_vendor_admin(vendor_id):
if not api_utils.check_user_is_vendor_admin(vendor['id']):
pecan.abort(403, 'Forbidden.')
_check_is_not_foundation(vendor_id)
_check_is_not_foundation(vendor['id'])
if vendor['type'] != const.PRIVATE_VENDOR:
raise api_exc.ValidationError(
'Invalid organization state for this action.')
# change vendor type to pending
org_info = {
'id': vendor_id,
'id': vendor['id'],
'type': const.PENDING_VENDOR}
db.update_organization(org_info)
def approve(self, vendor_id):
def approve(self, vendor):
"""Handler for making vendor official."""
if not api_utils.check_user_is_foundation_admin():
pecan.abort(403, 'Forbidden.')
_check_is_not_foundation(vendor_id)
_check_is_not_foundation(vendor['id'])
if vendor['type'] != const.PENDING_VENDOR:
raise api_exc.ValidationError(
'Invalid organization state for this action.')
# change vendor type to public
vendor = db.get_organization(vendor_id)
props = vendor.get('properties')
props = json.loads(props) if props else {}
props.pop('reason', None)
org_info = {
'id': vendor_id,
'id': vendor['id'],
'type': const.OFFICIAL_VENDOR,
'properties': json.dumps(props)}
db.update_organization(org_info)
def deny(self, vendor_id, reason):
"""Handler for denying a vendor."""
if not reason:
raise api_exc.ValidationError('Param "reason" can not be empty')
def cancel(self, vendor):
"""Handler for canceling registration.
This action available to user. It allows him to cancel
registrationand move state of his vendor from pending
to private.
"""
if not api_utils.check_user_is_vendor_admin(vendor['id']):
pecan.abort(403, 'Forbidden.')
_check_is_not_foundation(vendor['id'])
if vendor['type'] != const.PENDING_VENDOR:
raise api_exc.ValidationError(
'Invalid organization state for this action.')
# change vendor type back to private
org_info = {
'id': vendor['id'],
'type': const.PRIVATE_VENDOR}
db.update_organization(org_info)
def deny(self, vendor, reason):
"""Handler for denying a vendor."""
if not api_utils.check_user_is_foundation_admin():
pecan.abort(403, 'Forbidden.')
_check_is_not_foundation(vendor_id)
_check_is_not_foundation(vendor['id'])
if not reason:
raise api_exc.ValidationError('Param "reason" can not be empty')
if vendor['type'] != const.PENDING_VENDOR:
raise api_exc.ValidationError(
'Invalid organization state for this action.')
vendor = db.get_organization(vendor_id)
props = vendor.get('properties')
props = json.loads(props) if props else {}
props['reason'] = reason
# change vendor type back to private
org_info = {
'id': vendor_id,
'id': vendor['id'],
'type': const.PRIVATE_VENDOR,
'properties': json.dumps(props)}
db.update_organization(org_info)

View File

@ -249,8 +249,16 @@ def check_owner(test_id):
"""Check that user has access to specified test run as owner."""
if not is_authenticated():
return False
user = db.get_test_meta_key(test_id, const.USER)
return user and user == get_user_id()
test = db.get_test(test_id)
# If the test is owned by a product.
if test.get('product_version_id'):
version = db.get_product_version(test['product_version_id'])
return check_user_is_product_admin(version['product_id'])
# Otherwise, check the user ownership.
else:
user = db.get_test_meta_key(test_id, const.USER)
return user and user == get_user_id()
def check_permissions(level):
@ -330,3 +338,10 @@ def check_user_is_vendor_admin(vendor_id):
user = get_user_id()
org_users = db.get_organization_users(vendor_id)
return user in org_users
def check_user_is_product_admin(product_id):
"""Check if the current user is in the vendor group for a product."""
product = db.get_product(product_id)
vendor_id = product['organization_id']
return check_user_is_vendor_admin(vendor_id)

View File

@ -220,6 +220,7 @@ class ProductValidator(BaseValidator):
'description': {'type': 'string'},
'product_type': {'type': 'integer'},
'organization_id': {'type': 'string', 'format': 'uuid_hex'},
'version': {'type': 'string'}
},
'required': ['name', 'product_type'],
'additionalProperties': False
@ -231,3 +232,21 @@ class ProductValidator(BaseValidator):
body = json.loads(request.body)
self.check_emptyness(body, ['name', 'product_type'])
class ProductVersionValidator(BaseValidator):
"""Validate adding product versions."""
schema = {
'type': 'object',
'properties': {
'version': {'type': 'string'},
'cpid': {'type': 'string'}
},
'required': ['version'],
'additionalProperties': False
}
def validate(self, request):
"""Validate product version data."""
super(ProductVersionValidator, self).validate(request)

View File

@ -64,6 +64,14 @@ def delete_test(test_id):
return IMPL.delete_test(test_id)
def update_test(test_info):
"""Update test from the given test_info dictionary.
:param test_info: The test
"""
return IMPL.update_test(test_info)
def get_test_results(test_id):
"""Get all passed tempest tests for a specified test run.
@ -202,9 +210,9 @@ def update_product(product_info):
return IMPL.update_product(product_info)
def get_product(id):
def get_product(id, allowed_keys=None):
"""Get product by id."""
return IMPL.get_product(id)
return IMPL.get_product(id, allowed_keys=allowed_keys)
def delete_product(id):
@ -251,3 +259,37 @@ def get_products(allowed_keys=None):
def get_products_by_user(user_openid, allowed_keys=None):
"""Get all products that user can manage."""
return IMPL.get_products_by_user(user_openid, allowed_keys=allowed_keys)
def get_product_by_version(product_version_id, allowed_keys=None):
"""Get product info from a product version ID."""
return IMPL.get_product_by_version(product_version_id,
allowed_keys=allowed_keys)
def get_product_version(product_version_id, allowed_keys=None):
"""Get details of a specific version given the id."""
return IMPL.get_product_version(product_version_id,
allowed_keys=allowed_keys)
def get_product_versions(product_id, allowed_keys=None):
"""Get all versions for a product."""
return IMPL.get_product_versions(product_id, allowed_keys=allowed_keys)
def add_product_version(product_id, version, creator, cpid=None,
allowed_keys=None):
"""Add a new product version."""
return IMPL.add_product_version(product_id, version, creator, cpid,
allowed_keys=allowed_keys)
def update_product_version(product_version_info):
"""Update product version from product_info_version dictionary."""
return IMPL.update_product_version(product_version_info)
def delete_product_version(product_version_id):
"""Delete a product version."""
return IMPL.delete_product_version(product_version_id)

View File

@ -0,0 +1,28 @@
"""Add product_version_id column to test.
Revision ID: 23843be3da52
Revises: 35bf54e2c13c
Create Date: 2016-07-30 18:15:52.429610
"""
# revision identifiers, used by Alembic.
revision = '23843be3da52'
down_revision = '35bf54e2c13c'
MYSQL_CHARSET = 'utf8'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Upgrade DB."""
op.add_column('test', sa.Column('product_version_id', sa.String(36),
nullable=True))
op.create_foreign_key('fk_test_prod_version_id', 'test', 'product_version',
['product_version_id'], ['id'])
def downgrade():
"""Downgrade DB."""
op.drop_constraint('fk_test_prod_version_id', 'test', type_="foreignkey")
op.drop_column('test', 'product_version_id')

View File

@ -0,0 +1,46 @@
"""Add Product version table.
Also product_ref_id is removed from the product table.
Revision ID: 35bf54e2c13c
Revises: 709452f38a5c
Create Date: 2016-07-30 17:59:57.912306
"""
# revision identifiers, used by Alembic.
revision = '35bf54e2c13c'
down_revision = '709452f38a5c'
MYSQL_CHARSET = 'utf8'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Upgrade DB."""
op.create_table(
'product_version',
sa.Column('updated_at', sa.DateTime()),
sa.Column('deleted_at', sa.DateTime()),
sa.Column('deleted', sa.Integer, default=0),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('created_by_user', sa.String(128), nullable=False),
sa.Column('id', sa.String(36), nullable=False),
sa.Column('product_id', sa.String(36), nullable=False),
sa.Column('version', sa.String(length=36), nullable=True),
sa.Column('cpid', sa.String(length=36)),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['created_by_user'], ['user.openid'], ),
sa.UniqueConstraint('product_id', 'version', name='prod_ver_uc'),
mysql_charset=MYSQL_CHARSET
)
op.drop_column('product', 'product_ref_id')
def downgrade():
"""Downgrade DB."""
op.drop_table('product_version')
op.add_column('product',
sa.Column('product_ref_id', sa.String(36), nullable=True))

View File

@ -0,0 +1,28 @@
"""Add verification_status field to test.
Revision ID: 59df512e82f
Revises: 23843be3da52
Create Date: 2016-09-26 11:51:08.955006
"""
# revision identifiers, used by Alembic.
revision = '59df512e82f'
down_revision = '23843be3da52'
MYSQL_CHARSET = 'utf8'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Upgrade DB."""
op.add_column('test', sa.Column('verification_status',
sa.Integer,
nullable=False,
default=0))
def downgrade():
"""Downgrade DB."""
op.drop_column('test', 'verification_status')

View File

@ -19,3 +19,8 @@ def upgrade():
"""Upgrade DB."""
op.alter_column('product', 'product_id', nullable=True,
type_=sa.String(36))
def downgrade():
"""Downgrade DB."""
pass

View File

@ -0,0 +1,27 @@
"""Rename product_id to product_ref_id.
Revision ID: 709452f38a5c
Revises: 7093ca478d35
Create Date: 2016-06-27 13:10:00
"""
# revision identifiers, used by Alembic.
revision = '709452f38a5c'
down_revision = '7093ca478d35'
MYSQL_CHARSET = 'utf8'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Upgrade DB."""
op.alter_column('product', 'product_id', new_column_name='product_ref_id',
type_=sa.String(36))
def downgrade():
"""Downgrade DB."""
op.alter_column('product', 'product_ref_id', new_column_name='product_id',
type_=sa.String(36))

View File

@ -158,6 +158,24 @@ def delete_test(test_id):
raise NotFound('Test result %s not found' % test_id)
def update_test(test_info):
"""Update test from the given test_info dictionary."""
session = get_session()
_id = test_info.get('id')
test = session.query(models.Test).filter_by(id=_id).first()
if test is None:
raise NotFound('Test result with id %s not found' % _id)
keys = ['product_version_id', 'verification_status']
for key in keys:
if key in test_info:
setattr(test, key, test_info[key])
with session.begin():
test.save(session=session)
return _to_dict(test)
def get_test_meta_key(test_id, key, default=None):
"""Get metadata value related to specified test run."""
session = get_session()
@ -220,6 +238,18 @@ def _apply_filters_for_query(query, filters):
if cpid:
query = query.filter(models.Test.cpid == cpid)
verification_status = filters.get(api_const.VERIFICATION_STATUS)
if verification_status:
query = query.filter(models.Test.verification_status ==
verification_status)
if api_const.PRODUCT_ID in filters:
query = (query
.join(models.ProductVersion)
.filter(models.ProductVersion.product_id ==
filters[api_const.PRODUCT_ID]))
all_product_tests = filters.get(api_const.ALL_PRODUCT_TESTS)
signed = api_const.SIGNED in filters
# If we only want to get the user's test results.
if signed:
@ -228,7 +258,9 @@ def _apply_filters_for_query(query, filters):
.filter(models.TestMeta.meta_key == api_const.USER)
.filter(models.TestMeta.value == filters[api_const.OPENID])
)
else:
elif not all_product_tests:
# Get all non-signed (aka anonymously uploaded) test results
# along with signed but shared test results.
signed_results = (query.session
.query(models.TestMeta.test_id)
.filter_by(meta_key=api_const.USER))
@ -237,6 +269,7 @@ def _apply_filters_for_query(query, filters):
.filter_by(meta_key=api_const.SHARED_TEST_RUN))
query = (query.filter(models.Test.id.notin_(signed_results))
.union(query.filter(models.Test.id.in_(shared_results))))
return query
@ -424,6 +457,12 @@ def delete_organization(organization_id):
"""delete organization by id."""
session = get_session()
with session.begin():
product_ids = (session
.query(models.Product.id)
.filter_by(organization_id=organization_id))
(session.query(models.ProductVersion).
filter(models.ProductVersion.product_id.in_(product_ids)).
delete(synchronize_session=False))
(session.query(models.Product).
filter_by(organization_id=organization_id).
delete(synchronize_session=False))
@ -435,9 +474,10 @@ def delete_organization(organization_id):
def add_product(product_info, creator):
"""Add product."""
product = models.Product()
product.id = str(uuid.uuid4())
product.type = product_info['type']
product.product_type = product_info['product_type']
product.product_id = product_info.get('product_id')
product.product_ref_id = product_info.get('product_ref_id')
product.name = product_info['name']
product.description = product_info.get('description')
product.organization_id = product_info['organization_id']
@ -448,6 +488,12 @@ def add_product(product_info, creator):
session = get_session()
with session.begin():
product.save(session=session)
product_version = models.ProductVersion()
product_version.created_by_user = creator
product_version.version = product_info.get('version')
product_version.product_id = product.id
product_version.save(session=session)
return _to_dict(product)
@ -459,7 +505,7 @@ def update_product(product_info):
if product is None:
raise NotFound('Product with id %s not found' % _id)
keys = ['name', 'description', 'product_id', 'public', 'properties']
keys = ['name', 'description', 'product_ref_id', 'public', 'properties']
for key in keys:
if key in product_info:
setattr(product, key, product_info[key])
@ -469,19 +515,22 @@ def update_product(product_info):
return _to_dict(product)
def get_product(id):
def get_product(id, allowed_keys=None):
"""Get product by id."""
session = get_session()
product = session.query(models.Product).filter_by(id=id).first()
if product is None:
raise NotFound('Product with id "%s" not found' % id)
return _to_dict(product)
return _to_dict(product, allowed_keys=allowed_keys)
def delete_product(id):
"""delete product by id."""
session = get_session()
with session.begin():
(session.query(models.ProductVersion)
.filter_by(product_id=id)
.delete(synchronize_session=False))
(session.query(models.Product).filter_by(id=id).
delete(synchronize_session=False))
@ -588,3 +637,73 @@ def get_products_by_user(user_openid, allowed_keys=None):
.order_by(models.Organization.created_at.desc()).all())
items = [item[0] for item in items]
return _to_dict(items, allowed_keys=allowed_keys)
def get_product_by_version(product_version_id, allowed_keys=None):
"""Get product info from a product version ID."""
session = get_session()
product = (session.query(models.Product).join(models.ProductVersion)
.filter(models.ProductVersion.id == product_version_id).first())
return _to_dict(product, allowed_keys=allowed_keys)
def get_product_version(product_version_id, allowed_keys=None):
"""Get details of a specific version given the id."""
session = get_session()
version = (
session.query(models.ProductVersion)
.filter_by(id=product_version_id).first()
)
if version is None:
raise NotFound('Version with id "%s" not found' % product_version_id)
return _to_dict(version, allowed_keys=allowed_keys)
def get_product_versions(product_id, allowed_keys=None):
"""Get all versions for a product."""
session = get_session()
version_info = (
session.query(models.ProductVersion)
.filter_by(product_id=product_id).all()
)
return _to_dict(version_info, allowed_keys=allowed_keys)
def add_product_version(product_id, version, creator, cpid, allowed_keys=None):
"""Add a new product version."""
product_version = models.ProductVersion()
product_version.created_by_user = creator
product_version.version = version
product_version.product_id = product_id
product_version.cpid = cpid
session = get_session()
with session.begin():
product_version.save(session=session)
return _to_dict(product_version, allowed_keys=allowed_keys)
def update_product_version(product_version_info):
"""Update product version from product_info_version dictionary."""
session = get_session()
_id = product_version_info.get('id')
version = session.query(models.ProductVersion).filter_by(id=_id).first()
if version is None:
raise NotFound('Product version with id %s not found' % _id)
# Only allow updating cpid.
keys = ['cpid']
for key in keys:
if key in product_version_info:
setattr(version, key, product_version_info[key])
with session.begin():
version.save(session=session)
return _to_dict(version)
def delete_product_version(product_version_id):
"""Delete a product version."""
session = get_session()
with session.begin():
(session.query(models.ProductVersion).filter_by(id=product_version_id).
delete(synchronize_session=False))

View File

@ -59,11 +59,16 @@ class Test(BASE, RefStackBase): # pragma: no cover
duration_seconds = sa.Column(sa.Integer, nullable=False)
results = orm.relationship('TestResults', backref='test')
meta = orm.relationship('TestMeta', backref='test')
product_version_id = sa.Column(sa.String(36),
sa.ForeignKey('product_version.id'),
nullable=True, unique=False)
verification_status = sa.Column(sa.Integer, nullable=False, default=0)
product_version = orm.relationship('ProductVersion', backref='test')
@property
def _extra_keys(self):
"""Relation should be pointed directly."""
return ['results', 'meta']
return ['results', 'meta', 'product_version']
@property
def metadata_keys(self):
@ -74,7 +79,8 @@ class Test(BASE, RefStackBase): # pragma: no cover
@property
def default_allowed_keys(self):
"""Default keys."""
return 'id', 'created_at', 'duration_seconds', 'meta'
return ('id', 'created_at', 'duration_seconds', 'meta',
'verification_status', 'product_version')
class TestResults(BASE, RefStackBase): # pragma: no cover
@ -225,7 +231,6 @@ class Product(BASE, RefStackBase): # pragma: no cover
id = sa.Column(sa.String(36), primary_key=True,
default=lambda: six.text_type(uuid.uuid4()))
product_id = sa.Column(sa.String(36), nullable=True)
name = sa.Column(sa.String(80), nullable=False)
description = sa.Column(sa.Text())
organization_id = sa.Column(sa.String(36),
@ -241,6 +246,33 @@ class Product(BASE, RefStackBase): # pragma: no cover
@property
def default_allowed_keys(self):
"""Default keys."""
return ('id', 'name', 'description', 'product_id', 'product_type',
'public', 'properties', 'created_at', 'updated_at',
'organization_id', 'created_by_user', 'type')
return ('id', 'name', 'organization_id', 'public')
class ProductVersion(BASE, RefStackBase):
"""Product Version definition."""
__tablename__ = 'product_version'
__table_args__ = (
sa.UniqueConstraint('product_id', 'version'),
)
id = sa.Column(sa.String(36), primary_key=True,
default=lambda: six.text_type(uuid.uuid4()))
product_id = sa.Column(sa.String(36), sa.ForeignKey('product.id'),
index=True, nullable=False, unique=False)
version = sa.Column(sa.String(length=36), nullable=True)
cpid = sa.Column(sa.String(36), nullable=True)
created_by_user = sa.Column(sa.String(128), sa.ForeignKey('user.openid'),
nullable=False)
product_info = orm.relationship('Product', backref='product_version')
@property
def _extra_keys(self):
"""Relation should be pointed directly."""
return ['product_info']
@property
def default_allowed_keys(self):
"""Default keys."""
return ('id', 'version', 'cpid', 'product_info')

View File

@ -135,6 +135,17 @@ class TestProductsEndpoint(api.FunctionalTest):
self.get_json,
self.URL + post_response.get('id'))
mock_get_user.return_value = 'foo-open-id'
# Make product public.
product_info = {'id': post_response.get('id'), 'public': 1}
db.update_product(product_info)
# Test when getting product info when not owner/foundation.
get_response = self.get_json(self.URL + post_response.get('id'))
self.assertNotIn('created_by_user', get_response)
self.assertNotIn('created_at', get_response)
self.assertNotIn('updated_at', get_response)
@mock.patch('refstack.api.utils.get_user_id', return_value='test-open-id')
def test_delete(self, mock_get_user):
"""Test delete request."""
@ -179,3 +190,97 @@ class TestProductsEndpoint(api.FunctionalTest):
"""Test get(list) request with no items in DB."""
results = self.get_json(self.URL)
self.assertEqual([], results['products'])
class TestProductVersionEndpoint(api.FunctionalTest):
"""Test case for the 'products/<product_id>/version' API endpoint."""
def setUp(self):
super(TestProductVersionEndpoint, self).setUp()
self.config_fixture = config_fixture.Config()
self.CONF = self.useFixture(self.config_fixture).conf
self.user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(self.user_info)
patcher = mock.patch('refstack.api.utils.get_user_id')
self.addCleanup(patcher.stop)
self.mock_get_user = patcher.start()
self.mock_get_user.return_value = 'test-open-id'
product = json.dumps(FAKE_PRODUCT)
response = self.post_json('/v1/products/', params=product)
self.product_id = response['id']
self.URL = '/v1/products/' + self.product_id + '/versions/'
def test_get(self):
"""Test getting a list of versions."""
response = self.get_json(self.URL)
# Product created without version specified.
self.assertIsNone(response[0]['version'])
# Create a version
post_response = self.post_json(self.URL,
params=json.dumps({'version': '1.0'}))
response = self.get_json(self.URL)
self.assertEqual(2, len(response))
self.assertEqual(post_response['version'], response[1]['version'])
def test_get_one(self):
""""Test get a specific version."""
# Create a version
post_response = self.post_json(self.URL,
params=json.dumps({'version': '2.0'}))
version_id = post_response['id']
response = self.get_json(self.URL + version_id)
self.assertEqual(post_response['version'], response['version'])
# Test nonexistent version.
self.assertRaises(webtest.app.AppError, self.get_json,
self.URL + 'sdsdsds')
def test_post(self):
"""Test creating a product version."""
version = {'cpid': '123', 'version': '5.0'}
post_response = self.post_json(self.URL, params=json.dumps(version))
get_response = self.get_json(self.URL + post_response['id'])
self.assertEqual(version['cpid'], get_response['cpid'])
self.assertEqual(version['version'], get_response['version'])
self.assertEqual(self.product_id, get_response['product_id'])
self.assertIn('id', get_response)
# Test 'version' not in response body.
response = self.post_json(self.URL, expect_errors=True,
params=json.dumps({'cpid': '123'}))
self.assertEqual(400, response.status_code)
def test_put(self):
"""Test updating a product version."""
post_response = self.post_json(self.URL,
params=json.dumps({'version': '6.0'}))
version_id = post_response['id']
response = self.get_json(self.URL + version_id)
self.assertIsNone(response['cpid'])
props = {'cpid': '1233'}
self.put_json(self.URL + version_id, params=json.dumps(props))
response = self.get_json(self.URL + version_id)
self.assertEqual('1233', response['cpid'])
def test_delete(self):
"""Test deleting a product version."""
post_response = self.post_json(self.URL,
params=json.dumps({'version': '7.0'}))
version_id = post_response['id']
self.delete(self.URL + version_id)
self.assertRaises(webtest.app.AppError, self.get_json,
self.URL + 'version_id')

View File

@ -15,11 +15,14 @@
import json
import uuid
import mock
from oslo_config import fixture as config_fixture
import six
import webtest.app
from refstack.api import constants as api_const
from refstack.api import validators
from refstack import db
from refstack.tests import api
FAKE_TESTS_RESULT = {
@ -79,6 +82,107 @@ class TestResultsEndpoint(api.FunctionalTest):
self.URL,
params=results)
@mock.patch('refstack.api.utils.check_owner')
@mock.patch('refstack.api.utils.check_user_is_foundation_admin')
@mock.patch('refstack.api.utils.get_user_id', return_value='test-open-id')
def test_put(self, mock_user, mock_check_foundation, mock_check_owner):
"""Test results endpoint with put request."""
results = json.dumps(FAKE_TESTS_RESULT)
test_response = self.post_json(self.URL, params=results)
test_id = test_response.get('test_id')
url = self.URL + test_id
user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(user_info)
fake_product = {
'name': 'product name',
'description': 'product description',
'product_type': api_const.CLOUD,
}
# Create a product
product_response = self.post_json('/v1/products/',
params=json.dumps(fake_product))
# Create a product version
version_url = '/v1/products/' + product_response['id'] + '/versions/'
version_response = self.post_json(version_url,
params=json.dumps({'version': '1'}))
# Test Foundation admin can put.
mock_check_foundation.return_value = True
body = {'product_version_id': version_response['id']}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertEqual(version_response['id'],
get_response['product_version']['id'])
# Test when product_version_id is None.
body = {'product_version_id': None}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertIsNone(get_response['product_version'])
# Test when test verification preconditions are not met.
body = {'verification_status': api_const.TEST_VERIFIED}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Share the test run.
db.save_test_meta_item(test_id, api_const.SHARED_TEST_RUN, True)
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Now associate guideline and target program. Now we should be
# able to mark a test verified.
db.save_test_meta_item(test_id, 'target', 'platform')
db.save_test_meta_item(test_id, 'guideline', '2016.01.json')
put_response = self.put_json(url, params=json.dumps(body))
self.assertEqual(api_const.TEST_VERIFIED,
put_response['verification_status'])
# Unshare the test, and check that we can mark it not verified.
db.delete_test_meta_item(test_id, api_const.SHARED_TEST_RUN)
body = {'verification_status': api_const.TEST_NOT_VERIFIED}
put_response = self.put_json(url, params=json.dumps(body))
self.assertEqual(api_const.TEST_NOT_VERIFIED,
put_response['verification_status'])
# Test when verification_status value is invalid.
body = {'verification_status': 111}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(400, put_response.status_code)
# Check test owner can put.
mock_check_foundation.return_value = False
mock_check_owner.return_value = True
body = {'product_version_id': version_response['id']}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertEqual(version_response['id'],
get_response['product_version']['id'])
# Test non-Foundation user can't change verification_status.
body = {'verification_status': 1}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Test unauthorized put.
mock_check_foundation.return_value = False
mock_check_owner.return_value = False
self.assertRaises(webtest.app.AppError,
self.put_json,
url,
params=json.dumps(body))
def test_get_one(self):
"""Test get request."""
results = json.dumps(FAKE_TESTS_RESULT)
@ -223,3 +327,83 @@ class TestResultsEndpoint(api.FunctionalTest):
url = '/v1/results?end_date=1000-01-01 12:00:00'
filtering_results = self.get_json(url)
self.assertEqual([], filtering_results['results'])
@mock.patch('refstack.api.utils.get_user_id')
def test_get_with_product_id(self, mock_get_user):
user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(user_info)
mock_get_user.return_value = 'test-open-id'
fake_product = {
'name': 'product name',
'description': 'product description',
'product_type': api_const.CLOUD,
}
product = json.dumps(fake_product)
response = self.post_json('/v1/products/', params=product)
product_id = response['id']
# Create a version.
version_url = '/v1/products/' + product_id + '/versions'
version = {'cpid': '123', 'version': '6.0'}
post_response = self.post_json(version_url, params=json.dumps(version))
version_id = post_response['id']
# Create a test and associate it to the product version and user.
results = json.dumps(FAKE_TESTS_RESULT)
post_response = self.post_json('/v1/results', params=results)
test_id = post_response['test_id']
test_info = {'id': test_id, 'product_version_id': version_id}
db.update_test(test_info)
db.save_test_meta_item(test_id, api_const.USER, 'test-open-id')
url = self.URL + '?page=1&product_id=' + product_id
# Test GET.
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
# Test unauthorized.
mock_get_user.return_value = 'test-foo-id'
response = self.get_json(url, expect_errors=True)
self.assertEqual(403, response.status_code)
# Make product public.
product_info = {'id': product_id, 'public': 1}
db.update_product(product_info)
# Test result is not shared yet, so no tests should return.
response = self.get_json(url)
self.assertFalse(response['results'])
# Share the test run.
db.save_test_meta_item(test_id, api_const.SHARED_TEST_RUN, 1)
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
@mock.patch('refstack.api.utils.check_owner')
def test_delete(self, mock_check_owner):
results = json.dumps(FAKE_TESTS_RESULT)
test_response = self.post_json(self.URL, params=results)
test_id = test_response.get('test_id')
url = self.URL + test_id
mock_check_owner.return_value = True
# Test can't delete verified test run.
db.update_test({'id': test_id, 'verification_status': 1})
resp = self.delete(url, expect_errors=True)
self.assertEqual(403, resp.status_code)
# Test can delete verified test run.
db.update_test({'id': test_id, 'verification_status': 0})
resp = self.delete(url, expect_errors=True)
self.assertEqual(204, resp.status_code)

View File

@ -140,7 +140,9 @@ class ResultsControllerTestCase(BaseControllerTestCase):
mock_get_test_res.assert_called_once_with('fake_arg')
mock_get_test.assert_called_once_with(
'fake_arg', allowed_keys=['id', 'cpid', 'created_at',
'duration_seconds', 'meta']
'duration_seconds', 'meta',
'product_version',
'verification_status']
)
@mock.patch('refstack.db.store_results')
@ -247,7 +249,9 @@ class ResultsControllerTestCase(BaseControllerTestCase):
const.START_DATE,
const.END_DATE,
const.CPID,
const.SIGNED
const.SIGNED,
const.VERIFICATION_STATUS,
const.PRODUCT_ID
]
page_number = 1
total_pages_number = 10
@ -285,13 +289,21 @@ class ResultsControllerTestCase(BaseControllerTestCase):
db_get_test.assert_called_once_with(page_number, per_page, filters)
@mock.patch('refstack.db.get_test')
@mock.patch('refstack.db.delete_test')
def test_delete(self, mock_db_delete):
def test_delete(self, mock_db_delete, mock_get_test):
self.mock_get_user_role.return_value = const.ROLE_OWNER
self.controller.delete('test_id')
self.assertEqual(204, self.mock_response.status)
# Verified test deletion attempt should raise error.
mock_get_test.return_value = {'verification_status':
const.TEST_VERIFIED}
self.assertRaises(webob.exc.HTTPError,
self.controller.delete, 'test_id')
self.mock_get_user_role.return_value = const.ROLE_USER
self.mock_abort.side_effect = webob.exc.HTTPError()
self.assertRaises(webob.exc.HTTPError,
self.controller.delete, 'test_id')
@ -633,9 +645,13 @@ class MetadataControllerTestCase(BaseControllerTestCase):
self.mock_get_user_role.return_value = const.ROLE_FOUNDATION
self.assertEqual(42, self.controller.get_one('test_id', 'user'))
@mock.patch('refstack.db.get_test')
@mock.patch('refstack.db.save_test_meta_item')
def test_post(self, mock_save_test_meta_item):
def test_post(self, mock_save_test_meta_item, mock_get_test):
self.mock_get_user_role.return_value = const.ROLE_OWNER
mock_get_test.return_value = {
'verification_status': const.TEST_NOT_VERIFIED
}
# Test trying to post a valid key.
self.controller.post('test_id', 'shared')
@ -653,9 +669,13 @@ class MetadataControllerTestCase(BaseControllerTestCase):
self.assertRaises(webob.exc.HTTPError,
self.controller.post, 'test_id', 'shared')
@mock.patch('refstack.db.get_test')
@mock.patch('refstack.db.delete_test_meta_item')
def test_delete(self, mock_delete_test_meta_item):
def test_delete(self, mock_delete_test_meta_item, mock_get_test):
self.mock_get_user_role.return_value = const.ROLE_OWNER
mock_get_test.return_value = {
'verification_status': const.TEST_NOT_VERIFIED
}
self.controller.delete('test_id', 'shared')
self.assertEqual(204, self.mock_response.status)
mock_delete_test_meta_item.assert_called_once_with('test_id', 'shared')

View File

@ -336,16 +336,19 @@ class APIUtilsTestCase(base.BaseTestCase):
@mock.patch('refstack.api.utils.check_user_is_foundation_admin')
@mock.patch('pecan.abort', side_effect=exc.HTTPError)
@mock.patch('refstack.db.get_test_meta_key')
@mock.patch('refstack.db.get_test')
@mock.patch.object(api_utils, 'is_authenticated')
@mock.patch.object(api_utils, 'get_user_id')
def test_check_get_user_role(self, mock_get_user_id,
mock_is_authenticated,
mock_get_test,
mock_get_test_meta_key,
mock_pecan_abort,
mock_check_foundation):
# Check user level
mock_check_foundation.return_value = False
mock_get_test_meta_key.return_value = None
mock_get_test.return_value = {}
self.assertEqual(const.ROLE_USER, api_utils.get_user_role('fake_test'))
api_utils.enforce_permissions('fake_test', const.ROLE_USER)
self.assertRaises(exc.HTTPError, api_utils.enforce_permissions,
@ -409,10 +412,12 @@ class APIUtilsTestCase(base.BaseTestCase):
@mock.patch('refstack.api.utils.check_user_is_foundation_admin')
@mock.patch('pecan.abort', side_effect=exc.HTTPError)
@mock.patch('refstack.db.get_test_meta_key')
@mock.patch('refstack.db.get_test')
@mock.patch.object(api_utils, 'is_authenticated')
@mock.patch.object(api_utils, 'get_user_id')
def test_check_permissions(self, mock_get_user_id,
mock_is_authenticated,
mock_get_test,
mock_get_test_meta_key,
mock_pecan_abort,
mock_foundation_check):
@ -437,6 +442,7 @@ class APIUtilsTestCase(base.BaseTestCase):
private_test = 'fake_test'
mock_get_user_id.return_value = 'fake_openid'
mock_get_test.return_value = {}
mock_get_test_meta_key.side_effect = lambda *args: {
(public_test, const.USER): None,
(private_test, const.USER): 'fake_openid',

View File

@ -254,6 +254,21 @@ class DBBackendTestCase(base.BaseTestCase):
.first.return_value = None
self.assertRaises(api.NotFound, db.delete_test, 'fake_id')
@mock.patch.object(api, 'get_session')
@mock.patch.object(api, '_to_dict', side_effect=lambda x: x)
def test_update_test(self, mock_to_dict, mock_get_session):
session = mock_get_session.return_value
mock_test = mock.Mock()
session.query.return_value.filter_by.return_value\
.first.return_value = mock_test
test_info = {'product_version_id': '123'}
api.update_test(test_info)
mock_get_session.assert_called_once_with()
mock_test.save.assert_called_once_with(session=session)
session.begin.assert_called_once_with()
@mock.patch('refstack.db.sqlalchemy.api.models')
@mock.patch.object(api, 'get_session')
def test_get_test_meta_key(self, mock_get_session, mock_models):
@ -678,17 +693,23 @@ class DBBackendTestCase(base.BaseTestCase):
@mock.patch.object(api, 'get_session')
@mock.patch('refstack.db.sqlalchemy.models.Product')
@mock.patch('refstack.db.sqlalchemy.models.ProductVersion')
@mock.patch.object(api, '_to_dict', side_effect=lambda x: x)
def test_product_add(self, mock_to_dict, mock_product, mock_get_session):
def test_product_add(self, mock_to_dict, mock_version,
mock_product, mock_get_session):
session = mock_get_session.return_value
version = mock_version.return_value
product = mock_product.return_value
product_info = {'product_id': 'hash_or_guid', 'name': 'a',
product_info = {'product_ref_id': 'hash_or_guid', 'name': 'a',
'organization_id': 'GUID0', 'type': 0,
'product_type': 0}
result = api.add_product(product_info, 'user-123')
self.assertEqual(result, product)
self.assertIsNotNone(product.id)
self.assertIsNotNone(version.id)
self.assertIsNotNone(version.product_id)
self.assertIsNone(version.version)
mock_get_session.assert_called_once_with()
product.save.assert_called_once_with(session=session)
@ -710,12 +731,13 @@ class DBBackendTestCase(base.BaseTestCase):
product.id = '123'
filtered.first.return_value = product
product_info = {'product_id': '098', 'name': 'a', 'description': 'b',
'creator_openid': 'abc', 'organization_id': '1',
'type': 0, 'product_type': 0, 'id': '123'}
product_info = {'product_ref_id': '098', 'name': 'a',
'description': 'b', 'creator_openid': 'abc',
'organization_id': '1', 'type': 0, 'product_type': 0,
'id': '123'}
api.update_product(product_info)
self.assertEqual('098', product.product_id)
self.assertEqual('098', product.product_ref_id)
self.assertIsNone(product.created_by_user)
self.assertIsNone(product.organization_id)
self.assertIsNone(product.type)
@ -747,7 +769,7 @@ class DBBackendTestCase(base.BaseTestCase):
@mock.patch.object(api, 'get_session',
return_value=mock.Mock(name='session'),)
@mock.patch('refstack.db.sqlalchemy.models.Product')
@mock.patch.object(api, '_to_dict', side_effect=lambda x: x)
@mock.patch.object(api, '_to_dict', side_effect=lambda x, allowed_keys: x)
def test_product_get(self, mock_to_dict, mock_model, mock_get_session):
_id = 12345
session = mock_get_session.return_value
@ -768,7 +790,9 @@ class DBBackendTestCase(base.BaseTestCase):
session = mock_get_session.return_value
db.delete_product('product_id')
session.query.assert_called_once_with(mock_models.Product)
session.query.return_value.filter_by.assert_has_calls((
mock.call(product_id='product_id'),
mock.call().delete(synchronize_session=False)))
session.query.return_value.filter_by.assert_has_calls((
mock.call(id='product_id'),
mock.call().delete(synchronize_session=False)))