Merge feature/vendor branch into master branch
Change-Id: I9b86d0797a50635af440e3aa0068db8860248e98
This commit is contained in:
commit
ac472fe494
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
26
refstack-ui/app/components/products/cloud.html
Normal file
26
refstack-ui/app/components/products/cloud.html
Normal 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>
|
26
refstack-ui/app/components/products/distro.html
Normal file
26
refstack-ui/app/components/products/distro.html
Normal 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>
|
13
refstack-ui/app/components/products/partials/management.html
Normal file
13
refstack-ui/app/components/products/partials/management.html
Normal 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>
|
137
refstack-ui/app/components/products/partials/testsTable.html
Normal file
137
refstack-ui/app/components/products/partials/testsTable.html
Normal 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>
|
29
refstack-ui/app/components/products/partials/versions.html
Normal file
29
refstack-ui/app/components/products/partials/versions.html
Normal 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>
|
||||
|
||||
<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>
|
@ -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>
|
357
refstack-ui/app/components/products/productController.js
Normal file
357
refstack-ui/app/components/products/productController.js
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
79
refstack-ui/app/components/products/products.html
Normal file
79
refstack-ui/app/components/products/products.html
Normal 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();"> 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>
|
204
refstack-ui/app/components/products/productsController.js
Normal file
204
refstack-ui/app/components/products/productsController.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
||||
|
61
refstack-ui/app/components/vendors/partials/vendorEditModal.html
vendored
Normal file
61
refstack-ui/app/components/vendors/partials/vendorEditModal.html
vendored
Normal 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()">×</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> 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>
|
11
refstack-ui/app/components/vendors/vendor.html
vendored
11
refstack-ui/app/components/vendors/vendor.html
vendored
@ -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 />
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
@ -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))
|
@ -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')
|
@ -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
|
||||
|
@ -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))
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
|
@ -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)))
|
||||
|
Loading…
x
Reference in New Issue
Block a user