diff options
Diffstat (limited to 'testapi')
16 files changed, 136 insertions, 227 deletions
diff --git a/testapi/3rd_party/static/testapi-ui/app.js b/testapi/3rd_party/static/testapi-ui/app.js index 4a2f23a..8c701c3 100644 --- a/testapi/3rd_party/static/testapi-ui/app.js +++ b/testapi/3rd_party/static/testapi-ui/app.js @@ -37,30 +37,30 @@ $stateProvider. state('home', { url: '/', - templateUrl: '/testapi-ui/components/home/home.html' + templateUrl: 'testapi-ui/components/home/home.html' }). state('about', { url: '/about', - templateUrl: '/testapi-ui/components/about/about.html' + templateUrl: 'testapi-ui/components/about/about.html' }). state('guidelines', { url: '/guidelines', - templateUrl: '/testapi-ui/components/guidelines/guidelines.html', + templateUrl: 'testapi-ui/components/guidelines/guidelines.html', controller: 'GuidelinesController as ctrl' }). state('communityResults', { url: '/community_results', - templateUrl: '/testapi-ui/components/results/results.html', + templateUrl: 'testapi-ui/components/results/results.html', controller: 'ResultsController as ctrl' }). state('userResults', { - url: '/user_results', + url: 'user_results', templateUrl: '/testapi-ui/components/results/results.html', controller: 'ResultsController as ctrl' }). state('resultsDetail', { url: '/results/:testID', - templateUrl: '/testapi-ui/components/results-report' + + templateUrl: 'testapi-ui/components/results-report' + '/resultsReport.html', controller: 'ResultsReportController as ctrl' }). @@ -71,12 +71,12 @@ }). state('authFailure', { url: '/auth_failure', - templateUrl: '/testapi-ui/components/home/home.html', + templateUrl: 'testapi-ui/components/home/home.html', controller: 'AuthFailureController as ctrl' }). state('logout', { url: '/logout', - templateUrl: '/testapi-ui/components/logout/logout.html', + templateUrl: 'testapi-ui/components/logout/logout.html', controller: 'LogoutController as ctrl' }). state('userVendors', { diff --git a/testapi/3rd_party/static/testapi-ui/assets/img/OpenStack_Project_Refstack_mascot_90x90.png b/testapi/3rd_party/static/testapi-ui/assets/img/OpenStack_Project_Refstack_mascot_90x90.png Binary files differdeleted file mode 100755 index 4695090..0000000 --- a/testapi/3rd_party/static/testapi-ui/assets/img/OpenStack_Project_Refstack_mascot_90x90.png +++ /dev/null diff --git a/testapi/3rd_party/static/testapi-ui/assets/img/openstack-logo.png b/testapi/3rd_party/static/testapi-ui/assets/img/openstack-logo.png Binary files differdeleted file mode 100644 index 826bf2e..0000000 --- a/testapi/3rd_party/static/testapi-ui/assets/img/openstack-logo.png +++ /dev/null diff --git a/testapi/3rd_party/static/testapi-ui/assets/img/refstack-logo.png b/testapi/3rd_party/static/testapi-ui/assets/img/refstack-logo.png Binary files differdeleted file mode 100755 index fc45f3e..0000000 --- a/testapi/3rd_party/static/testapi-ui/assets/img/refstack-logo.png +++ /dev/null diff --git a/testapi/3rd_party/static/testapi-ui/assets/img/testapi-logo.png b/testapi/3rd_party/static/testapi-ui/assets/img/testapi-logo.png Binary files differnew file mode 100644 index 0000000..ff78eb1 --- /dev/null +++ b/testapi/3rd_party/static/testapi-ui/assets/img/testapi-logo.png diff --git a/testapi/3rd_party/static/testapi-ui/components/results/results.html b/testapi/3rd_party/static/testapi-ui/components/results/results.html index 2a43cd1..3056e1d 100644 --- a/testapi/3rd_party/static/testapi-ui/components/results/results.html +++ b/testapi/3rd_party/static/testapi-ui/components/results/results.html @@ -43,184 +43,36 @@ <div cg-busy="{promise:ctrl.resultsRequest,message:'Loading'}"></div> <div ng-show="ctrl.data" class="results-table"> - <table ng-show="ctrl.data" class="table table-striped table-hover"> + <table ng-data="ctrl.data.result" ng-show="ctrl.data" class="table table-striped table-hover"> <thead> <tr> - <th ng-if="ctrl.isUserResults"></th> - <th>Upload Date</th> - <th>Test Run ID</th> - <th ng-if="ctrl.isUserResults">Vendor</th> - <th ng-if="ctrl.isUserResults">Product (version)</th> - <th ng-if="ctrl.isUserResults">Target Program</th> - <th ng-if="ctrl.isUserResults">Guideline</th> - <th ng-if="ctrl.isUserResults">Verified</th> - <th ng-if="ctrl.isUserResults">Shared</th> + <th>ID</th> + <th>Pod</th> + <th>Project</th> + <th>Test Case</th> + <th>Installer</th> + <th>Version</th> + <th>Scenario</th> + <th>Criteria</th> + <th>Start Date</th> + <th>Stop Date</th> </tr> </thead> <tbody> <tr ng-repeat-start="(index, result) in ctrl.data.results"> - <td ng-if="ctrl.isUserResults"> - <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.slice(0, 8)}}...{{result.id.slice(-8)}} - </a> - </td> - <td ng-if="ctrl.isUserResults"> - {{ctrl.vendors[result.product_version.product_info.organization_id].name || '-'}} - </td> - <td ng-if="ctrl.isUserResults">{{result.product_version.product_info.name || '-'}} - <span ng-if="result.product_version.version"> - ({{result.product_version.version}}) - </span> - </td> - <td ng-if="ctrl.isUserResults">{{ctrl.targetMappings[result.meta.target] || '-'}}</td> - <td ng-if="ctrl.isUserResults">{{result.meta.guideline.slice(0, -5) || '-'}}</td> - <td ng-if="ctrl.isUserResults"> - <span ng-if="result.verification_status" class="glyphicon glyphicon-ok"></span> - <span ng-if="!result.verification_status">-</span> - - </td> - <td ng-if="ctrl.isUserResults"> - <span ng-show="result.meta.shared" class="glyphicon glyphicon-share"></span> - </td> + <td>{{ result._id }}</td> + <td>{{ result.pod_name }}</td> + <td>{{ result.project_name }}</td> + <td>{{ result.case_name }}</td> + <td>{{ result.installer }}</td> + <td>{{ result.version }}</td> + <td>{{ result.scenario }}</td> + <td>{{ result.criteria }}</td> + <td>{{ result.start_date }}</td> + <td>{{ result.stop_date }}</td> </tr> - <tr ng-if="result.expanded" ng-repeat-end> - <td></td> - <td colspan="3"> - <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.associateMeta(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.getVersionList();result.guidelineEdit = true" - title="Edit" - class="glyphicon glyphicon-pencil"></a> - <a ng-if="result.guidelineEdit" - ng-click="ctrl.associateMeta(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.associateMeta(index, 'target', result.meta.target)" - 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 ng-repeat-end=> </tr> </tbody> </table> diff --git a/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js b/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js index 2b0338c..9e3540d 100644 --- a/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js +++ b/testapi/3rd_party/static/testapi-ui/components/results/resultsController.js @@ -38,7 +38,6 @@ ctrl.associateMeta = associateMeta; ctrl.getVersionList = getVersionList; ctrl.getUserProducts = getUserProducts; - ctrl.getVendors = getVendors; ctrl.associateProductVersion = associateProductVersion; ctrl.getProductVersions = getProductVersions; ctrl.prepVersionEdit = prepVersionEdit; @@ -100,8 +99,6 @@ ctrl.update(); } - ctrl.getVendors(); - /** * This will contact the TestAPI API to get a listing of test run * results. @@ -114,11 +111,11 @@ var start = $filter('date')(ctrl.startDate, 'yyyy-MM-dd'); if (start) { content_url = - content_url + '&start_date=' + start + ' 00:00:00'; + content_url + '&from=' + start + ' 00:00:00'; } var end = $filter('date')(ctrl.endDate, 'yyyy-MM-dd'); if (end) { - content_url = content_url + '&end_date=' + end + ' 23:59:59'; + content_url = content_url + '&to=' + end + ' 23:59:59'; } if (ctrl.isUserResults) { content_url = content_url + '&signed'; @@ -126,8 +123,7 @@ ctrl.resultsRequest = $http.get(content_url).success(function (data) { ctrl.data = data; - ctrl.totalItems = ctrl.data.pagination.total_pages * - ctrl.itemsPerPage; + ctrl.totalItems = ctrl.data.pagination.total_pages * ctrl.itemsPerPage; ctrl.currentPage = ctrl.data.pagination.current_page; }).error(function (error) { ctrl.data = null; @@ -248,27 +244,6 @@ } /** - * This will contact the TestAPI API to get a listing of - * vendors. - */ - function getVendors() { - var contentUrl = testapiApiUrl + '/vendors'; - ctrl.vendorsRequest = - $http.get(contentUrl).success(function (data) { - ctrl.vendors = {}; - data.vendors.forEach(function(vendor) { - ctrl.vendors[vendor.id] = vendor; - }); - }).error(function (error) { - ctrl.vendors = null; - ctrl.showError = true; - ctrl.error = - 'Error retrieving vendor listing from server: ' + - angular.toJson(error); - }); - } - - /** * Send a PUT request to the API server to associate a product with * a test result. */ diff --git a/testapi/3rd_party/static/testapi-ui/config.json b/testapi/3rd_party/static/testapi-ui/config.json index 5d48c7b..9fdd85f 100644 --- a/testapi/3rd_party/static/testapi-ui/config.json +++ b/testapi/3rd_party/static/testapi-ui/config.json @@ -1 +1 @@ -{"testapiApiUrl": "http://localhost:8000/api/v1"} +{"testapiApiUrl": "http://testresults.opnfv.org/test/api/v1"} diff --git a/testapi/3rd_party/static/testapi-ui/shared/alerts/alertModalFactory.js b/testapi/3rd_party/static/testapi-ui/shared/alerts/alertModalFactory.js index 3ce76c9..929f543 100644 --- a/testapi/3rd_party/static/testapi-ui/shared/alerts/alertModalFactory.js +++ b/testapi/3rd_party/static/testapi-ui/shared/alerts/alertModalFactory.js @@ -28,7 +28,7 @@ function raiseAlert($uibModal) { return function(mode, title, text) { $uibModal.open({ - templateUrl: '/shared/alerts/alertModal.html', + templateUrl: 'testapi-ui/shared/alerts/alertModal.html', controller: 'RaiseAlertModalController as alert', backdrop: true, keyboard: true, diff --git a/testapi/etc/config.ini b/testapi/etc/config.ini index 692e488..dad59d2 100644 --- a/testapi/etc/config.ini +++ b/testapi/etc/config.ini @@ -8,8 +8,12 @@ dbname = test_results_collection [api] # Listening port -url = http://localhost:8000/api/v1 +url = http://testresults.opnfv.org/test/api/v1 port = 8000 + +# Number of results for one page (integer value) +#results_per_page = 20 + # With debug_on set to true, error traces will be shown in HTTP responses debug = True authenticate = False @@ -18,7 +22,7 @@ authenticate = False base_url = http://localhost:8000 [ui] -url = http://localhost:8000 +url = http://testresults.opnfv.org/test [osid] @@ -41,7 +45,7 @@ openid_ns = http://specs.openid.net/auth/2.0 # Return endpoint in Refstack's API. Value indicating the endpoint # where the user should be returned to after signing in. Openstack Id # Idp only supports HTTPS address types. (string value) -openid_return_to = /api/v1/auth/signin_return +openid_return_to = v1/auth/signin_return # Claimed identifier. This value must be set to # "http://specs.openid.net/auth/2.0/identifier_select". or to user diff --git a/testapi/opnfv_testapi/common/config.py b/testapi/opnfv_testapi/common/config.py index 46765ff..f73c0ab 100644 --- a/testapi/opnfv_testapi/common/config.py +++ b/testapi/opnfv_testapi/common/config.py @@ -17,6 +17,7 @@ class Config(object): def __init__(self): self.file = self.CONFIG if self.CONFIG else self._default_config() self._parse() + self._parse_per_page() self.static_path = os.path.join( os.path.dirname(os.path.normpath(__file__)), os.pardir, @@ -37,6 +38,10 @@ class Config(object): [setattr(self, '{}_{}'.format(section, k), self._parse_value(v)) for k, v in config.items(section)] + def _parse_per_page(self): + if not hasattr(self, 'api_results_per_page'): + self.api_results_per_page = 20 + @staticmethod def _parse_value(value): try: diff --git a/testapi/opnfv_testapi/resources/handlers.py b/testapi/opnfv_testapi/resources/handlers.py index 2fc31ca..42372e8 100644 --- a/testapi/opnfv_testapi/resources/handlers.py +++ b/testapi/opnfv_testapi/resources/handlers.py @@ -104,17 +104,35 @@ class GenericApiHandler(web.RequestHandler): if query is None: query = {} data = [] + sort = kwargs.get('sort') + page = kwargs.get('page') + last = kwargs.get('last') + per_page = kwargs.get('per_page') + cursor = self._eval_db(self.table, 'find', query) - if 'sort' in kwargs: - cursor = cursor.sort(kwargs.get('sort')) - if 'last' in kwargs: - cursor = cursor.limit(kwargs.get('last')) + if sort: + cursor = cursor.sort(sort) + if last and last != 0: + cursor = cursor.limit(last) + if page: + records_count = yield cursor.count() + total_pages, remainder = divmod(records_count, per_page) + if remainder > 0: + total_pages += 1 + cursor = cursor.skip((page - 1) * per_page).limit(per_page) while (yield cursor.fetch_next): data.append(self.format_data(cursor.next_object())) if res_op is None: res = {self.table: data} else: res = res_op(data, *args) + if page: + res.update({ + 'pagination': { + 'current_page': page, + 'total_pages': total_pages + } + }) self.finish_request(res) @web.asynchronous diff --git a/testapi/opnfv_testapi/resources/result_handlers.py b/testapi/opnfv_testapi/resources/result_handlers.py index 214706f..208af6d 100644 --- a/testapi/opnfv_testapi/resources/result_handlers.py +++ b/testapi/opnfv_testapi/resources/result_handlers.py @@ -11,12 +11,15 @@ from datetime import timedelta from bson import objectid +from opnfv_testapi.common import config from opnfv_testapi.common import message from opnfv_testapi.common import raises from opnfv_testapi.resources import handlers from opnfv_testapi.resources import result_models from opnfv_testapi.tornado_swagger import swagger +CONF = config.Config() + class GenericResultHandler(handlers.GenericApiHandler): def __init__(self, application, request, **kwargs): @@ -35,6 +38,8 @@ class GenericResultHandler(handlers.GenericApiHandler): def set_query(self): query = dict() + date_range = dict() + for k in self.request.query_arguments.keys(): v = self.get_query_argument(k) if k == 'project' or k == 'pod' or k == 'case': @@ -47,8 +52,14 @@ class GenericResultHandler(handlers.GenericApiHandler): query['start_date'] = obj elif k == 'trust_indicator': query[k + '.current'] = float(v) - elif k != 'last': + elif k == 'from': + date_range.update({'$gte': str(v)}) + elif k == 'to': + date_range.update({'$lt': str(v)}) + elif k != 'last' and k != 'page': query[k] = v + if date_range: + query['start_date'] = date_range return query @@ -64,9 +75,11 @@ class ResultsCLHandler(GenericResultHandler): - case : case name - pod : pod name - version : platform version (Arno-R1, ...) - - installer (fuel, ...) + - installer : fuel/apex/compass/joid/daisy - build_tag : Jenkins build tag name - - period : x (x last days) + - period : x last days, incompatible with from/to + - from : starting time in 2016-01-01 or 2016-01-01 00:01:23 + - to : ending time in 2016-01-01 or 2016-01-01 00:01:23 - scenario : the test scenario (previously version) - criteria : the global criteria status passed or failed - trust_indicator : evaluate the stability of the test case @@ -113,22 +126,40 @@ class ResultsCLHandler(GenericResultHandler): @type period: L{string} @in period: query @required period: False + @param from: i.e. 2016-01-01 or 2016-01-01 00:01:23 + @type from: L{string} + @in from: query + @required from: False + @param to: i.e. 2016-01-01 or 2016-01-01 00:01:23 + @type to: L{string} + @in to: query + @required to: False @param last: last records stored until now @type last: L{string} @in last: query @required last: False + @param page: which page to list + @type page: L{int} + @in page: query + @required page: False @param trust_indicator: must be float @type trust_indicator: L{float} @in trust_indicator: query @required trust_indicator: False """ + limitations = {'sort': [('start_date', -1)]} last = self.get_query_argument('last', 0) if last is not None: last = self.get_int('last', last) + limitations.update({'last': last}) + + page = self.get_query_argument('page', 1) + if page is not None: + page = self.get_int('page', page) + limitations.update({'page': page, + 'per_page': CONF.api_results_per_page}) - self._list(query=self.set_query(), - sort=[('start_date', -1)], - last=last) + self._list(query=self.set_query(), **limitations) @swagger.operation(nickname="createTestResult") def post(self): diff --git a/testapi/opnfv_testapi/router/url_mappings.py b/testapi/opnfv_testapi/router/url_mappings.py index aea85a4..a2312de 100644 --- a/testapi/opnfv_testapi/router/url_mappings.py +++ b/testapi/opnfv_testapi/router/url_mappings.py @@ -55,7 +55,7 @@ mappings = [ (r"/api/v1/scenarios/([^/]+)", scenario_handlers.ScenarioGURHandler), # static path - (r'/(.*\.(css|png|gif|js|html|json|map))', + (r'/(.*\.(css|png|gif|js|html|json|map|woff2|woff|ttf))', tornado.web.StaticFileHandler, {'path': config.Config().static_path}), diff --git a/testapi/opnfv_testapi/tests/unit/fake_pymongo.py b/testapi/opnfv_testapi/tests/unit/fake_pymongo.py index ef74a08..b2564a6 100644 --- a/testapi/opnfv_testapi/tests/unit/fake_pymongo.py +++ b/testapi/opnfv_testapi/tests/unit/fake_pymongo.py @@ -20,18 +20,18 @@ def thread_execute(method, *args, **kwargs): class MemCursor(object): def __init__(self, collection): self.collection = collection - self.count = len(self.collection) + self.length = len(self.collection) self.sorted = [] def _is_next_exist(self): - return self.count != 0 + return self.length != 0 @property def fetch_next(self): return thread_execute(self._is_next_exist) def next_object(self): - self.count -= 1 + self.length -= 1 return self.collection.pop() def sort(self, key_or_list): @@ -48,10 +48,25 @@ class MemCursor(object): def limit(self, limit): if limit != 0 and limit < len(self.collection): - self.collection = self.collection[0:limit] - self.count = limit + self.collection = self.collection[0: limit] + self.length = limit return self + def skip(self, skip): + if skip < self.length and (skip > 0): + self.collection = self.collection[self.length - skip: -1] + self.length -= skip + elif skip >= self.length: + self.collection = [] + self.length = 0 + return self + + def _count(self): + return self.length + + def count(self): + return thread_execute(self._count) + class MemDb(object): diff --git a/testapi/opnfv_testapi/ui/auth/sign.py b/testapi/opnfv_testapi/ui/auth/sign.py index 7319084..6a9d94e 100644 --- a/testapi/opnfv_testapi/ui/auth/sign.py +++ b/testapi/opnfv_testapi/ui/auth/sign.py @@ -32,6 +32,9 @@ class SigninHandler(base.BaseHandler): class SigninReturnHandler(base.BaseHandler): def get(self): + if self.get_query_argument(const.OPENID_MODE) == 'cancel': + self._auth_failure('Authentication canceled.') + openid = self.get_query_argument(const.OPENID_CLAIMED_ID) user_info = { 'openid': openid, @@ -44,6 +47,12 @@ class SigninReturnHandler(base.BaseHandler): self.set_secure_cookie('openid', openid) self.redirect(url=CONF.ui_url) + def _auth_failure(self, message): + params = {'message': message} + url = parse.urljoin(CONF.ui_url, + '/#/auth_failure?' + parse.urlencode(params)) + self.redirect(url) + class SignoutHandler(base.BaseHandler): def get(self): |