Add tests to product page
This patch updates the product page to include tests associated to versions of the product. This allows filtering results based on a product_id. Tests associated with private products will have their product information hidden. Change-Id: Ic5b6b45c9e3d14d9c2cb36a8eba72f2a6e31d2aa
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| <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"> | ||||
| @@ -14,6 +15,8 @@ | ||||
|         <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"> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <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"> | ||||
| @@ -14,6 +15,8 @@ | ||||
|         <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"> | ||||
|   | ||||
							
								
								
									
										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> | ||||
| @@ -37,8 +37,12 @@ | ||||
|         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. */ | ||||
| @@ -49,8 +53,21 @@ | ||||
|             $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. | ||||
| @@ -147,6 +164,30 @@ | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 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. | ||||
|          */ | ||||
| @@ -161,6 +202,73 @@ | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 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. | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1107,6 +1107,9 @@ describe('Refstack controllers', function () { | ||||
|                                '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, | ||||
| @@ -1134,6 +1137,8 @@ describe('Refstack controllers', function () { | ||||
|                 '/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); | ||||
|         })); | ||||
| @@ -1189,6 +1194,26 @@ describe('Refstack controllers', function () { | ||||
|                 $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}; | ||||
|   | ||||
| @@ -21,6 +21,8 @@ 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' | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,8 @@ class VersionsController(validation.BaseRestControllerWithValidation): | ||||
|         if not product['public'] and not is_admin: | ||||
|             pecan.abort(403, 'Forbidden.') | ||||
|  | ||||
|         return db.get_product_versions(id) | ||||
|         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): | ||||
| @@ -62,8 +63,8 @@ class VersionsController(validation.BaseRestControllerWithValidation): | ||||
|                     api_utils.check_user_is_vendor_admin(vendor_id)) | ||||
|         if not product['public'] and not is_admin: | ||||
|             pecan.abort(403, 'Forbidden.') | ||||
|  | ||||
|         return db.get_product_version(version_id) | ||||
|         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') | ||||
| @@ -171,19 +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_ref_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 | ||||
|   | ||||
| @@ -112,7 +112,7 @@ class ResultsController(validation.BaseRestControllerWithValidation): | ||||
|             test_info = db.get_test( | ||||
|                 test_id, allowed_keys=['id', 'cpid', 'created_at', | ||||
|                                        'duration_seconds', 'meta', | ||||
|                                        'product_version_id', | ||||
|                                        'product_version', | ||||
|                                        'verification_status'] | ||||
|             ) | ||||
|         else: | ||||
| @@ -123,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 | ||||
| @@ -176,10 +182,22 @@ class ResultsController(validation.BaseRestControllerWithValidation): | ||||
|             const.END_DATE, | ||||
|             const.CPID, | ||||
|             const.SIGNED, | ||||
|             const.VERIFICATION_STATUS | ||||
|             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) | ||||
| @@ -187,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 | ||||
| @@ -209,8 +232,8 @@ 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 | ||||
|  | ||||
| @@ -229,7 +252,8 @@ class ResultsController(validation.BaseRestControllerWithValidation): | ||||
|  | ||||
|             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']) | ||||
|                 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']) | ||||
|   | ||||
| @@ -210,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): | ||||
|   | ||||
| @@ -243,6 +243,13 @@ def _apply_filters_for_query(query, filters): | ||||
|         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: | ||||
| @@ -251,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)) | ||||
| @@ -260,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 | ||||
|  | ||||
|  | ||||
| @@ -505,13 +515,13 @@ 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): | ||||
| @@ -653,7 +663,8 @@ 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) | ||||
|         session.query(models.ProductVersion) | ||||
|         .filter_by(product_id=product_id).all() | ||||
|     ) | ||||
|     return _to_dict(version_info, allowed_keys=allowed_keys) | ||||
|  | ||||
|   | ||||
| @@ -63,11 +63,12 @@ class Test(BASE, RefStackBase):  # pragma: no cover | ||||
|                                    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): | ||||
| @@ -79,7 +80,7 @@ class Test(BASE, RefStackBase):  # pragma: no cover | ||||
|     def default_allowed_keys(self): | ||||
|         """Default keys.""" | ||||
|         return ('id', 'created_at', 'duration_seconds', 'meta', | ||||
|                 'product_version_id', 'verification_status') | ||||
|                 'verification_status', 'product_version') | ||||
|  | ||||
|  | ||||
| class TestResults(BASE, RefStackBase):  # pragma: no cover | ||||
| @@ -245,9 +246,7 @@ class Product(BASE, RefStackBase):  # pragma: no cover | ||||
|     @property | ||||
|     def default_allowed_keys(self): | ||||
|         """Default keys.""" | ||||
|         return ('id', 'name', 'description', 'product_ref_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): | ||||
| @@ -266,8 +265,14 @@ class ProductVersion(BASE, RefStackBase): | ||||
|     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', 'product_id', 'version', 'cpid') | ||||
|         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.""" | ||||
| @@ -182,7 +193,7 @@ class TestProductsEndpoint(api.FunctionalTest): | ||||
|  | ||||
|  | ||||
| class TestProductVersionEndpoint(api.FunctionalTest): | ||||
|     """Test case for the 'product/<product_id>/version' API endpoint.""" | ||||
|     """Test case for the 'products/<product_id>/version' API endpoint.""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super(TestProductVersionEndpoint, self).setUp() | ||||
| @@ -218,7 +229,7 @@ class TestProductVersionEndpoint(api.FunctionalTest): | ||||
|  | ||||
|         response = self.get_json(self.URL) | ||||
|         self.assertEqual(2, len(response)) | ||||
|         self.assertEqual(post_response, response[1]) | ||||
|         self.assertEqual(post_response['version'], response[1]['version']) | ||||
|  | ||||
|     def test_get_one(self): | ||||
|         """"Test get a specific version.""" | ||||
| @@ -228,7 +239,7 @@ class TestProductVersionEndpoint(api.FunctionalTest): | ||||
|         version_id = post_response['id'] | ||||
|  | ||||
|         response = self.get_json(self.URL + version_id) | ||||
|         self.assertEqual(post_response, response) | ||||
|         self.assertEqual(post_response['version'], response['version']) | ||||
|  | ||||
|         # Test nonexistent version. | ||||
|         self.assertRaises(webtest.app.AppError, self.get_json, | ||||
| @@ -238,14 +249,17 @@ class TestProductVersionEndpoint(api.FunctionalTest): | ||||
|         """Test creating a product version.""" | ||||
|         version = {'cpid': '123', 'version': '5.0'} | ||||
|         post_response = self.post_json(self.URL, params=json.dumps(version)) | ||||
|         self.assertEqual(version['cpid'], post_response['cpid']) | ||||
|         self.assertEqual(version['version'], post_response['version']) | ||||
|         self.assertEqual(self.product_id, post_response['product_id']) | ||||
|         self.assertIn('id', post_response) | ||||
|  | ||||
|         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. | ||||
|         self.assertRaises(webtest.app.AppError, self.get_json, | ||||
|                           self.URL + '/sdsdsds') | ||||
|         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.""" | ||||
|   | ||||
| @@ -119,13 +119,13 @@ class TestResultsEndpoint(api.FunctionalTest): | ||||
|         self.put_json(url, params=json.dumps(body)) | ||||
|         get_response = self.get_json(url) | ||||
|         self.assertEqual(version_response['id'], | ||||
|                          get_response['product_version_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_id']) | ||||
|         self.assertIsNone(get_response['product_version']) | ||||
|  | ||||
|         # Test when test verification preconditions are not met. | ||||
|         body = {'verification_status': api_const.TEST_VERIFIED} | ||||
| @@ -167,7 +167,7 @@ class TestResultsEndpoint(api.FunctionalTest): | ||||
|         self.put_json(url, params=json.dumps(body)) | ||||
|         get_response = self.get_json(url) | ||||
|         self.assertEqual(version_response['id'], | ||||
|                          get_response['product_version_id']) | ||||
|                          get_response['product_version']['id']) | ||||
|  | ||||
|         # Test non-Foundation user can't change verification_status. | ||||
|         body = {'verification_status': 1} | ||||
| @@ -328,6 +328,67 @@ class TestResultsEndpoint(api.FunctionalTest): | ||||
|         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) | ||||
|   | ||||
| @@ -141,7 +141,7 @@ class ResultsControllerTestCase(BaseControllerTestCase): | ||||
|         mock_get_test.assert_called_once_with( | ||||
|             'fake_arg', allowed_keys=['id', 'cpid', 'created_at', | ||||
|                                       'duration_seconds', 'meta', | ||||
|                                       'product_version_id', | ||||
|                                       'product_version', | ||||
|                                       'verification_status'] | ||||
|         ) | ||||
|  | ||||
| @@ -251,6 +251,7 @@ class ResultsControllerTestCase(BaseControllerTestCase): | ||||
|             const.CPID, | ||||
|             const.SIGNED, | ||||
|             const.VERIFICATION_STATUS, | ||||
|             const.PRODUCT_ID | ||||
|         ] | ||||
|         page_number = 1 | ||||
|         total_pages_number = 10 | ||||
|   | ||||
| @@ -769,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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Paul Van Eck
					Paul Van Eck