diff --git a/app/account/account.routes.js b/app/account/account.routes.js index 1151c6a58..13baeb6e6 100644 --- a/app/account/account.routes.js +++ b/app/account/account.routes.js @@ -10,12 +10,33 @@ $stateProvider .state('login', { - url: '/login', - templateUrl: 'account/login/login.html' + parent: 'root', + url: '/login?next', + data: { + title: 'Login' + }, + views: { + 'container@': { + templateUrl: 'account/login/login.html', + controller: 'LoginController' + }, + 'footer@': { + // no footer + template: '' + } + } }) .state('register', { - url: '/register', - templateUrl: 'account/register/register.html' + url: '/register?next', + data: { + title: "Join" + }, + views: { + 'container@': { + templateUrl: 'account/register/register.html', + controller: 'RegisterController' + } + } }); $urlRouterProvider.otherwise('/login'); diff --git a/app/account/login/login.controller.js b/app/account/login/login.controller.js index 852d39db2..7a1b2b10b 100644 --- a/app/account/login/login.controller.js +++ b/app/account/login/login.controller.js @@ -3,10 +3,24 @@ angular.module('tc.account').controller('LoginController', LoginController); - LoginController.$inject = []; + LoginController.$inject = ['$log', '$state', '$stateParams', 'auth', '$location']; - function LoginController() { + function LoginController($log, $state, $stateParams, auth, $location) { var vm = this; vm.name = 'login'; + + // check if the user is already logged in + if (auth.isAuthenticated()) { + // redirect to next if exists else dashboard + if ($stateParams.next) { + $log.debug('Redirecting: ' + $stateParams.next); + $log.debug($location.path()); + $location.path($stateParams.next); + } else { + // FIXME + $state.go('sample.child1'); + } + } + } })(); diff --git a/app/blocks/exception/exception-handler.provider.js b/app/blocks/exception/exception-handler.provider.js new file mode 100644 index 000000000..5360b4665 --- /dev/null +++ b/app/blocks/exception/exception-handler.provider.js @@ -0,0 +1,70 @@ +// Include in index.html so that app level exceptions are handled. +// Exclude from testRunner.html which should run exactly what it wants to run +(function () { + 'use strict'; + + angular + .module('blocks.exception') + .provider('exceptionHandler', exceptionHandlerProvider) + .config(config); + + /** + * Must configure the exception handling + * @return {[type]} + */ + function exceptionHandlerProvider() { + /* jshint validthis:true */ + this.config = { + appErrorPrefix: undefined + }; + + this.configure = function (appErrorPrefix) { + this.config.appErrorPrefix = appErrorPrefix; + }; + + this.$get = function () { + return {config: this.config}; + }; + } + + config.$inject = ['$provide']; + + /** + * Configure by setting an optional string value for appErrorPrefix. + * Accessible via config.appErrorPrefix (via config value). + * @param {[type]} $provide + * @return {[type]} + * @ngInject + */ + function config($provide) { + $provide.decorator('$exceptionHandler', extendExceptionHandler); + } + + extendExceptionHandler.$inject = ['$delegate', 'exceptionHandler', 'logger']; + + /** + * Extend the $exceptionHandler service to also display a toast. + * @param {Object} $delegate + * @param {Object} exceptionHandler + * @param {Object} logger + * @return {Function} the decorated $exceptionHandler service + */ + function extendExceptionHandler($delegate, exceptionHandler, logger) { + return function (exception, cause) { + var appErrorPrefix = exceptionHandler.config.appErrorPrefix || ''; + var errorData = {exception: exception, cause: cause}; + exception.message = appErrorPrefix + exception.message; + $delegate(exception, cause); + /** + * Could add the error to a service's collection, + * add errors to $rootScope, log errors to remote web server, + * or log locally. Or throw hard. It is entirely up to you. + * throw exception; + * + * @example + * throw { message: 'error message we added' }; + */ + logger.error(exception.message, errorData); + }; + } +})(); diff --git a/app/blocks/exception/exception-handler.provider.spec.js b/app/blocks/exception/exception-handler.provider.spec.js new file mode 100644 index 000000000..e8c5e7b4d --- /dev/null +++ b/app/blocks/exception/exception-handler.provider.spec.js @@ -0,0 +1,67 @@ +/* jshint -W117, -W030 */ +describe('blocks.exception', function () { + var exceptionHandlerProvider; + var mocks = { + errorMessage: 'fake error', + prefix: '[TEST]: ' + }; + + beforeEach(function () { + bard.inject('$rootScope'); + }); + + bard.verifyNoOutstandingHttpRequests(); + + describe('$exceptionHandler', function () { + it('should have a dummy test', inject(function () { + expect(true).to.equal(true); + })); + + it('should be defined', inject(function ($exceptionHandler) { + expect($exceptionHandler).to.be.defined; + })); + + it('should have configuration', inject(function ($exceptionHandler) { + expect($exceptionHandler.config).to.be.defined; + })); + + // describe('with appErrorPrefix', function () { + // beforeEach(function () { + // exceptionHandlerProvider.configure(mocks.prefix); + // }); + + // it('should have exceptionHandlerProvider defined', inject(function () { + // expect(exceptionHandlerProvider).to.be.defined; + // })); + + // it('should have appErrorPrefix defined', inject(function () { + // expect(exceptionHandlerProvider.$get().config.appErrorPrefix).to.be.defined; + // })); + + // it('should have appErrorPrefix set properly', inject(function () { + // expect(exceptionHandlerProvider.$get().config.appErrorPrefix) + // .to.equal(mocks.prefix); + // })); + + // it('should throw an error when forced', inject(function () { + // expect(functionThatWillThrow).to.throw(); + // })); + + // it('manual error is handled by decorator', function () { + // var exception; + // exceptionHandlerProvider.configure(mocks.prefix); + // try { + // $rootScope.$apply(functionThatWillThrow); + // } + // catch (ex) { + // exception = ex; + // expect(ex.message).to.equal(mocks.prefix + mocks.errorMessage); + // } + // }); + // }); + }); + + function functionThatWillThrow() { + throw new Error(mocks.errorMessage); + } +}); diff --git a/app/blocks/exception/exception.js b/app/blocks/exception/exception.js new file mode 100644 index 000000000..e5c2cae80 --- /dev/null +++ b/app/blocks/exception/exception.js @@ -0,0 +1,23 @@ +(function () { + 'use strict'; + + angular + .module('blocks.exception') + .factory('exception', exception); + + exception.$inject = ['logger']; + + /* @ngInject */ + function exception(logger) { + var service = { + catcher: catcher + }; + return service; + + function catcher(message) { + return function (reason) { + logger.error(message, reason); + }; + } + } +})(); diff --git a/app/blocks/exception/exception.module.js b/app/blocks/exception/exception.module.js new file mode 100644 index 000000000..18900c551 --- /dev/null +++ b/app/blocks/exception/exception.module.js @@ -0,0 +1,5 @@ +(function () { + 'use strict'; + + angular.module('blocks.exception', ['blocks.logger']); +})(); diff --git a/app/blocks/logger/logger.js b/app/blocks/logger/logger.js new file mode 100644 index 000000000..c43368627 --- /dev/null +++ b/app/blocks/logger/logger.js @@ -0,0 +1,43 @@ +(function () { + 'use strict'; + + angular + .module('blocks.logger') + .factory('logger', logger); + + logger.$inject = ['$log']; + + /* @ngInject */ + function logger($log) { + var service = { + showToasts: false, + + error: error, + info: info, + success: success, + warning: warning, + + // straight to console; bypass toastr + log: $log.log + }; + + return service; + ///////////////////// + + function error(message, data, title) { + $log.error('Error: ' + message, data); + } + + function info(message, data, title) { + $log.info('Info: ' + message, data); + } + + function success(message, data, title) { + $log.info('Success: ' + message, data); + } + + function warning(message, data, title) { + $log.warn('Warning: ' + message, data); + } + } +}()); diff --git a/app/blocks/logger/logger.module.js b/app/blocks/logger/logger.module.js new file mode 100644 index 000000000..514b4a397 --- /dev/null +++ b/app/blocks/logger/logger.module.js @@ -0,0 +1,5 @@ +(function () { + 'use strict'; + + angular.module('blocks.logger', []); +})(); diff --git a/app/index.jade b/app/index.jade index 2f3c4dca3..7829aff02 100644 --- a/app/index.jade +++ b/app/index.jade @@ -30,14 +30,19 @@ html body(ng-app="topcoder", ng-controller="TopcoderController as main", ng-strict-di) - include ./layout/header/sidebar.jade - include ./layout/header/header.jade + //- include ./layout/header/header.jade + div(ui-view="header") + + //- include ./layout/header/sidebar.jade + div(ui-view="sidebar") + .view-container(ng-class="{slided: main.sidebarActive}") - div(ui-view, ng-class="$state.current.name") + div(ui-view="container", ng-class="$state.current.name") - include ./layout/footer/footer.jade + //- include ./layout/footer/footer.jade + div(ui-view="footer") // build:js js/vendor.js //- bower:js @@ -60,13 +65,18 @@ html script(src="topcoder.constants.js") script(src="topcoder.controller.js") script(src="topcoder.interceptors.js") + script(src="topcoder.routes.js") script(src="account/account.module.js") script(src="layout/layout.module.js") script(src="peer-review/peer-review.module.js") - script(src="filters/local-time.filter.js") + script(src="sample/sample.module.js") + script(src="blocks/exception/exception.module.js") + script(src="blocks/logger/logger.module.js") script(src="account/account.routes.js") + script(src="filters/local-time.filter.js") script(src="peer-review/peer-review.routes.js") script(src="peer-review/slideable.directive.js") + script(src="sample/sample.routes.js") script(src="services/api.service.js") script(src="services/auth.service.js") script(src="services/authtoken.service.js") @@ -78,12 +88,27 @@ html script(src="services/user.service.js") script(src="account/login/login.controller.js") script(src="account/register/register.controller.js") + script(src="blocks/exception/exception-handler.provider.js") + script(src="blocks/exception/exception.js") + script(src="blocks/logger/logger.js") + script(src="bower_components/lodash/lodash.js") + script(src="bower_components/lodash/lodash.min.js") + script(src="bower_components/angular/angular.js") + script(src="bower_components/angular/angular.min.js") + script(src="bower_components/angular/index.js") + script(src="bower_components/restangular/Gruntfile.js") + script(src="bower_components/restangular/karma.conf.js") + script(src="bower_components/restangular/karma.underscore.conf.js") script(src="layout/header/header.controller.js") script(src="peer-review/completed-review/completed-review.controller.js") + script(src="peer-review/edit-review/edit-review.controller.js") script(src="peer-review/readOnlyScorecard/readOnlyScorecard.controller.js") script(src="peer-review/review-status/review-status.controller.js") script(src="peer-review/review-status/review-status.filter.js") - script(src="peer-review/edit-review/edit-review.controller.js") + script(src="bower_components/restangular/dist/restangular.js") + script(src="bower_components/restangular/dist/restangular.min.js") + script(src="bower_components/restangular/test/restangularSpec.js") + script(src="bower_components/restangular/src/restangular.js") //- endinject // inject:templates.js diff --git a/app/peer-review/peer-review.routes.js b/app/peer-review/peer-review.routes.js index 26a936087..1c681ca53 100644 --- a/app/peer-review/peer-review.routes.js +++ b/app/peer-review/peer-review.routes.js @@ -11,28 +11,41 @@ function routes($stateProvider, $urlRouterProvider, $httpProvider) { var name, state, states; states = { - reviewStatus: { + review: { + abstract: true, + parent: 'root', + template: '
', + data: { + authRequired: true, + } + }, + 'reviewStatus': { + parent: 'review', url: '/challenge/:challengeId', templateUrl: 'peer-review/review-status/review-status.html', - controller: 'ReviewStatusController as vm', - authenticate: true + controller: 'ReviewStatusController', + controllerAs: 'vm', + data: { + title: 'Peer Review' + } }, - readOnlyScorecard: { + 'readOnlyScorecard': { + parent: 'review', url: '/scorecard/:scorecardId', templateUrl: 'peer-review/readOnlyScorecard/readOnlyScorecard.html', controller: 'ReadOnlyScorecardController' }, - completed: { + 'completed': { + parent: 'review', url: '/:challengeId/reviews/:reviewId/completed', templateUrl: 'peer-review/completed-review/completed-review.html', controller: 'CompletedReviewController', - authenticate: true }, - edit: { + 'edit': { + parent: 'review', url: '/:challengeId/reviews/:reviewId/edit', templateUrl: 'peer-review/edit-review/edit-review.html', controller: 'EditReviewController', - authenticate: true } }; for (name in states) { diff --git a/app/sample/sample.module.js b/app/sample/sample.module.js new file mode 100644 index 000000000..b66bb3ba7 --- /dev/null +++ b/app/sample/sample.module.js @@ -0,0 +1,12 @@ +(function() { + 'use strict'; + + var dependencies = [ + 'angular-jwt', + 'ui.router', + ]; + + angular + .module('tc.sample', dependencies); + +})(); diff --git a/app/sample/sample.routes.js b/app/sample/sample.routes.js new file mode 100644 index 000000000..dc205a319 --- /dev/null +++ b/app/sample/sample.routes.js @@ -0,0 +1,36 @@ +(function() { + 'use strict'; + + angular.module('tc.sample').config([ + '$stateProvider', + '$urlRouterProvider', + routes + ]); + + function routes($stateProvider, $urlRouterProvider) { + var name, state, states; + states = { + sample: { + parent: 'root', + abstract: true, + url: '/sample', + template: '
Sample test code
', + data: { + authRequired: false, + title: 'Sample page' + } + }, + 'sample.child1': { + url: '/child1', + template: '
Sample child1
', + data: { + title: 'Child1' + } + } + }; + for (name in states) { + state = states[name]; + $stateProvider.state(name, state); + } + }; +})(); diff --git a/app/services/helpers.service.js b/app/services/helpers.service.js index 288344164..b42ff1f98 100644 --- a/app/services/helpers.service.js +++ b/app/services/helpers.service.js @@ -14,7 +14,8 @@ parseAnswers: parseAnswers, compileReviewItems: compileReviewItems, countCompleted: countCompleted, - getParameterByName: getParameterByName + getParameterByName: getParameterByName, + getPageTitle: getPageTitle }; return service; @@ -92,5 +93,67 @@ } return results; } + + /** + * Given a string of the type 'object.property.property', traverse the given context (eg the current $state object) and return the + * value found at that path. + * + * @param objectPath + * @param context + * @returns {*} + */ + function _getObjectValue(objectPath, context) { + var i; + var propertyArray = objectPath.split('.'); + var propertyReference = context; + for (i = 0; i < propertyArray.length; i++) { + if (angular.isDefined(propertyReference[propertyArray[i]])) { + propertyReference = propertyReference[propertyArray[i]]; + } else { + // if the specified property was not found, default to the state's name + return undefined; + } + } + return propertyReference; + } + + /** + * + * @param template + * @param context + * @returns {*} + */ + function _renderTemplateStr(template, context) { + var str2BCompiled = template.match(/{{[.\w]+}}/g); + var compiledMap = {}; + if (str2BCompiled) { + str2BCompiled.forEach(function(str) { + var expr = str.replace('{{', '').replace('}}', ''); + compiledMap[str] = _getObjectValue(expr.trim(), context); + }); + // now loop over all keys and replace with compiled value + Object.keys(compiledMap).forEach(function(k) { + template = template.replace(k, compiledMap[k]) + }); + } + return template; + } + + + function getPageTitle(state, $currentState) { + var title = ''; + if (state.data && state.data.title) { + title = state.data.title; + if (title.indexOf('{{') > -1) { + // dynamic data + var resolveData = $currentState.local.resolve.$$values; + title = _renderTemplateStr(title, resolveData); + } + } + if (title) { + title += ' | ' + } + return title + 'TopCoder'; + } } })(); diff --git a/app/topcoder.module.js b/app/topcoder.module.js index d5e6665e9..017213920 100644 --- a/app/topcoder.module.js +++ b/app/topcoder.module.js @@ -5,7 +5,10 @@ 'tc.layout', 'tc.account', 'tc.peer-review', + 'tc.sample', 'ui.router', + 'blocks.logger', 'blocks.exception', + 'restangular', 'ngCookies' ]; @@ -13,17 +16,28 @@ .module('topcoder', dependencies) .run(appRun); - appRun.$inject = ['$rootScope', '$state', 'auth', '$cookies']; + appRun.$inject = ['$rootScope', '$state', 'auth', '$cookies', 'helpers', '$log']; - function appRun($rootScope, $state, auth, $cookies) { + function appRun($rootScope, $state, auth, $cookies, helpers, $log) { // Attaching $state to the $rootScope allows us to access the // current state in index.html (see div with ui-view on the index page) $rootScope.$state = $state; - $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) { - if(toState.authenticate && !auth.isAuthenticated()) { - console.log('State requires authentication, and user is not logged in.'); + // check AuthNAuth on change state start + $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) { + if (toState.data.authRequired && !auth.isAuthenticated()) { + $log.debug('State requires authentication, and user is not logged in, redirecting'); + // setup redirect for post login + event.preventDefault(); + var next = $state.href(toState.name, toParams, {absolute: false}); + $state.go('login', {next: next}); } + + }); + + $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) { + // set document title + document.title = helpers.getPageTitle(toState, $state.$current); }); } diff --git a/app/topcoder.routes.js b/app/topcoder.routes.js new file mode 100644 index 000000000..c835f45fc --- /dev/null +++ b/app/topcoder.routes.js @@ -0,0 +1,54 @@ +(function() { + 'use strict'; + + angular.module('topcoder').config([ + '$stateProvider', + '$urlRouterProvider', + routes + ]); + + function routes($stateProvider, $urlRouterProvider) { + var name, state, states; + states = { + '404': { + parent: 'root', + url: '/404', + templateUrl: '404.html', + data: { + title: 'Page Not Found', + } + }, + /** + * Base state that all other routes should inherit from. + * Child routes can override any of the specified regions + */ + 'root': { + url: '', + abstract: true, + data: { + authRequired: false, + }, + views: { + 'header@': { + templateUrl: 'layout/header/header.html', + controller: 'HeaderController' + }, + 'sidebar@': { + // TODO revisit to see how the layout works + templateUrl: 'layout/header/sidebar.html', + }, + 'container@': { + template: "
Main container, add your stuff here
" + }, + 'footer@': { + templateUrl: 'layout/footer/footer.html', + } + } + } + }; + for (name in states) { + state = states[name]; + $stateProvider.state(name, state); + } + }; +})();