Add filter for signed test results and related UI

With this patch users can list their own test results.

Change-Id: Ie2d944924f6ae966a13d0ca9908810c315ade5ab
This commit is contained in:
sslypushenko 2015-07-22 20:22:53 +03:00
parent 217cadd608
commit 2b89f65ad4
21 changed files with 178 additions and 136 deletions

View File

@ -26,8 +26,13 @@ refstackApp.config([
templateUrl: '/components/capabilities/capabilities.html',
controller: 'capabilitiesController'
}).
state('results', {
url: '/results',
state('community_results', {
url: '/community_results',
templateUrl: '/components/results/results.html',
controller: 'resultsController'
}).
state('user_results', {
url: '/user_results',
templateUrl: '/components/results/results.html',
controller: 'resultsController'
}).
@ -45,19 +50,46 @@ refstackApp.config([
]);
/**
* Try to authenticate user
* Injections in $rootscope
*/
refstackApp.run(['$http', '$rootScope', 'refstackApiUrl',
function($http, $rootScope, refstackApiUrl) {
refstackApp.run(['$http', '$rootScope', '$window', 'refstackApiUrl',
function($http, $rootScope, $window, refstackApiUrl) {
'use strict';
/**
* This function injects sign in function in all scopes
*/
$rootScope.auth = {};
var sign_in_url = refstackApiUrl + '/auth/signin';
$rootScope.auth.doSignIn = function () {
$window.location.href = sign_in_url;
};
/**
* This function injects sign out function in all scopes
*/
var sign_out_url = refstackApiUrl + '/auth/signout';
$rootScope.auth.doSignOut = function () {
$rootScope.currentUser = null;
$rootScope.isAuthenticated = false;
$window.location.href = sign_out_url;
};
/**
* This block tries to authenticate user
*/
var profile_url = refstackApiUrl + '/profile';
$http.get(profile_url, {withCredentials: true}).
success(function(data) {
$rootScope.currentUser = data;
$rootScope.auth.currentUser = data;
$rootScope.auth.isAuthenticated = true;
}).
error(function() {
$rootScope.currentUser = null;
$rootScope.auth.currentUser = null;
$rootScope.auth.isAuthenticated = false;
});
}
]);

View File

@ -1,29 +0,0 @@
var refstackApp = angular.module('refstackApp');
/**
* Refstack Auth Controller
* This controller handles account authentication for users.
*/
refstackApp.controller('authController',
['$scope', '$window', '$rootScope', 'refstackApiUrl',
function($scope, $window, $rootScope, refstackApiUrl){
'use strict';
var sign_in_url = refstackApiUrl + '/auth/signin';
$scope.doSignIn = function () {
$window.location.href = sign_in_url;
};
var sign_out_url = refstackApiUrl + '/auth/signout';
$scope.doSignOut = function () {
$rootScope.currentUser = null;
$window.location.href = sign_out_url;
};
$scope.isAuthenticated = function () {
if ($scope.currentUser) {
return !!$scope.currentUser.openid;
}
return false;
};
}]);

View File

@ -58,7 +58,7 @@
<div cg-busy="{promise:capsRequest,message:'Loading capabilities'}"></div>
<!-- Get the version-specific template -->
<div ng-include src="detailsTemplate"></header>
<div ng-include src="detailsTemplate"></div>
<div ng-show="showError" class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>

View File

@ -1,11 +1,11 @@
<h3>User profile</h3>
<div ng-show="user">
<table ng-show="user" class="table table-striped table-hover">
<div>
<table class="table table-striped table-hover">
<tbody>
<tr> <td>User name</td> <td>{{user.fullname}}</td> </tr>
<tr> <td>User OpenId</td> <td>{{user.openid}}</td> </tr>
<tr> <td>Email</td> <td>{{user.email}}</td> </tr>
<tr> <td>User name</td> <td>{{auth.currentUser.fullname}}</td> </tr>
<tr> <td>User OpenId</td> <td>{{auth.currentUser.openid}}</td> </tr>
<tr> <td>Email</td> <td>{{auth.currentUser.email}}</td> </tr>
</tbody>
</table>
</div>
@ -22,8 +22,8 @@
</div>
</div>
<div ng-show="pubkeys">
<table ng-show="pubkeys" class="table table-striped table-hover">
<div>
<table class="table table-striped table-hover">
<tbody>
<tr ng-repeat="pubKey in pubkeys" ng-click="openShowPubKeyModal(pubKey)">
<td>{{pubKey.format}}</td>
@ -33,4 +33,3 @@
</tbody>
</table>
</div>

View File

@ -19,16 +19,6 @@ refstackApp.controller('profileController',
function($scope, $http, refstackApiUrl, $state,
PubKeys, $modal, raiseAlert) {
'use strict';
$scope.updateProfile = function () {
var profile_url = refstackApiUrl + '/profile';
$http.get(profile_url, {withCredentials: true}).
success(function(data) {
$scope.user = data;
}).
error(function() {
$state.go('home');
});
};
$scope.updatePubKeys = function (){
var keys = PubKeys.query(function(){
@ -77,7 +67,6 @@ refstackApp.controller('profileController',
$scope.showRes = function(pubKey){
raiseAlert('success', '', pubKey.key);
};
$scope.updateProfile();
$scope.updatePubKeys();
}
]);

View File

@ -1,4 +1,4 @@
<h3>Community Results</h3>
<h3>{{pageHeader}}</h3>
<p>The most recently uploaded community test results are listed here. Currently, these results are anonymous.</p>
<div class="result-filters">

View File

@ -6,8 +6,8 @@ var refstackApp = angular.module('refstackApp');
* a listing of community uploaded results.
*/
refstackApp.controller('resultsController',
['$scope', '$http', '$filter', 'refstackApiUrl',
function ($scope, $http, $filter, refstackApiUrl) {
['$scope', '$http', '$filter', '$state', 'refstackApiUrl',
function ($scope, $http, $filter, $state, refstackApiUrl) {
'use strict';
/** Initial page to be on. */
@ -33,6 +33,9 @@ refstackApp.controller('resultsController',
/** The upload date upper limit to be used in filtering results. */
$scope.endDate = '';
$scope.isUserResults = $state.current.name === 'user_results';
$scope.pageHeader = $scope.isUserResults ?
'Private test results' : 'Community test results';
/**
* This will contact the Refstack API to get a listing of test run
* results.
@ -51,7 +54,9 @@ refstackApp.controller('resultsController',
if (end) {
content_url = content_url + '&end_date=' + end + ' 23:59:59';
}
if ($scope.isUserResults) {
content_url = content_url + '&signed';
}
$scope.resultsRequest =
$http.get(content_url).success(function (data) {
$scope.data = data;

View File

@ -38,12 +38,11 @@
<!-- Controllers -->
<script src="shared/header/headerController.js"></script>
<script src="shared/alerts/alertModalFactory.js"></script>
<script src="components/capabilities/capabilitiesController.js"></script>
<script src="components/results/resultsController.js"></script>
<script src="components/results-report/resultsReportController.js"></script>
<script src="components/profile/profileController.js"></script>
<script src="components/auth/authController.js"></script>
<script src="components/alerts/alertModalFactory.js"></script>
<!-- Filters -->
<script src="shared/filters.js"></script>

View File

@ -5,7 +5,7 @@ refstackApp.factory('raiseAlert',
'use strict';
return function(mode, title, text) {
$modal.open({
templateUrl: '/components/alerts/alertModal.html',
templateUrl: '/shared/alerts/alertModal.html',
controller: 'raiseAlertModalController',
backdrop: true,
keyboard: true,

View File

@ -18,14 +18,14 @@ Refstack
<li ng-class="{ active: isActive('/')}"><a ui-sref="home">Home</a></li>
<li ng-class="{ active: isActive('/about')}"><a ui-sref="about">About</a></li>
<li ng-class="{ active: isActive('/capabilities')}"><a ui-sref="capabilities">DefCore Capabilities</a></li>
<li ng-class="{ active: isActive('/results')}"><a ui-sref="results">Community Results</a></li>
<li ng-class="{ active: isActive('/community_results')}"><a ui-sref="community_results">Community Results</a></li>
</ul>
<ul ng-controller="authController" class="nav navbar-nav navbar-right">
<li ng-class="{ active: isActive('/profile')}" ng-if="isAuthenticated()"><a ui-sref="profile">{{currentUser.fullname}}</a></li>
<li ng-if="isAuthenticated()"><a href="" ng-click="doSignOut()">Sign Out</a></li>
<li ng-if="!isAuthenticated()"><a href="" ng-click="doSignIn()">Sign In / Sign Up</a></li>
<ul class="nav navbar-nav navbar-right">
<li ng-class="{ active: isActive('/user_results')}" ng-if="auth.isAuthenticated"><a ui-sref="user_results">My Results</a></li>
<li ng-class="{ active: 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>
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,40 @@
describe('Auth', function () {
'use strict';
var fakeApiUrl = 'http://foo.bar/v1';
var $window;
beforeEach(function () {
$window = {location: { href: jasmine.createSpy()} };
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
$provide.value('$window', $window);
});
module('refstackApp');
});
var $rootScope, $httpBackend;
beforeEach(inject(function (_$httpBackend_, _$rootScope_) {
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
}));
it('should show signin url for signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond({'openid': 'foo@bar.com',
'email': 'foo@bar.com',
'fullname': 'foo' });
$httpBackend.flush();
$rootScope.auth.doSignIn();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signin');
expect($rootScope.auth.isAuthenticated).toBe(true);
});
it('should show signout url for not signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond(401);
$httpBackend.flush();
$rootScope.auth.doSignOut();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signout');
expect($rootScope.auth.isAuthenticated).toBe(false);
});
});

View File

@ -3,11 +3,18 @@ describe('Refstack controllers', function () {
'use strict';
var fakeApiUrl = 'http://foo.bar/v1';
var $httpBackend;
beforeEach(function () {
module(function ($provide) {
$provide.constant('refstackApiUrl', fakeApiUrl);
});
module('refstackApp');
inject(function(_$httpBackend_){
$httpBackend = _$httpBackend_;
});
$httpBackend.whenGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.whenGET('/components/home/home.html')
.respond('<div>mock template</div>');
});
describe('headerController', function () {
@ -36,42 +43,10 @@ describe('Refstack controllers', function () {
});
});
describe('authController', function () {
var scope, $httpBackend, $window;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
$window = {location: { href: jasmine.createSpy()} };
$controller('authController', {$scope: scope, $window: $window});
}));
it('should show signin url for signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond({'openid': 'foo@bar.com',
'email': 'foo@bar.com',
'fullname': 'foo' });
$httpBackend.flush();
scope.doSignIn();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signin');
expect(scope.isAuthenticated()).toBe(true);
});
it('should show signout url for not signed user', function () {
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond(401);
$httpBackend.flush();
scope.doSignOut();
expect($window.location.href).toBe(fakeApiUrl + '/auth/signout');
expect(scope.isAuthenticated()).toBe(false);
});
});
describe('capabilitiesController', function () {
var scope, $httpBackend;
var scope;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
$controller('capabilitiesController', {$scope: scope});
}));
@ -102,8 +77,6 @@ describe('Refstack controllers', function () {
}
};
$httpBackend.expectGET(fakeApiUrl +
'/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/capabilities').respond(['2015.03.json', '2015.04.json']);
// Should call request with latest version.
@ -170,7 +143,7 @@ describe('Refstack controllers', function () {
});
describe('resultsController', function () {
var scope, $httpBackend;
var scope;
var fakeResponse = {
'pagination': {'current_page': 1, 'total_pages': 2},
'results': [{
@ -180,8 +153,7 @@ describe('Refstack controllers', function () {
}]
};
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
$controller('resultsController', {$scope: scope});
}));
@ -189,9 +161,8 @@ describe('Refstack controllers', function () {
it('should fetch the first page of results with proper URL args',
function () {
// Initial results should be page 1 of all results.
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results?page=1').respond(fakeResponse);
$httpBackend.expectGET(fakeApiUrl + '/results?page=1')
.respond(fakeResponse);
$httpBackend.flush();
expect(scope.data).toEqual(fakeResponse);
expect(scope.currentPage).toBe(1);
@ -211,7 +182,6 @@ describe('Refstack controllers', function () {
});
it('should set an error when results cannot be retrieved', function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl + '/results?page=1').respond(404,
{'detail': 'Not Found'});
$httpBackend.flush();
@ -224,23 +194,16 @@ describe('Refstack controllers', function () {
it('should have an function to clear filters and update the view',
function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results?page=1').respond(fakeResponse);
scope.startDate = 'some date';
scope.endDate = 'some other date';
scope.clearFilters();
expect(scope.startDate).toBe(null);
expect(scope.endDate).toBe(null);
$httpBackend.expectGET(fakeApiUrl +
'/results?page=1').respond(fakeResponse);
$httpBackend.flush();
expect(scope.data).toEqual(fakeResponse);
});
});
describe('resultsReportController', function () {
var scope, $httpBackend, stateparams;
var scope, stateparams;
var fakeResultResponse = {'results': ['test_id_1']};
var fakeCapabilityResponse = {
'platform': {'required': ['compute']},
@ -261,8 +224,7 @@ describe('Refstack controllers', function () {
}
};
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
beforeEach(inject(function ($rootScope, $controller) {
stateparams = {testID: 1234};
scope = $rootScope.$new();
$controller('resultsReportController',
@ -272,7 +234,6 @@ describe('Refstack controllers', function () {
it('should make all necessary API requests to get results ' +
'and capabilities',
function () {
$httpBackend.expectGET(fakeApiUrl + '/profile').respond(401);
$httpBackend.expectGET(fakeApiUrl +
'/results/1234').respond(fakeResultResponse);
$httpBackend.expectGET(fakeApiUrl +

View File

@ -19,6 +19,7 @@ START_DATE = 'start_date'
END_DATE = 'end_date'
CPID = 'cpid'
PAGE = 'page'
SIGNED = 'signed'
# OpenID parameters
OPENID_MODE = 'openid.mode'
@ -36,3 +37,6 @@ OPENID_ERROR = 'openid.error'
# User session parameters
CSRF_TOKEN = 'csrf_token'
USER_OPENID = 'user_openid'
# Test metadata fields
PUBLIC_KEY = 'public_key'

View File

@ -55,7 +55,7 @@ class ResultsController(validation.BaseRestControllerWithValidation):
if pecan.request.headers.get('X-Public-Key'):
if 'metadata' not in item:
item['metadata'] = {}
item['metadata']['public_key'] = \
item['metadata'][const.PUBLIC_KEY] = \
pecan.request.headers.get('X-Public-Key')
test_id = db.store_results(item)
LOG.debug(item)
@ -79,6 +79,7 @@ class ResultsController(validation.BaseRestControllerWithValidation):
const.START_DATE,
const.END_DATE,
const.CPID,
const.SIGNED
]
try:
@ -88,9 +89,6 @@ class ResultsController(validation.BaseRestControllerWithValidation):
api_utils.get_page_number(records_count)
except api_utils.ParseInputsError as ex:
pecan.abort(400, 'Reason: %s' % ex)
except Exception as ex:
LOG.debug('An error occurred: %s' % ex)
pecan.abort(500)
try:
per_page = CONF.api.results_per_page
@ -102,7 +100,8 @@ class ResultsController(validation.BaseRestControllerWithValidation):
'test_id': r.id,
'created_at': r.created_at,
'cpid': r.cpid,
'url': CONF.api.test_results_url % r.id
'url': parse.urljoin(CONF.ui_url,
CONF.api.test_results_url) % r.id
})
page = {'results': results,

View File

@ -86,6 +86,16 @@ def parse_input_params(expected_input_params):
'start': const.START_DATE,
'end': const.END_DATE
})
if const.SIGNED in filters:
if is_authenticated():
filters['openid'] = get_user_id()
filters['pubkeys'] = [
' '.join((pk['format'], pk['key']))
for pk in db.get_user_pubkeys(filters['openid'])
]
else:
raise ParseInputsError('To see signed test '
'results you need to authenticate')
return filters
@ -176,6 +186,11 @@ def get_user():
return db.user_get(get_user_id())
def get_user_public_keys():
"""Return db record for authenticated user."""
return db.get_user_pubkeys(get_user_id())
def is_authenticated():
"""Return True if user is authenticated."""
if get_user_id():

View File

@ -34,6 +34,7 @@ def upgrade():
sa.ForeignKeyConstraint(['openid'], ['user.openid'], ),
mysql_charset=MYSQL_CHARSET
)
op.create_index('indx_meta_value', 'meta', ['value'], mysql_length=32)
def downgrade():

View File

@ -117,6 +117,19 @@ def _apply_filters_for_query(query, filters):
if cpid:
query = query.filter(models.Test.cpid == cpid)
signed = api_const.SIGNED in filters
if signed:
query = (query
.join(models.Test.meta)
.filter(models.TestMeta.meta_key == api_const.PUBLIC_KEY)
.filter(models.TestMeta.value.in_(filters['pubkeys']))
)
else:
signed_results = (query.session
.query(models.TestMeta.test_id)
.filter_by(meta_key=api_const.PUBLIC_KEY))
query = query.filter(models.Test.id.notin_(signed_results))
return query

View File

@ -134,7 +134,7 @@ class ResultsControllerTestCase(base.BaseTestCase):
'url': self.test_results_url % 'fake_test_id'})
self.assertEqual(mock_response.status, 201)
mock_store_results.assert_called_once_with(
{'answer': 42, 'metadata': {'public_key': 'fake-key'}}
{'answer': 42, 'metadata': {const.PUBLIC_KEY: 'fake-key'}}
)
@mock.patch('pecan.abort')
@ -216,6 +216,7 @@ class ResultsControllerTestCase(base.BaseTestCase):
const.START_DATE,
const.END_DATE,
const.CPID,
const.SIGNED
]
page_number = 1
total_pages_number = 10

View File

@ -193,9 +193,12 @@ class DBBackendTestCase(base.BaseTestCase):
self.assertEqual(expected_result, actual_result)
@mock.patch('refstack.db.sqlalchemy.models.Test')
def test_apply_filters_for_query(self, mock_model):
@mock.patch('refstack.db.sqlalchemy.models.TestMeta')
def test_apply_filters_for_query_unsigned(self, mock_meta,
mock_test):
query = mock.Mock()
mock_model.created_at = six.text_type()
mock_test.created_at = six.text_type()
mock_meta.test_id = six.text_type()
filters = {
api_const.START_DATE: 'fake1',
@ -205,19 +208,30 @@ class DBBackendTestCase(base.BaseTestCase):
result = api._apply_filters_for_query(query, filters)
query.filter.assert_called_once_with(mock_model.created_at >=
query.filter.assert_called_once_with(mock_test.created_at >=
filters[api_const.START_DATE])
query = query.filter.return_value
query.filter.assert_called_once_with(mock_model.created_at <=
query.filter.assert_called_once_with(mock_test.created_at <=
filters[api_const.END_DATE])
query = query.filter.return_value
query.filter.assert_called_once_with(mock_model.cpid ==
query.filter.assert_called_once_with(mock_test.cpid ==
filters[api_const.CPID])
query = query.filter.return_value
self.assertEqual(result, query)
query.session.query.assert_called_once_with(mock_meta.test_id)
meta_query = query.session.query.return_value
meta_query.filter_by.\
assert_called_once_with(meta_key=api_const.PUBLIC_KEY)
unsigned_test_id_query = meta_query.filter_by.return_value
mock_test.id.notin_.assert_called_once_with(unsigned_test_id_query)
query.filter.assert_called_once_with(mock_test.id.notin_.return_value)
filtered_query = query.filter.return_value
self.assertEqual(result, filtered_query)
@mock.patch.object(api, '_apply_filters_for_query')
@mock.patch.object(api, 'get_session')

View File

@ -8,6 +8,5 @@ oslotest>=1.2.0 # Apache-2.0
python-subunit>=0.0.18
testrepository>=0.0.18
testtools>=0.9.34
mysqlclient
six>=1.7.0
pep257>=0.5.0