├── .gitignore ├── LICENSE ├── README.md ├── build ├── .bowerrc ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── bower.json ├── build.config.js ├── package.json └── requirejs │ ├── bootstrap_dev.js │ └── bootstrap_prod.js ├── client ├── assets │ ├── css │ │ ├── bootstrap.css │ │ └── monokai.css │ ├── data │ │ ├── images │ │ │ └── q1_1.jpg │ │ └── quiz_1.json │ ├── font │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── images │ │ ├── bkgrnd.jpg │ │ ├── body_bg.gif │ │ ├── gray_bg.gif │ │ ├── login_btn.png │ │ └── shadow.png │ ├── js │ │ └── boot.js │ ├── less │ │ ├── bootstrap-responsive.less │ │ ├── bootstrap.less │ │ ├── main.less │ │ └── variables.less │ └── views │ │ ├── login.tpl.html │ │ ├── quiz.tpl.html │ │ └── score.tpl.html ├── index.html ├── src │ ├── main.js │ ├── mindspace │ │ ├── directives │ │ │ └── Gravatar.js │ │ ├── interceptors │ │ │ └── ResponseInterceptor.js │ │ └── utils │ │ │ ├── BrowserDetect.js │ │ │ ├── DateTime.js │ │ │ ├── Factory.js │ │ │ ├── createGuid.js │ │ │ ├── crypto │ │ │ └── md5.js │ │ │ ├── logger │ │ │ ├── ExternalLogger.js │ │ │ ├── LogDecorator.js │ │ │ └── LogEnhancer.js │ │ │ ├── makeTryCatch.js │ │ │ └── supplant.js │ └── quizzer │ │ ├── authentication │ │ ├── AuthenticateModule.js │ │ ├── Authenticator.js │ │ ├── LoginController.js │ │ ├── Session.js │ │ └── SessionController.js │ │ └── quiz │ │ ├── QuizModule.js │ │ ├── RouteManager.js │ │ ├── builders │ │ ├── QuizBuilder.js │ │ └── ScoreBuilder.js │ │ ├── controllers │ │ ├── ScoreController.js │ │ └── TestController.js │ │ └── delegates │ │ └── QuizDelegate.js └── test │ ├── config │ ├── bootstrap.js │ ├── karma.conf.js │ └── karmaBoot.js │ ├── spec │ └── quizzer │ │ └── authentication │ │ ├── AuthenticatorSpec.js │ │ ├── LoginControllerSpec.js │ │ └── SessionSpec.js │ └── testRunner.html ├── docs ├── Proveyourself.pdf ├── Quizzler_Workflow.jpg ├── Quizzler_Workflow_large.fw.png ├── Quizzler_Workflow_large.jpg └── quiz_comps.jpg └── tools └── webserver ├── Configure NodeJS Server.jpg ├── node_modules └── httpServers.coffee ├── npm-debug.log └── run.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | .idea/ 4 | .project/ 5 | .settings/ 6 | /build/docs/ 7 | /deploy/ 8 | /client/vendor/ 9 | /build/node_modules/ 10 | /build/npm-debug.log 11 | 12 | /bin/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![quizzler_workflow](https://f.cloud.github.com/assets/210413/1701194/d97319f4-6046-11e3-8442-05b549afaa1a.jpg) 2 | 3 | * Click here to use the [Live Demo](http://thomasburleson.github.io/angularjs-Quizzler/#/loginl) 4 | * Click here to review the [Live Jasmine TestRunner](http://thomasburleson.github.io/angularjs-Quizzler/test/testRunner.html) output. 5 | 6 | --- 7 | 8 | ### Purpose of a Challenge 9 | 10 | The purpose of the challenge is to present a **work-from-home** challenge to developers interviewing for positions on AngularJS projects. Their **challenge** is to create an online HTML5 application... And I preface the request with a comment that this is a non-trivial request imposed on the candidate. 11 | 12 | But such a challenge has HUGE benefits in that it (1) allows a *review team* myriad opportunities to perform a full-suite assessment of the *developer* candidate and (2) it also removes much of the `time` factor from the challenge. Candidates can use their own tools, own resources, and invest significantly more time into the project than would otherwise be available in a single screensharing session such as **Google Hangout**. 13 | 14 | Ideally, you have candidates to whom you can present this challenge... candidates that have not seen/studied the solution presented here. ;-) 15 | 16 | --- 17 | 18 | ### The `Quizzler` Challenge 19 | 20 | The developer candidate is asked to implement an AngularJS web application that will allow users to take a quiz, evaluate the *given* answers, and present a review of quiz scoring to the user/tester. The review should show the correct answers and - when appropriate - also show the tester their incorrect answers. Logout should also be supported after the review in presented. 21 | 22 | * Six (6) questions are initially provided; without answers to the actual questions. 23 | 24 | Once the developer has **finished** (to whatever level they decide is appropriate), the developer should configure a GitHub repository as well as an online verion of the live app. How the demo is deployed is up to the developer. 25 | 26 | Upon completion [or partial completion] of the challenge, the review team's assessment will consider factors such as: 27 | 28 | * motivation, attitude, and competence 29 | * skill levels, best practices, coding style, clarity-of-code 30 | * architecture solutions, component implementation, and DRY-ness 31 | * solution usability and user testing features 32 | * and more… 33 | 34 | The developer should note that the challenge is expected to require approximately 20-40 hours of effort. 35 | 36 | * See the [Requirements PDF](https://github.com/thomasburleson/angularjs-Quizzler/blob/master/docs/Proveyourself.pdf?raw=true) 37 | * See the [Design Requirements](https://f.cloud.github.com/assets/210413/1786317/ea38fd30-68f2-11e3-9efc-9ee48607a87f.jpg) 38 | 39 | ### Technical Requirements 40 | 41 | The developer should use: 42 | 43 | * GitHub 44 | * AngularJS 45 | * Jasmine 46 | * Bonus points for use of HAML and CoffeeScript 47 | 48 | Additionally the developer should: 49 | 50 | * Avoid jQuery (rely on AngularJS’ JQLite where possible) 51 | * Organize project in a way that makes sense. 52 | * Provide sufficient test coverage for all Javascript. 53 | 54 | --- 55 | 56 | ### Application Implementation 57 | 58 | > 59 | Before you explore the source code for Quizzler, I highly recommend that you first read the Dependency Injection using RequireJS and AngularJS tutorial; since all of the source uses both RequireJS injection and AngularJS injection. 60 | 61 | Quizzler is an AngularJS `online quiz builder and testing` application; developed as a deliverable to a challenge presented to developers who are interviewing at `www..com`. 62 | 63 | Visitors can either walkthru the [Live Demo](http://thomasburleson.github.io/angularjs-Quizzler/#/login) or simply look at snapshots of running application: 64 | 65 | * [Quizzler Login](https://f.cloud.github.com/assets/210413/1701314/e73aee92-604e-11e3-8624-db4537de9a90.jpg) 66 | * [Quiz](https://f.cloud.github.com/assets/210413/1701315/f1409a72-604e-11e3-9331-989b5f81416c.jpg) 67 | * [Scoring ](https://f.cloud.github.com/assets/210413/1701316/f9660ac0-604e-11e3-9f88-86b080463345.jpg) 68 | 69 | 70 | Using AngularJS (v1.2.x) and RequireJS, `Quizzler` is architected with minimum coupling and crisp bootstrapping. 71 | HeadJS is used to asynchronously load the required scripts **before** bootstrapping the NG application. 72 | 73 | * Quizzler supports 1…n quizes defined in JSON format. 74 | * The quiz data is dynamically loaded and the dynamic workflow will guide the tester thru 1..n questions. 75 | * Quiz questions can contain HTML with references to external images, etc. 76 | 77 | > 78 | ![screen shot 2013-12-08 at 11 15 22 am](https://f.cloud.github.com/assets/210413/1701199/33d97d70-6047-11e3-8768-aa7ad52996de.jpg) 79 | 80 | Extra application features added to the implementation include 81 | 82 | * Use of RequireJS with AngularJS 83 | * Authentication module, 84 | * session management, 85 | * history navigation, 86 | * question validation, 87 | * enhanced logging, 88 | * and more. 89 | 90 | Robust logging is used through-out the application and even logging during the bootstrapping process: before `$log` injection is available; see my blog article [Using Decorators to Enhance AngularJS $log](http://solutionoptimist.com/2013/10/07/enhance-angularjs-logging-using-decorators/) for details. 91 | > 92 | ![quizzler_logging](https://f.cloud.github.com/assets/210413/1701319/e169e7ba-604f-11e3-9f61-8fb45fad300e.jpg) 93 | 94 | 95 | --- 96 | 97 | ### Installation 98 | 99 | Here is a brief explanation of the directory structures: 100 | 101 | * `client`: directory contains the source code, css, html, and vendor libraries for Quizzler. 102 | * `build`: directory contains the Bower settings, initial files for Travis deployment, and pending Grunt files for builds 103 | * `tools`: directory contains a CoffeeScript webserver that allows developers to easily run and debug `Quizzler` 104 | * `docs`: directory contains the initial challenge requirements (PDF) and mockups. 105 | 106 | To install the project, download entire repository to local project directory and open a Terminal window at *local* project directory. Then use **Bower** to install the vendor tools/libraries and **Grunt** to install the build tools: 107 | 108 | ``` 109 | cd ./build 110 | bower install 111 | npm update 112 | ``` 113 | 114 | 115 | Use Terminal to start the CoffeeScript, built-in **Quizzler Web Server** web server; provided in the `tools` directory: 116 | 117 | ``` 118 | cd ./client 119 | coffee ../tools/webserver/run.coffee 120 | ``` 121 | 122 | Launch webServer. Developers can use the `./client` directory as the webroot... So open browser and navigate to `http://localhost:8000/index.html`. 123 | 124 | 125 | A deployment process is now available to deploy your application in development mode or production mode. 126 | 127 | `Development` mode deploys copy all the class files to the webroot and uses RequireJS to lazy load required class files. Use the following Terminal commands: 128 | 129 | ``` 130 | cd ./build 131 | grunt dev 132 | ``` 133 | 134 | Production deploys use Grunt RequireJS to concatenate, minimize, and deploy a production version of `Quizzler` . All application code is compressed into `webroot/assets/js/quizzler.js`. 135 | 136 | ``` 137 | cd ./build 138 | grunt prod 139 | ``` 140 | 141 | 142 | --- 143 | 144 | ### Pending Features 145 | 146 | Considering the short deliverable time for this solution, the current implementation has several aspects that should/must be improved: 147 | 148 | * Implementation of multiple quizes with quiz selector 149 | * Persistence of Quiz results 150 | * Reduction and clarity-improvement of CSS using LessCSS 151 | * Improved UX for Login 152 | * input fields color code when errors occur 153 | * input fields clear state indicators when typing 154 | * focus indicators improved 155 | * implementation of register 156 | * auto-restore last-signin email 157 | * look-ahead for email field 158 | * Improved UX for Quiz 159 | * Hover indicators for questions 160 | * Field for user-submitted questions/comments 161 | * Timer for entire quiz 162 | * Breadcrumbs for quiz 163 | * Animation of questions as user selects *continue* or *submit* 164 | * Splash Preloading screen with progress indicator 165 | * Header bar with email 166 | * Footer bar with copyright information 167 | 168 | Additionally developer workflow processes could be significantly improved with the following: 169 | 170 | * Use of grunt for builds/deploys 171 | * Use of Git pre-commit hooks to run tests 172 | * Use of JSHint checks 173 | * Deployment to Jenkins/Travis for CI and testing 174 | * Use of CoffeeScript instead of hand-written Javascript 175 | * Minification of application code 176 | 177 | --- 178 | 179 | ### Working In-Progress 180 | 181 | NOTE: Launching of Jasmine unit tests and execution of Karma should be considered in-complete and are currently **in-progress**! 182 | 183 | -------------------------------------------------------------------------------- /build/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "../client/vendor", 3 | "json": "bower.json" 4 | } -------------------------------------------------------------------------------- /build/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : false, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : false, // true: Require {} for every new block or scope 11 | "eqeqeq" : false, // true: Require triple equals (===) for comparison 12 | "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : false, // {int} Number of spaces to use for indentation 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : false, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : false, // true: Prohibit use of empty blocks 19 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : false, // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : false, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : false, // true: Require all defined variables be used 28 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 29 | "trailing" : false, // true: Prohibit trailing whitespaces 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 44 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 45 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 46 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 47 | "iterator" : false, // true: Tolerate using the `__iterator__` property 48 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 49 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 50 | "laxcomma" : false, // true: Tolerate comma-first style coding 51 | "loopfunc" : false, // true: Tolerate functions being defined in loops 52 | "multistr" : false, // true: Tolerate multi-line strings 53 | "proto" : false, // true: Tolerate using the `__proto__` property 54 | "scripturl" : false, // true: Tolerate script-targeted URLs 55 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 56 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 57 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 58 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 59 | "validthis" : false, // true: Tolerate using this in a non-constructor function 60 | 61 | // Environments 62 | "browser" : true, // Web Browser (window, document, etc) 63 | "couch" : false, // CouchDB 64 | "devel" : true, // Development/debugging (alert, confirm, etc) 65 | "dojo" : false, // Dojo Toolkit 66 | "jquery" : true, // jQuery 67 | "mootools" : false, // MooTools 68 | "node" : true, // Node.js 69 | "nonstandard" : true, // Widely adopted globals (escape, unescape, etc) 70 | "prototypejs" : false, // Prototype and Scriptaculous 71 | "rhino" : false, // Rhino 72 | "worker" : false, // Web Workers 73 | "wsh" : false, // Windows Scripting Host 74 | "yui" : false, // Yahoo User Interface 75 | 76 | // Legacy 77 | "nomen" : false, // true: Prohibit dangling `_` in variables 78 | "onevar" : false, // true: Allow only one `var` statement per function 79 | "passfail" : false, // true: Stop on first error 80 | "white" : false, // true: Check against strict whitespace and indentation rules 81 | 82 | // Custom Globals 83 | "predef" : [ ] // additional predefined global variables 84 | } 85 | -------------------------------------------------------------------------------- /build/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - npm install --quiet -g grunt-cli bower 8 | - bower install 9 | - npm install karma ../client/test 10 | - npm install protractor ../client/test 11 | - npm install karma-jasmine ../client/test 12 | - npm install karma-requirejs ../client/test 13 | 14 | script: grunt 15 | 16 | -------------------------------------------------------------------------------- /build/Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | /** 4 | * Load required Grunt tasks. These are installed based on the versions listed 5 | * in `package.json` when you do `npm install` in this directory. 6 | */ 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | grunt.loadNpmTasks('grunt-contrib-uglify'); 9 | grunt.loadNpmTasks('grunt-contrib-clean'); 10 | grunt.loadNpmTasks('grunt-contrib-concat'); 11 | grunt.loadNpmTasks('grunt-contrib-copy'); 12 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 13 | 14 | /** 15 | * Load in our build configuration file. 16 | */ 17 | var userConfig = require('./build.config.js'); 18 | 19 | /** 20 | * This is the configuration object Grunt uses to give each plugin its 21 | * instructions. 22 | */ 23 | var taskConfig = { 24 | 25 | /** 26 | * We read in our `package.json` file so we can access the package name and version. It's already there, so 27 | * we don't repeat ourselves here. 28 | */ 29 | pkg: grunt.file.readJSON("package.json"), 30 | 31 | /** 32 | * The banner is the comment that is placed at the top of our compiled source files. It is first processed 33 | * as a Grunt template, where the `<%=` pairs are evaluated based on this very configuration object. 34 | */ 35 | meta: { 36 | banner: '/**\n' + 37 | ' * @appName <%= pkg.name %>\n' + 38 | ' * @version <%= pkg.version %>\n' + 39 | ' * @date <%= grunt.template.today("yyyy-mm-dd") %>\n' + 40 | ' * @homepage <%= pkg.homepage %>\n' + 41 | ' * @copyright <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + 42 | ' * Licensed <%= pkg.licenses.type %> <<%= pkg.licenses.url %>>\n' + 43 | ' */\n' 44 | }, 45 | 46 | /** 47 | * The directories to delete when `grunt clean` is executed. 48 | */ 49 | clean: { 50 | src: [ 51 | '<%= buildDir %>' 52 | ], 53 | hooks: [ 54 | ], 55 | options: { 56 | force: true 57 | } 58 | }, 59 | 60 | /** 61 | * The `copy` task just copies files from A to B. We use it here to copy our project assets 62 | * (images, fonts, etc.) and javascripts into `buildDir`, and then to copy the assets to `compileDir`. 63 | */ 64 | copy: { 65 | index: { 66 | files: [ 67 | { 68 | src: '<%= devDir %>/index.html', 69 | dest: '<%= buildDir %>/index.html' 70 | } 71 | ] 72 | }, 73 | build_assets: { 74 | files: [ 75 | { 76 | src: [ '**', '!less/**' ], 77 | cwd: '<%= devDir %>/assets', 78 | dest: '<%= buildDir %>/assets/', 79 | expand: true 80 | } 81 | ] 82 | }, 83 | build_appjs: { 84 | files: [ 85 | { 86 | src: [ '**/*.js' ], 87 | cwd: '<%= devDir %>/src', 88 | dest: '<%= buildDir %>/src/', 89 | expand: true 90 | } 91 | ] 92 | }, 93 | prod_boot: { 94 | files: [ 95 | { 96 | src: './requirejs/bootstrap_prod.js', 97 | dest: '<%= buildDir %>/assets/js/boot.js', 98 | expand: false 99 | } 100 | ] 101 | }, 102 | dev_boot: { 103 | files: [ 104 | { 105 | src: './requirejs/bootstrap_dev.js', 106 | dest: '<%= buildDir %>/assets/js/boot.js' 107 | } 108 | ] 109 | }, 110 | build_vendorjs: { 111 | files: [ 112 | { 113 | src: [ '**' ], 114 | cwd: '<%= devDir %>/vendor', 115 | dest: '<%= buildDir %>/vendor', 116 | expand: true 117 | } 118 | ] 119 | }, 120 | compile: { 121 | files: [ 122 | { 123 | src: [ '**' ], 124 | cwd: '<%= buildDir %>/assets', 125 | dest: '<%= compileDir %>/assets', 126 | expand: true 127 | }, 128 | { 129 | src: [ '**' ], 130 | dest: '<%= compileDir %>/vendor', 131 | cwd: '<%= buildDir %>/vendor', 132 | expand: true 133 | } 134 | ] 135 | } 136 | }, 137 | 138 | /** 139 | * `grunt concat` concatenates multiple source files into a single file. 140 | */ 141 | concat: { 142 | 143 | /** 144 | * The `source` target is the concatenation of our application source code and all specified vendor 145 | * source code into a single file. 146 | */ 147 | source: { 148 | options: { 149 | banner: '<%= meta.banner %>' 150 | }, 151 | src: [ 152 | '<%= buildDir %>/assets/js/quizzler.js' 153 | ], 154 | dest: '<%= buildDir %>/assets/js/quizzler.js' 155 | } 156 | }, 157 | 158 | 159 | /** 160 | * `jshint` defines the rules of our linter as well as which files we should check. This file, all javascript 161 | * sources, and all our unit tests are linted based on the policies listed in `options`. But we can also 162 | * specify exclusionary patterns by prefixing them with an exclamation point (!); this is useful when code comes 163 | * from a third party but is nonetheless inside `src/`. 164 | */ 165 | jshint: { 166 | src: [ 167 | '<%= appFiles.js %>' 168 | ], 169 | test: [ 170 | '<%= appFiles.jsunit %>' 171 | ], 172 | scenario: [ 173 | '<%= appFiles.jsscenario %>' 174 | ], 175 | gruntfile: [ 176 | 'Gruntfile.js' 177 | ], 178 | options: { 179 | curly: true, 180 | immed: true, 181 | newcap: true, 182 | noarg: true, 183 | sub: true, 184 | boss: true, 185 | eqnull: true 186 | }, 187 | globals: {} 188 | }, 189 | 190 | /** 191 | * Minifies RJS files and makes it production ready 192 | * Build files are minified and encapsulated using RJS Optimizer plugin 193 | */ 194 | requirejs: { 195 | compile: { 196 | options: { 197 | baseUrl: "../client/src", 198 | paths : 199 | { 200 | // Configure alias to full paths; relative to `baseURL` 201 | 202 | 'auth' : './quizzer/authentication', 203 | 'quiz' : './quizzer/quiz', 204 | 'utils' : './mindspace/utils' 205 | 206 | }, 207 | out: '<%= buildDir %>/assets/js/quizzler.js', 208 | name: 'main' 209 | 210 | }, 211 | preserveLicenseComments : false, 212 | optimize: "uglify" 213 | } 214 | } 215 | 216 | }; 217 | 218 | grunt.initConfig(grunt.util._.extend(taskConfig, userConfig)); 219 | 220 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 221 | // Register Tasks 222 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 223 | 224 | grunt.registerTask("dev", [ 225 | 'clean:src', 226 | 'copy:build_assets', 227 | 'copy:build_appjs', 228 | 'copy:build_vendorjs', 229 | 'copy:dev_boot', 230 | 'copy:index' 231 | 232 | ]); 233 | 234 | grunt.registerTask( "prod", [ 235 | 'clean:src', 236 | 'copy:build_assets', 237 | 'copy:build_vendorjs', 238 | 'copy:prod_boot', 239 | 'copy:index', 240 | "requirejs", 241 | "concat:source" 242 | ]); 243 | 244 | 245 | function stripBanner( src ) { 246 | var m = [ 247 | '(?:.*\\/\\/.*\\r?\\n)*\\s*', // Strip // ... leading banners. 248 | '\\/\\*[\\s\\S]*?\\*\\/' // Strips all /* ... */ block comment banners. 249 | ], 250 | re = new RegExp('^\\s*(?:' + m.join('|') + ')\\s*', ''); 251 | 252 | return src.replace(re, '', "gm"); 253 | }; 254 | }; 255 | -------------------------------------------------------------------------------- /build/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quizzler", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/Mindspace/Desk-Quizzler", 5 | "authors": [ 6 | "Thomas Burleson " 7 | ], 8 | "description": "Dynamic Online-Quiz application using AngularJS HTML5", 9 | "main": "index.html", 10 | "license": "GPL", 11 | "ignore": [ 12 | "**/.*", 13 | ".node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "headjs-notify": "0.9.7", 20 | "jquery": "~2.0.3", 21 | "underscore": "~1.5.1", 22 | "greensock-js": "1.10.2", 23 | "highlightjs": "7.5.0", 24 | "requirejs": "~2.1.8", 25 | "requirejs-text": "2.0.10", 26 | "angular": "~1.2.3", 27 | "angular-animate": "~1.2.1", 28 | "angular-route": "~1.2.1", 29 | "angular-sanitize": "~1.2.4", 30 | "angular-ui-router": "~0.2.0", 31 | "angular-ui-utils": "~0.0.4", 32 | "angular-bootstrap": "~0.6.0", 33 | "angular-mocks": "~1.2.0", 34 | "angular-specs": "~0.0.1", 35 | "angular-scenario": "~1.2.0", 36 | "jasmine": "~1.3.1", 37 | "jasmine-as-promised": "0.0.6", 38 | "sinon": "~1.7.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build/build.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file/module contains all configuration for the build process. 3 | */ 4 | module.exports = { 5 | 6 | devDir : "../client", 7 | buildDir : "../bin", 8 | compileDir : "../deploy", 9 | 10 | appFiles: { 11 | 12 | js: [ 13 | "../client/src/**/*.js" 14 | ], 15 | 16 | jsunit: [ 17 | "../client/test/**/*Spec.js", 18 | "../client/test/*.js" 19 | ], 20 | 21 | jsscenario: [ 22 | "../client/test/**/*Scenario.js", 23 | "../client/test/*.js" 24 | ], 25 | 26 | templates : [ 27 | "../client/src/assets/views/**/*.tpl.html" 28 | ], 29 | 30 | html: [ 31 | "../client/src/index.html" 32 | ], 33 | 34 | css : [ 35 | "../client/assets/css/bootstrap.css", 36 | "../client/assets/css/monokai.css" 37 | ], 38 | 39 | less: "../client/src/assets/less/main.less" 40 | }, 41 | 42 | /** 43 | * The compiled HTML template JavaScript file as well as the name of the template angular module. 44 | */ 45 | htmlTemplateName: "HTMLTemplateModule", 46 | 47 | /** 48 | * This is the same as `appFiles`, except it contains patterns that reference vendor code (`vendor/`) that we 49 | * need to place into the build process somewhere. While the `appFiles` property ensures all standardized files 50 | * are collected for compilation, it is the user's job to ensure non-standardized (i.e. vendor-related) files are 51 | * handled appropriately in `vendorFiles.js`. 52 | * 53 | * The `vendorFiles.js` property holds files to be automatically concatenated and minified with our project source 54 | * files. 55 | * 56 | * The `vendorFiles.css` property holds any CSS files to be automatically included in our app. 57 | */ 58 | vendorFiles: { 59 | js: [ 60 | "../client/vendor/angular/angular.js", 61 | "../client/vendor/angular-route/angular-route.js", 62 | "../client/vendor/angular-sanitize/angular-sanitize.js", 63 | "../client/vendor/headjs-notify/src/load.js", 64 | "../client/vendor/require/require.js", 65 | "../client/vendor/requirejs-text/text.js", 66 | "../client/vendor/underscore.js", 67 | "../client/vendor/highlightjs/highlight.pack.js" 68 | ], 69 | css: [ 70 | ] 71 | }, 72 | 73 | /** 74 | * Defines server ports for local, static web server and karma servers. 75 | */ 76 | ports: { 77 | webServer: { 78 | build : 8890, 79 | compile: 8888 80 | }, 81 | karma: { 82 | unit: { 83 | runnerPort: 9101, 84 | port: 9877 85 | } 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Thomas Burleson ", 3 | "name": "Quizzler", 4 | "version": "0.0.2", 5 | "homepage": "http://mindspace.github.io/Desk-Quizzler/", 6 | "bugs": "", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Mindspace/Desk-Quizzler" 10 | }, 11 | "licenses": { 12 | "type" : "GPL", 13 | "url" : "https://github.com/Mindspace/Desk-Quizzler/blob/master/LICENSE" 14 | }, 15 | "dependencies": { 16 | }, 17 | "devDependencies": { 18 | "grunt": "~0.4.1", 19 | "grunt-contrib-clean": "~0.4.1", 20 | "grunt-contrib-copy": "~0.4.1", 21 | "grunt-contrib-jshint": "~0.4.3", 22 | "grunt-contrib-concat": "~0.3.0", 23 | "grunt-contrib-uglify": "~0.2.0", 24 | "grunt-ngmin": "0.0.2", 25 | "grunt-html2js": "~0.1.3", 26 | "grunt-bump": "0.0.6", 27 | "grunt-contrib-connect": "~0.5.0", 28 | "grunt-shell": "~0.3.1", 29 | "grunt-express": "~1.0", 30 | "xml2js": "~0.2.8", 31 | "open": "0.0.4", 32 | "hogan.js": "~2.0.0", 33 | "grunt-jsbeautifier": "~0.2.2", 34 | "grunt-contrib-requirejs": "~0.4.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /build/requirejs/bootstrap_dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use aysnc script loader, configure the application module (for AngularJS) 3 | * and initialize the application ( which configures routing ) 4 | * 5 | * @author Thomas Burleson 6 | */ 7 | 8 | (function( window, head ) { 9 | "use strict"; 10 | 11 | head.js( 12 | 13 | // Pre-load these for splash-screen progress bar... 14 | 15 | { require : "./vendor/requirejs/require.js", size: "80196" }, 16 | { underscore : "./vendor/underscore/underscore.js", size: "43568" }, 17 | 18 | { angular : "./vendor/angular/angular.js", size: "551057" }, 19 | { ngRoute : "./vendor/angular-route/angular-route.js", size: "30052" }, 20 | { ngSanitize : "./vendor/angular-sanitize/angular-sanitize.js", size: "19990" } 21 | 22 | ) 23 | .ready("ALL", function() { 24 | 25 | require.config ( 26 | { 27 | appDir : '', 28 | baseUrl : './src', 29 | priority: 'angular', 30 | paths : 31 | { 32 | // Configure alias to full paths 33 | 34 | 'auth' : './quizzer/authentication', 35 | 'quiz' : './quizzer/quiz', 36 | 'utils' : './mindspace/utils' 37 | 38 | }, 39 | shim : 40 | { 41 | 'underscore': 42 | { 43 | exports : '_' 44 | } 45 | } 46 | }); 47 | 48 | 49 | require( [ "main" ], function( app ) 50 | { 51 | // Application has bootstrapped and started... 52 | }); 53 | 54 | 55 | }); 56 | 57 | 58 | 59 | }( window, head )); 60 | -------------------------------------------------------------------------------- /build/requirejs/bootstrap_prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use aysnc script loader, configure the application module (for AngularJS) 3 | * and initialize the application ( which configures routing ) 4 | * 5 | * @author Thomas Burleson 6 | */ 7 | 8 | (function( window, head ) { 9 | "use strict"; 10 | 11 | head.js( 12 | 13 | // Pre-load these for splash-screen progress bar... 14 | 15 | { require : "./vendor/requirejs/require.js", size: "80196" }, 16 | { underscore : "./vendor/underscore/underscore.js", size: "43568" }, 17 | 18 | { angular : "./vendor/angular/angular.js", size: "551057" }, 19 | { ngRoute : "./vendor/angular-route/angular-route.js", size: "30052" }, 20 | { ngSanitize : "./vendor/angular-sanitize/angular-sanitize.js", size: "19990" }, 21 | 22 | { quizzler : "./assets/js/quizzler.js" } 23 | 24 | ) 25 | .ready("ALL", function() 26 | { 27 | // All application code is concat/uglified in 1 file: `quizzler.js` 28 | 29 | require( [ "main" ], function( app ) 30 | { 31 | // Application has bootstrapped and started... 32 | }); 33 | 34 | 35 | }); 36 | 37 | 38 | 39 | }( window, head )); 40 | -------------------------------------------------------------------------------- /client/assets/css/monokai.css: -------------------------------------------------------------------------------- 1 | /* 2 | Monokai style - ported by Luigi Maselli - http://grigio.org 3 | */ 4 | 5 | pre code { 6 | display: block; padding: 0.5em; 7 | background: #272822; 8 | } 9 | 10 | pre .tag, 11 | pre .tag .title, 12 | pre .keyword, 13 | pre .literal, 14 | pre .strong, 15 | pre .change, 16 | pre .winutils, 17 | pre .flow, 18 | pre .lisp .title, 19 | pre .clojure .built_in, 20 | pre .nginx .title, 21 | pre .tex .special { 22 | color: #F92672; 23 | } 24 | 25 | pre code { 26 | color: #DDD; 27 | } 28 | 29 | pre code .constant, 30 | pre .asciidoc .code { 31 | color: #66D9EF; 32 | } 33 | 34 | pre .code, 35 | pre .class .title, 36 | pre .header { 37 | color: white; 38 | } 39 | 40 | pre .link_label, 41 | pre .attribute, 42 | pre .symbol, 43 | pre .symbol .string, 44 | pre .value, 45 | pre .regexp { 46 | color: #BF79DB; 47 | } 48 | 49 | pre .link_url, 50 | pre .tag .value, 51 | pre .string, 52 | pre .bullet, 53 | pre .subst, 54 | pre .title, 55 | pre .emphasis, 56 | pre .haskell .type, 57 | pre .preprocessor, 58 | pre .pragma, 59 | pre .ruby .class .parent, 60 | pre .built_in, 61 | pre .sql .aggregate, 62 | pre .django .template_tag, 63 | pre .django .variable, 64 | pre .smalltalk .class, 65 | pre .javadoc, 66 | pre .django .filter .argument, 67 | pre .smalltalk .localvars, 68 | pre .smalltalk .array, 69 | pre .attr_selector, 70 | pre .pseudo, 71 | pre .addition, 72 | pre .stream, 73 | pre .envvar, 74 | pre .apache .tag, 75 | pre .apache .cbracket, 76 | pre .tex .command, 77 | pre .prompt { 78 | color: #A6E22E; 79 | } 80 | 81 | pre .comment, 82 | pre .java .annotation, 83 | pre .smartquote, 84 | pre .blockquote, 85 | pre .horizontal_rule, 86 | pre .python .decorator, 87 | pre .template_comment, 88 | pre .pi, 89 | pre .doctype, 90 | pre .deletion, 91 | pre .shebang, 92 | pre .apache .sqbracket, 93 | pre .tex .formula { 94 | color: #75715E; 95 | } 96 | 97 | pre .keyword, 98 | pre .literal, 99 | pre .css .id, 100 | pre .phpdoc, 101 | pre .title, 102 | pre .header, 103 | pre .haskell .type, 104 | pre .vbscript .built_in, 105 | pre .sql .aggregate, 106 | pre .rsl .built_in, 107 | pre .smalltalk .class, 108 | pre .diff .header, 109 | pre .chunk, 110 | pre .winutils, 111 | pre .bash .variable, 112 | pre .apache .tag, 113 | pre .tex .special, 114 | pre .request, 115 | pre .status { 116 | font-weight: bold; 117 | } 118 | 119 | pre .coffeescript .javascript, 120 | pre .javascript .xml, 121 | pre .tex .formula, 122 | pre .xml .javascript, 123 | pre .xml .vbscript, 124 | pre .xml .css, 125 | pre .xml .cdata { 126 | opacity: 0.5; 127 | } 128 | -------------------------------------------------------------------------------- /client/assets/data/images/q1_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/data/images/q1_1.jpg -------------------------------------------------------------------------------- /client/assets/data/quiz_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid" : 1, 3 | "name": "Wanna be a Front-end AngularJS Engineer", 4 | 5 | "questions" : [ 6 | { 7 | "question" : "Which is not an advantage of using a closure?", 8 | "choices" : [ 9 | "Prevent pollution of global scope", 10 | "Encapsulation", 11 | "Private properties and methods", 12 | "Allow conditional use of ‘strict mode" 13 | ], 14 | "answer" : "2" 15 | }, 16 | { 17 | "question" : "To create a columned list of two-line email subjects and dates for a master-detail view, which are the most semantically correct?", 18 | "choices" : [ 19 | "
+", 20 | "+", 21 | "
    +
  • ", 22 | "

    +
    ", 23 | "none of these", 24 | "all of these" 25 | ], 26 | "answer" : "2" 27 | }, 28 | { 29 | "question" : "To pass an array of strings to a function, you should not use...", 30 | "choices" : [ 31 | "fn.apply(this, stringsArray)", 32 | "fn.call(this, stringsArray)", 33 | "fn.bind(this, stringsArray)" 34 | ], 35 | "answer" : "2" 36 | }, 37 | { 38 | "question" : "____ and ____ would be the HTML tags you would use to display a menu item and its description.", 39 | "choices" : [ 40 | "

  • + ", 41 | "
    + ", 42 | " + " 43 | ], 44 | "answer" : "0" 45 | }, 46 | { 47 | "question" : "Given
    , which of these two is the most performant way to select the inner div ?", 48 | "choices" : [ 49 | "getElementById('outer').children[0]", 50 | "getElementsByClassName('inner')[0]" 51 | ], 52 | "answer" : "0" 53 | }, 54 | { 55 | "question" : "Given this:", 56 | "details" : "

    Which message will be returned by injecting this service and executing 'myService.getMessage()'?

    ", 57 | "choices" : [ 58 | "Message one!", 59 | "Message two!", 60 | "Message three!" 61 | ], 62 | "answer" : "2" 63 | }, 64 | { 65 | "question" : "When using Jasmine for TDD, what would you use to determine if an event handler was called:", 66 | "choices" : [ 67 | "spyOn(object, 'eventHandlerMethodName')!", 68 | "stub(object, 'eventHandlerMethodName')!", 69 | "mock(object, 'eventHandlerMethodName')!", 70 | "all three", 71 | "none of these" 72 | ], 73 | "answer" : "0" 74 | }, 75 | { 76 | "question" : "In AngularJS, when do you need to use $scope.apply()? Which is the best answer:", 77 | "choices" : [ 78 | "Anytime you want to trigger databinding changes to update the UI.", 79 | "Only after $http remote service calls", 80 | "For any model changes during event callback/notifications", 81 | "For async callbacks outside AngularJS context", 82 | "all of these", 83 | "none of these" 84 | ], 85 | "answer" : "3" 86 | }, 87 | { 88 | "question" : "In AngularJS, where should DOM manipulation occur ?", 89 | "choices" : [ 90 | "Only in Controllers", 91 | "Only in Directives", 92 | "In Both Controllers and Directives", 93 | "Only in async callback code", 94 | "all of these", 95 | "none of these" 96 | ], 97 | "answer" : "1" 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /client/assets/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/font/FontAwesome.otf -------------------------------------------------------------------------------- /client/assets/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/assets/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/assets/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/assets/images/bkgrnd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/images/bkgrnd.jpg -------------------------------------------------------------------------------- /client/assets/images/body_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/images/body_bg.gif -------------------------------------------------------------------------------- /client/assets/images/gray_bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/images/gray_bg.gif -------------------------------------------------------------------------------- /client/assets/images/login_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/images/login_btn.png -------------------------------------------------------------------------------- /client/assets/images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/images/shadow.png -------------------------------------------------------------------------------- /client/assets/js/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use aysnc script loader, configure the application module (for AngularJS) 3 | * and initialize the application ( which configures routing ) 4 | * 5 | * @author Thomas Burleson 6 | */ 7 | 8 | (function( head ) { 9 | "use strict"; 10 | 11 | head.js( 12 | 13 | // Pre-load these for splash-screen progress bar... 14 | 15 | { require : "./vendor/requirejs/require.js", size: "80196" }, 16 | { underscore : "./vendor/underscore/underscore.js", size: "43568" }, 17 | 18 | { angular : "./vendor/angular/angular.js", size: "551057" }, 19 | { ngRoute : "./vendor/angular-route/angular-route.js", size: "30052" }, 20 | { ngSanitize : "./vendor/angular-sanitize/angular-sanitize.js", size: "19990" } 21 | 22 | ) 23 | .ready("ALL", function() { 24 | 25 | require.config ( 26 | { 27 | appDir : '', 28 | baseUrl : './src', 29 | paths : 30 | { 31 | // Configure alias to full paths 32 | 33 | 'auth' : './quizzer/authentication', 34 | 'quiz' : './quizzer/quiz', 35 | 'utils' : './mindspace/utils' 36 | 37 | }, 38 | shim : 39 | { 40 | 'underscore': 41 | { 42 | exports : '_' 43 | } 44 | } 45 | }); 46 | 47 | 48 | require( [ "main" ], function( app ) 49 | { 50 | // Application has bootstrapped and started... 51 | }); 52 | 53 | 54 | }); 55 | 56 | 57 | 58 | }( window.head )); 59 | -------------------------------------------------------------------------------- /client/assets/less/main.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/client/assets/less/main.less -------------------------------------------------------------------------------- /client/assets/less/variables.less: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // -------------------------------------------------- 4 | 5 | 6 | // Global values 7 | // -------------------------------------------------- 8 | 9 | 10 | // Grays 11 | // ------------------------- 12 | @black: #000; 13 | @grayDarker: #222; 14 | @grayDark: #333; 15 | @gray: #777; 16 | @grayLight: #999; 17 | @grayLighter: #eee; 18 | @white: #fff; 19 | 20 | 21 | // Accent colors 22 | // ------------------------- 23 | @blue: #049cdb; 24 | @blueDark: #0064cd; 25 | @green: #46a546; 26 | @red: #9d261d; 27 | @yellow: #ffc40d; 28 | @orange: #f89406; 29 | @pink: #c3325f; 30 | @purple: #7a43b6; 31 | 32 | 33 | // Scaffolding 34 | // ------------------------- 35 | @bodyBackground: rgb(255, 255, 255); 36 | @textColor: @grayDark; 37 | 38 | 39 | // Links 40 | // ------------------------- 41 | @linkColor: #08c; 42 | @linkColorHover: darken(@linkColor, 15%); 43 | 44 | 45 | // Typography 46 | // ------------------------- 47 | @sansFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif; 48 | @serifFontFamily: Georgia, "Times New Roman", Times, serif; 49 | @monoFontFamily: Monaco, Menlo, Consolas, "Courier New", monospace; 50 | 51 | @baseFontSize: 14px; 52 | @baseFontFamily: @sansFontFamily; 53 | @baseLineHeight: 20px; 54 | @altFontFamily: @serifFontFamily; 55 | 56 | @headingsFontFamily: inherit; 57 | @headingsFontWeight: bold; 58 | @headingsColor: rgb(153, 0, 0); 59 | 60 | 61 | // Component sizing 62 | // ------------------------- 63 | // Based on 14px font-size and 20px line-height 64 | 65 | @fontSizeLarge: @baseFontSize * 1.25; 66 | @fontSizeSmall: @baseFontSize * 0.85; 67 | @fontSizeMini: @baseFontSize * 0.75; 68 | 69 | @paddingLarge: 11px 19px; 70 | @paddingSmall: 2px 10px; 71 | @paddingMini: 0 6px; 72 | 73 | @baseBorderRadius: 4px; 74 | @borderRadiusLarge: 6px; 75 | @borderRadiusSmall: 3px; 76 | 77 | 78 | // Tables 79 | // ------------------------- 80 | @tableBackground: transparent; 81 | @tableBackgroundAccent: #f9f9f9; 82 | @tableBackgroundHover: #f5f5f5; 83 | @tableBorder: #ddd; 84 | 85 | // Buttons 86 | // ------------------------- 87 | @btnBackground: rgb(255, 204, 102); 88 | @btnBackgroundHighlight: #ab6c32; 89 | @btnBorder: #ccc; 90 | 91 | @btnPrimaryBackground: rgb(255, 0, 0); 92 | @btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 20%); 93 | 94 | @btnInfoBackground: #5bc0de; 95 | @btnInfoBackgroundHighlight: #2f96b4; 96 | 97 | @btnSuccessBackground: #62c462; 98 | @btnSuccessBackgroundHighlight: #51a351; 99 | 100 | @btnWarningBackground: lighten(@orange, 15%); 101 | @btnWarningBackgroundHighlight: @orange; 102 | 103 | @btnDangerBackground: #ee5f5b; 104 | @btnDangerBackgroundHighlight: #bd362f; 105 | 106 | @btnInverseBackground: #444; 107 | @btnInverseBackgroundHighlight: @grayDarker; 108 | 109 | 110 | // Forms 111 | // ------------------------- 112 | @inputBackground: @white; 113 | @inputBorder: #ccc; 114 | @inputBorderRadius: @baseBorderRadius; 115 | @inputDisabledBackground: @grayLighter; 116 | @formActionsBackground: #f5f5f5; 117 | @inputHeight: @baseLineHeight + 10px; 118 | 119 | 120 | // Dropdowns 121 | // ------------------------- 122 | @dropdownBackground: @white; 123 | @dropdownBorder: rgba(0,0,0,.2); 124 | @dropdownDividerTop: #e5e5e5; 125 | @dropdownDividerBottom: @white; 126 | 127 | @dropdownLinkColor: @grayDark; 128 | @dropdownLinkColorHover: @white; 129 | @dropdownLinkColorActive: @white; 130 | 131 | @dropdownLinkBackgroundActive: @linkColor; 132 | @dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive; 133 | 134 | 135 | 136 | // COMPONENT VARIABLES 137 | // -------------------------------------------------- 138 | 139 | 140 | // Z-index master list 141 | // ------------------------- 142 | // Used for a bird's eye view of components dependent on the z-axis 143 | // Try to avoid customizing these :) 144 | @zindexDropdown: 1000; 145 | @zindexPopover: 1010; 146 | @zindexTooltip: 1030; 147 | @zindexFixedNavbar: 1030; 148 | @zindexModalBackdrop: 1040; 149 | @zindexModal: 1050; 150 | 151 | 152 | // Sprite icons path 153 | // ------------------------- 154 | @iconSpritePath: "../img/glyphicons-halflings.png"; 155 | @iconWhiteSpritePath: "../img/glyphicons-halflings-white.png"; 156 | 157 | 158 | // Input placeholder text color 159 | // ------------------------- 160 | @placeholderText: @grayLight; 161 | 162 | 163 | // Hr border color 164 | // ------------------------- 165 | @hrBorder: @grayLighter; 166 | 167 | 168 | // Horizontal forms & lists 169 | // ------------------------- 170 | @horizontalComponentOffset: 180px; 171 | 172 | 173 | // Wells 174 | // ------------------------- 175 | @wellBackground: #f5f5f5; 176 | 177 | 178 | // Navbar 179 | // ------------------------- 180 | @navbarCollapseWidth: 979px; 181 | @navbarCollapseDesktopWidth: @navbarCollapseWidth + 1; 182 | 183 | @navbarHeight: 40px; 184 | @navbarBackgroundHighlight: rgb(153, 0, 0); 185 | @navbarBackground: darken(@navbarBackgroundHighlight, 5%); 186 | @navbarBorder: darken(@navbarBackground, 12%); 187 | 188 | @navbarText: #777; 189 | @navbarLinkColor: #777; 190 | @navbarLinkColorHover: @grayDark; 191 | @navbarLinkColorActive: @gray; 192 | @navbarLinkBackgroundHover: transparent; 193 | @navbarLinkBackgroundActive: darken(@navbarBackground, 5%); 194 | 195 | @navbarBrandColor: @navbarLinkColor; 196 | 197 | // Inverted navbar 198 | @navbarInverseBackground: #111111; 199 | @navbarInverseBackgroundHighlight: #222222; 200 | @navbarInverseBorder: #252525; 201 | 202 | @navbarInverseText: @grayLight; 203 | @navbarInverseLinkColor: @grayLight; 204 | @navbarInverseLinkColorHover: @white; 205 | @navbarInverseLinkColorActive: @navbarInverseLinkColorHover; 206 | @navbarInverseLinkBackgroundHover: transparent; 207 | @navbarInverseLinkBackgroundActive: @navbarInverseBackground; 208 | 209 | @navbarInverseSearchBackground: lighten(@navbarInverseBackground, 25%); 210 | @navbarInverseSearchBackgroundFocus: @white; 211 | @navbarInverseSearchBorder: @navbarInverseBackground; 212 | @navbarInverseSearchPlaceholderColor: #ccc; 213 | 214 | @navbarInverseBrandColor: @navbarInverseLinkColor; 215 | 216 | 217 | // Pagination 218 | // ------------------------- 219 | @paginationBackground: #fff; 220 | @paginationBorder: #ddd; 221 | @paginationActiveBackground: #f5f5f5; 222 | 223 | 224 | // Hero unit 225 | // ------------------------- 226 | @heroUnitBackground: rgb(255, 204, 102); 227 | @heroUnitHeadingColor: rgb(153, 0, 0); 228 | @heroUnitLeadColor: inherit; 229 | 230 | 231 | // Form states and alerts 232 | // ------------------------- 233 | @warningText: #c09853; 234 | @warningBackground: #fcf8e3; 235 | @warningBorder: darken(spin(@warningBackground, -10), 3%); 236 | 237 | @errorText: #b94a48; 238 | @errorBackground: #f2dede; 239 | @errorBorder: darken(spin(@errorBackground, -10), 3%); 240 | 241 | @successText: #468847; 242 | @successBackground: #dff0d8; 243 | @successBorder: darken(spin(@successBackground, -10), 5%); 244 | 245 | @infoText: #3a87ad; 246 | @infoBackground: #d9edf7; 247 | @infoBorder: darken(spin(@infoBackground, -10), 7%); 248 | 249 | 250 | // Tooltips and popovers 251 | // ------------------------- 252 | @tooltipColor: #fff; 253 | @tooltipBackground: #000; 254 | @tooltipArrowWidth: 5px; 255 | @tooltipArrowColor: @tooltipBackground; 256 | 257 | @popoverBackground: #fff; 258 | @popoverArrowWidth: 10px; 259 | @popoverArrowColor: #fff; 260 | @popoverTitleBackground: darken(@popoverBackground, 3%); 261 | 262 | // Special enhancement for popovers 263 | @popoverArrowOuterWidth: @popoverArrowWidth + 1; 264 | @popoverArrowOuterColor: rgba(0,0,0,.25); 265 | 266 | 267 | 268 | // GRID 269 | // -------------------------------------------------- 270 | 271 | 272 | // Default 940px grid 273 | // ------------------------- 274 | @gridColumns: 12; 275 | @gridColumnWidth: 60px; 276 | @gridGutterWidth: 20px; 277 | @gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); 278 | 279 | // 1200px min 280 | @gridColumnWidth1200: 70px; 281 | @gridGutterWidth1200: 30px; 282 | @gridRowWidth1200: (@gridColumns * @gridColumnWidth1200) + (@gridGutterWidth1200 * (@gridColumns - 1)); 283 | 284 | // 768px-979px 285 | @gridColumnWidth768: 42px; 286 | @gridGutterWidth768: 20px; 287 | @gridRowWidth768: (@gridColumns * @gridColumnWidth768) + (@gridGutterWidth768 * (@gridColumns - 1)); 288 | 289 | 290 | // Fluid grid 291 | // ------------------------- 292 | @fluidGridColumnWidth: percentage(@gridColumnWidth/@gridRowWidth); 293 | @fluidGridGutterWidth: percentage(@gridGutterWidth/@gridRowWidth); 294 | 295 | // 1200px min 296 | @fluidGridColumnWidth1200: percentage(@gridColumnWidth1200/@gridRowWidth1200); 297 | @fluidGridGutterWidth1200: percentage(@gridGutterWidth1200/@gridRowWidth1200); 298 | 299 | // 768px-979px 300 | @fluidGridColumnWidth768: percentage(@gridColumnWidth768/@gridRowWidth768); 301 | @fluidGridGutterWidth768: percentage(@gridGutterWidth768/@gridRowWidth768); 302 | -------------------------------------------------------------------------------- /client/assets/views/login.tpl.html: -------------------------------------------------------------------------------- 1 |
    73 | 74 | 75 | -------------------------------------------------------------------------------- /client/assets/views/quiz.tpl.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 |
    5 |
    6 |

    Quiz:   '{{ quizName }}'

    7 |
    8 | 9 |
    10 | 11 |
    12 | 13 |
    14 |

    {{ challenge.index }}.)   {{ challenge.question }}

    15 | 16 |
    17 |
    18 | 19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 | 26 |
    27 | 28 |
    29 | 30 |
    31 | 32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/assets/views/score.tpl.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 |
    5 |

    Quiz Scoring results:   '{{ title }}'

    6 |
    7 | 8 |
    9 | 10 | 11 |
    12 |

    13 | Your quiz score is {{ grade }}% correct. 14 |

    15 |
    16 | 17 |

    18 | Below is a review of your answers to the questions in this quiz: 19 |

    20 | 21 |
    22 | 23 |
    24 |

    25 | {{ score.index }}. {{ score.title }} 26 |

    27 | 28 |
    29 | 30 |
    31 | 32 | 33 | 34 |
    35 | 36 |
    37 | 38 |

    Correct:

    39 |

    {{ score.expected }}

    40 | 41 |
    42 | 43 |
    44 | 45 |
    46 | 47 |
    48 | 49 |

    You answered incorrectly:

    50 |

    {{ score.answered }}

    51 | 52 |
    53 | 54 |
    55 | 56 |

    Correct answer was:

    57 |

    {{ score.expected }}

    58 | 59 |
    60 | 61 |
    62 | 63 |
    64 | 65 |
    66 | {{ email }} 67 |
    68 | 69 |
    70 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quizzler Online Testing 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Now let's start our AngularJS app... 3 | * which uses RequireJS to load packages and code 4 | * 5 | */ 6 | (function ( define ) { 7 | "use strict"; 8 | 9 | define([ 10 | 'utils/logger/ExternalLogger', 11 | 'utils/logger/LogDecorator', 12 | 'auth/AuthenticateModule', 13 | 'quiz/RouteManager', 14 | 'quiz/QuizModule' 15 | ], 16 | function ( $log, LogDecorator, AuthenticateModule, RouteManager, QuizModule ) 17 | { 18 | /** 19 | * Specify main application dependencies... 20 | * one of which is the Authentication module. 21 | * 22 | * @type {Array} 23 | */ 24 | var app, appName = 'quizzer.OnlineTest'; 25 | 26 | $log = $log.getInstance( "BOOTSTRAP" ); 27 | $log.debug( "Initializing {0}", [ appName ] ); 28 | 29 | /** 30 | * Start the main application 31 | * 32 | * We manually start this bootstrap process; since ng:app is gone 33 | * ( necessary to allow Loader splash pre-AngularJS activity to finish properly ) 34 | */ 35 | 36 | app = angular 37 | .module( 38 | appName, 39 | [ "ngRoute", "ngSanitize", AuthenticateModule, QuizModule ] 40 | ) 41 | .config( LogDecorator ) 42 | .config( RouteManager ); 43 | 44 | angular.bootstrap( document.getElementsByTagName("body")[0], [ appName ]); 45 | 46 | return app; 47 | } 48 | ); 49 | 50 | }( define )); 51 | -------------------------------------------------------------------------------- /client/src/mindspace/directives/Gravatar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Gravatar AngularJS Directive used to display Gravatar image based on specified email. 4 | * Uses RequireJS and the md5 cipher in the Crypto module 5 | * 6 | * Usages: 7 | * 8 | * 9 | * which injects into the DOM: 10 | * ' 11 | * 12 | * Configuration: 13 | * 14 | * angular.module( 'myApp') 15 | * .directive( 'gravatar', Gravatar ); 16 | * 17 | * 18 | * @author Thomas Burleson 19 | * @copyright Mindspace, LLC 20 | * 21 | * @see utils.md5 22 | * 23 | */ 24 | (function( define, angular ) { 25 | "use strict"; 26 | 27 | /** 28 | * Register the Gravatar construction function with RequireJS 29 | * 30 | */ 31 | define( [ 'mindspace/utils/crypto/md5' ], function ( md5 ) 32 | { 33 | /** 34 | * Construction function 35 | * Does not need any AngularJS DI 36 | * 37 | * @constructor 38 | */ 39 | var Gravatar = function( ) { 40 | 41 | var scope = null, 42 | /** 43 | * Iterate the `scope.options` list 44 | * to build a query string for the Gravatar img tag... 45 | */ 46 | generateParams = function () 47 | { 48 | var options = []; 49 | scope.params = ''; 50 | angular.forEach(scope.options, function(value, key) { 51 | if ( value ) { 52 | options.push(key + '=' + encodeURIComponent(value)); 53 | } 54 | }); 55 | if ( options.length > 0 ) { 56 | scope.params = '?' + options.join('&'); 57 | } 58 | }, 59 | /** 60 | * EventHandler for `email` attribute changes 61 | * @param email 62 | */ 63 | onEmailChange = function( email ) 64 | { 65 | if ( email ) { 66 | // Encrypt email using md5 cipher 67 | scope.hash = md5( email.trim().toLowerCase() ); 68 | } 69 | }, 70 | /** 71 | * EventHandler for `size` attribute changes 72 | * @param size 73 | */ 74 | onSizeChange = function( size ) 75 | { 76 | scope.options.s = (angular.isNumber(size)) ? size : undefined; 77 | generateParams(); 78 | }, 79 | /** 80 | * EventHandler for `forceDefault` attribute changes 81 | * @param forceDefault 82 | */ 83 | onForceDefault = function( forceDefault ) 84 | { 85 | scope.options.f = forceDefault ? 'y' : undefined; 86 | generateParams(); 87 | }, 88 | /** 89 | * EventHandler for `defaultImage` attribute changes 90 | * @param defaultImage 91 | */ 92 | onImageChanged = function( defaultImage ) 93 | { 94 | scope.options.d = defaultImage ? defaultImage : undefined; 95 | generateParams(); 96 | }; 97 | 98 | // Return configured, directive instance 99 | 100 | return { 101 | restrict : 'E', 102 | replace : true, 103 | scope : { 104 | email : '=', 105 | size : '=', 106 | defaultImage: '=', 107 | forceDefault: '=' 108 | }, 109 | link: function($scope, element, attrs) 110 | { 111 | scope = $scope; 112 | scope.options = { 113 | s : undefined, // size 114 | f : undefined, // forceDefault 115 | d : undefined // default image 116 | }; 117 | scope.params = ''; // query params of options 118 | scope.hash = ''; // md5 of email 119 | 120 | scope.$watch( 'email', onEmailChange ); 121 | scope.$watch( 'size', onSizeChange ); 122 | scope.$watch( 'forceDefault', onForceDefault ); 123 | scope.$watch( 'defaultImage', onImageChanged ); 124 | 125 | }, 126 | template : '' 127 | }; 128 | }; 129 | 130 | // Publish the Gravatar directive construction function 131 | 132 | return Gravatar; 133 | 134 | }); 135 | 136 | })( define, angular ); 137 | -------------------------------------------------------------------------------- /client/src/mindspace/interceptors/ResponseInterceptor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date September 15, 2013 4 | * @copyright 2013 Mindspace, LLC 5 | * 6 | * @description 7 | * Establishes global response interceptors and potential routing based on DataService responses 8 | * 9 | */ 10 | (function () 11 | { 12 | "use strict"; 13 | 14 | define(function () 15 | { 16 | var errorModel = null, 17 | logger = null, 18 | $scope = null, 19 | $$q = null; 20 | 21 | /** 22 | * Constructor function for the ResponseInterceptor. 23 | */ 24 | var ResponseInterceptor = function ($httpProvider) 25 | { 26 | /** 27 | * Only RESPONSE interceptors are implemented below. 28 | * These interceptors receive and return promises. 29 | * NOTE: We have not yet implemented REQUEST interceptors/transforms ! 30 | */ 31 | var globalResponseInterceptor = function (promise) 32 | { 33 | var onSuccess = function (packet) 34 | { 35 | // Routing by-pass 36 | // !! ngRoutes will load templateUrls as strings... 37 | 38 | if(angular.isString(packet.data)) 39 | { 40 | return packet; 41 | } 42 | 43 | logger.debug("onSuccess()"); 44 | 45 | // Here we can check status codes, etc. 46 | // and then extract the `true` data body 47 | 48 | return packet; 49 | 50 | }, 51 | /** 52 | * FaultHandler 53 | */ 54 | onFault = function (fault) 55 | { 56 | logger.debug("onFault({status})", fault); 57 | 58 | var error = angular.isDefined(fault.error) ? fault.error : 59 | angular.isDefined(fault.status) ? 60 | { 61 | code: fault.status, 62 | message: "Unexpected Server Error" 63 | } : 64 | { 65 | code: "404", 66 | message: "Not Found" 67 | }; 68 | 69 | // Extract error and `report` via updates to the `errorModel` 70 | return $$q.reject(error); 71 | }; 72 | 73 | return promise.then(onSuccess, onFault); 74 | }, 75 | /** 76 | * Capture the injected instances and return our global ResponseInterceptor 77 | * @returns {Function} 78 | */ 79 | registerInterceptor = function (session, $rootScope, $q, $log) 80 | { 81 | // Save references; required for interceptor features 82 | 83 | $$q = $q; 84 | logger = $log.getInstance("ResponseInterceptor"); 85 | $scope = $rootScope; 86 | errorModel = session.error; 87 | 88 | return globalResponseInterceptor; 89 | }; 90 | 91 | 92 | /** 93 | * Register global HTTP response interceptor 94 | */ 95 | $httpProvider.responseInterceptors.push( 96 | ["session", "$rootScope", "$q", "$log", registerInterceptor ] 97 | ); 98 | 99 | }; 100 | 101 | return ["$httpProvider", ResponseInterceptor]; 102 | }); 103 | 104 | }()); 105 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/BrowserDetect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * 5 | * @description 6 | * Browser detect script from `QuirksMode` 7 | * 8 | */ 9 | (function (navigator) 10 | { 11 | "use strict"; 12 | 13 | define([], function () 14 | { 15 | var BrowserDetect = { 16 | 17 | /** 18 | * Sets the browser version and OS(Operating Systems) uses {@link mindspace.utils:BrowserDetect#searchString searchString} 19 | * and {@link mindspace.utils:BrowserDetect#searchVersion searchVersion} internally 20 | */ 21 | init: function () 22 | { 23 | this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; 24 | this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || 25 | "an unknown version"; 26 | this.OS = this.searchString(this.dataOS) || "an unknown OS"; 27 | 28 | return BrowserDetect; 29 | }, 30 | 31 | /** 32 | * Checks whether the browser is IE8. Root element(html) is already set with class='ie8 33 | * this function uses the same class reference and provides the status. 34 | */ 35 | isIE8: function () 36 | { 37 | if(document.documentElement.hasAttribute("class") && document.documentElement.getAttribute("class") === "ie8") 38 | { 39 | return true; 40 | } 41 | return false; 42 | }, 43 | 44 | /** 45 | * User for determining the browser and OS based on the input provided by the data param. 46 | * Also sets the versionSearchString parameter which would be used by 47 | * {@link mindspace.utils:BrowserDetect#searchVersion searchVersion} 48 | */ 49 | searchString: function (data) 50 | { 51 | for(var i = 0; i < data.length; i++) 52 | { 53 | var dataString = data[i].string; 54 | var dataProp = data[i].prop; 55 | 56 | this.versionSearchString = data[i].versionSearch || data[i].identity; 57 | if(dataString) 58 | { 59 | if(dataString.indexOf(data[i].subString) != -1) 60 | { 61 | return data[i].identity; 62 | } 63 | } 64 | else if(dataProp) 65 | { 66 | return data[i].identity; 67 | } 68 | } 69 | }, 70 | 71 | /** 72 | * User for determining the browser version based on input string 73 | */ 74 | searchVersion: function (dataString) 75 | { 76 | var index = dataString.indexOf(this.versionSearchString); 77 | if(index == -1) 78 | { 79 | return; 80 | } 81 | return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); 82 | }, 83 | 84 | // NOTE: It's important to list PhantomJS first since it has the same browser information as Safari 85 | dataBrowser: [ 86 | { 87 | string: "PhantomJS", 88 | subString: "PhantomJS", 89 | identity: "PhantomJS", 90 | versionSearch: "PhantomJS" 91 | }, 92 | { 93 | string: navigator.userAgent, 94 | subString: "Chrome", 95 | identity: "Chrome" 96 | }, 97 | { 98 | string: navigator.userAgent, 99 | subString: "OmniWeb", 100 | versionSearch: "OmniWeb/", 101 | identity: "OmniWeb" 102 | }, 103 | { 104 | string: navigator.vendor, 105 | subString: "Apple", 106 | identity: "Safari", 107 | versionSearch: "Version" 108 | }, 109 | { 110 | prop: window.opera, 111 | identity: "Opera", 112 | versionSearch: "Version" 113 | }, 114 | { 115 | string: navigator.vendor, 116 | subString: "iCab", 117 | identity: "iCab" 118 | }, 119 | { 120 | string: navigator.vendor, 121 | subString: "KDE", 122 | identity: "Konqueror" 123 | }, 124 | { 125 | string: navigator.userAgent, 126 | subString: "Firefox", 127 | identity: "Firefox" 128 | }, 129 | { 130 | string: navigator.vendor, 131 | subString: "Camino", 132 | identity: "Camino" 133 | }, 134 | { // for newer Netscapes (6+) 135 | string: navigator.userAgent, 136 | subString: "Netscape", 137 | identity: "Netscape" 138 | }, 139 | { 140 | string: navigator.userAgent, 141 | subString: "MSIE", 142 | identity: "Explorer", 143 | versionSearch: "MSIE" 144 | }, 145 | { 146 | string: navigator.userAgent, 147 | subString: "Gecko", 148 | identity: "Mozilla", 149 | versionSearch: "rv" 150 | }, 151 | { 152 | // for older Netscapes (4-) 153 | string: navigator.userAgent, 154 | subString: "Mozilla", 155 | identity: "Netscape", 156 | versionSearch: "Mozilla" 157 | }], 158 | dataOS: [ 159 | { 160 | string: navigator.platform, 161 | subString: "Win", 162 | identity: "Windows" 163 | }, 164 | { 165 | string: navigator.platform, 166 | subString: "Mac", 167 | identity: "Mac" 168 | }, 169 | { 170 | string: navigator.userAgent, 171 | subString: "iPhone", 172 | identity: "iPhone/iPod" 173 | }, 174 | { 175 | string: navigator.platform, 176 | subString: "Linux", 177 | identity: "Linux" 178 | }] 179 | 180 | }; 181 | 182 | return BrowserDetect.init(); 183 | }); 184 | 185 | })(window.navigator); 186 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/DateTime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @author StackOverflow - Harto, http://stackoverflow.com/questions/2315408/how-do-i-format-a-timestamp-in-javascript-to-display-it-in-graphs-utc-is-fine 4 | * @description 5 | * 6 | * DateTime utility class that spits out UTC timestamp strings usually used in a reporting, print-capable process. 7 | */ 8 | (function () 9 | { 10 | "use strict"; 11 | 12 | /** 13 | * Register the class with RequireJS. 14 | */ 15 | define([], function () 16 | { 17 | /** 18 | * Creates a date timestamp string. 19 | */ 20 | var buildTimeString = function (date, format) 21 | { 22 | format = format || "%h:%m:%s:%z"; 23 | 24 | function pad(value, isMilliSeconds) 25 | { 26 | if(typeof (isMilliSeconds) === "undefined") 27 | { 28 | isMilliSeconds = false; 29 | } 30 | if(isMilliSeconds) 31 | { 32 | if(value < 10) 33 | { 34 | value = "00" + value; 35 | } 36 | else if(value < 100) 37 | { 38 | value = "0" + value; 39 | } 40 | } 41 | return(value.toString().length < 2) ? "0" + value : value; 42 | } 43 | 44 | return format.replace(/%([a-zA-Z])/g, function (_, fmtCode) 45 | { 46 | switch(fmtCode) 47 | { 48 | case "Y": 49 | return date.getFullYear(); 50 | case "M": 51 | return pad(date.getMonth() + 1); 52 | case "d": 53 | return pad(date.getDate()); 54 | case "h": 55 | return pad(date.getHours()); 56 | case "m": 57 | return pad(date.getMinutes()); 58 | case "s": 59 | return pad(date.getSeconds()); 60 | case "z": 61 | return pad(date.getMilliseconds(), true); 62 | default: 63 | throw new Error("Unsupported format code: " + fmtCode); 64 | } 65 | }); 66 | }; 67 | 68 | // Publish API for DateTime utils 69 | return { 70 | formattedNow: function () 71 | { 72 | return buildTimeString(new Date()); 73 | } 74 | }; 75 | 76 | }); 77 | 78 | })(); 79 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/Factory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * @description 5 | * 6 | * Factory.instanceOf( ) 7 | * 8 | * Typically developers allow AngularJS to construct instances with injected parameters. 9 | * Some scenarios however force developers to manually create instances from the object registered with 10 | * RequireJS define()... these objects are `Construction Arrays`. 11 | * 12 | * This utility function is useful to extract the constructor function from the `array` 13 | * and quickly build an instance with any specified arguments. 14 | * 15 | */ 16 | (function () 17 | { 18 | "use strict"; 19 | 20 | define(function () 21 | { 22 | /** 23 | * Internal util to slice arguments into a list of dependent modules 24 | */ 25 | 26 | function sliceArgs(args, startIndex) 27 | { 28 | return [].slice.call(args, startIndex || 0); 29 | } 30 | 31 | /** 32 | * Find the construction function in the array (last element) 33 | */ 34 | 35 | function extractFrom(target) 36 | { 37 | if(angular.isArray(target)) 38 | { 39 | target = target[target.length - 1]; 40 | } 41 | return target; 42 | } 43 | 44 | /** 45 | * Extract to instantiate with a constructor function or array; may 46 | * also specify optional arguments to be passed to the constructor function. 47 | */ 48 | var createInstanceOf = function () 49 | { 50 | var params = sliceArgs(arguments), 51 | Constructor = extractFrom(params.shift()); 52 | 53 | if(angular.isFunction(Constructor)) 54 | { 55 | return Constructor.length > 0 ? Constructor.apply(undefined, params) : new Constructor(); 56 | 57 | } 58 | else 59 | { 60 | 61 | throw new Error("Specified target is not a constructor function or constructor array"); 62 | } 63 | 64 | }; 65 | 66 | // Publish this `construction-function` extractor 67 | 68 | return { 69 | instanceOf: createInstanceOf 70 | }; 71 | 72 | }); 73 | 74 | }()); 75 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/createGuid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ****************************************************************************************************** 3 | * 4 | * createGuid() 5 | * 6 | * Utility function to create dynamic UUIDs 7 | * 8 | * @author Thomas Burleson 9 | * @date December, 2013 10 | * 11 | * 12 | * ****************************************************************************************************** 13 | */ 14 | (function () 15 | { 16 | "use strict"; 17 | 18 | define(function () 19 | { 20 | function createGuid() 21 | { 22 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) 23 | { 24 | var r = Math.random() * 16 | 0, 25 | v = c === 'x' ? r : (r & 0x3 | 0x8); 26 | return v.toString(16); 27 | }); 28 | } 29 | 30 | return createGuid; 31 | }); 32 | 33 | }()); 34 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/crypto/md5.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cryptographic library - md5 cipher 3 | * 4 | * @author Thomas Burleson 5 | * 6 | */ 7 | (function( define ) { 8 | "use strict"; 9 | 10 | define( function ( ) 11 | { 12 | 13 | function md5cycle(x, k) { 14 | var a = x[0], 15 | b = x[1], 16 | c = x[2], 17 | d = x[3]; 18 | 19 | a = ff(a, b, c, d, k[0], 7, -680876936); 20 | d = ff(d, a, b, c, k[1], 12, -389564586); 21 | c = ff(c, d, a, b, k[2], 17, 606105819); 22 | b = ff(b, c, d, a, k[3], 22, -1044525330); 23 | a = ff(a, b, c, d, k[4], 7, -176418897); 24 | d = ff(d, a, b, c, k[5], 12, 1200080426); 25 | c = ff(c, d, a, b, k[6], 17, -1473231341); 26 | b = ff(b, c, d, a, k[7], 22, -45705983); 27 | a = ff(a, b, c, d, k[8], 7, 1770035416); 28 | d = ff(d, a, b, c, k[9], 12, -1958414417); 29 | c = ff(c, d, a, b, k[10], 17, -42063); 30 | b = ff(b, c, d, a, k[11], 22, -1990404162); 31 | a = ff(a, b, c, d, k[12], 7, 1804603682); 32 | d = ff(d, a, b, c, k[13], 12, -40341101); 33 | c = ff(c, d, a, b, k[14], 17, -1502002290); 34 | b = ff(b, c, d, a, k[15], 22, 1236535329); 35 | 36 | a = gg(a, b, c, d, k[1], 5, -165796510); 37 | d = gg(d, a, b, c, k[6], 9, -1069501632); 38 | c = gg(c, d, a, b, k[11], 14, 643717713); 39 | b = gg(b, c, d, a, k[0], 20, -373897302); 40 | a = gg(a, b, c, d, k[5], 5, -701558691); 41 | d = gg(d, a, b, c, k[10], 9, 38016083); 42 | c = gg(c, d, a, b, k[15], 14, -660478335); 43 | b = gg(b, c, d, a, k[4], 20, -405537848); 44 | a = gg(a, b, c, d, k[9], 5, 568446438); 45 | d = gg(d, a, b, c, k[14], 9, -1019803690); 46 | c = gg(c, d, a, b, k[3], 14, -187363961); 47 | b = gg(b, c, d, a, k[8], 20, 1163531501); 48 | a = gg(a, b, c, d, k[13], 5, -1444681467); 49 | d = gg(d, a, b, c, k[2], 9, -51403784); 50 | c = gg(c, d, a, b, k[7], 14, 1735328473); 51 | b = gg(b, c, d, a, k[12], 20, -1926607734); 52 | 53 | a = hh(a, b, c, d, k[5], 4, -378558); 54 | d = hh(d, a, b, c, k[8], 11, -2022574463); 55 | c = hh(c, d, a, b, k[11], 16, 1839030562); 56 | b = hh(b, c, d, a, k[14], 23, -35309556); 57 | a = hh(a, b, c, d, k[1], 4, -1530992060); 58 | d = hh(d, a, b, c, k[4], 11, 1272893353); 59 | c = hh(c, d, a, b, k[7], 16, -155497632); 60 | b = hh(b, c, d, a, k[10], 23, -1094730640); 61 | a = hh(a, b, c, d, k[13], 4, 681279174); 62 | d = hh(d, a, b, c, k[0], 11, -358537222); 63 | c = hh(c, d, a, b, k[3], 16, -722521979); 64 | b = hh(b, c, d, a, k[6], 23, 76029189); 65 | a = hh(a, b, c, d, k[9], 4, -640364487); 66 | d = hh(d, a, b, c, k[12], 11, -421815835); 67 | c = hh(c, d, a, b, k[15], 16, 530742520); 68 | b = hh(b, c, d, a, k[2], 23, -995338651); 69 | 70 | a = ii(a, b, c, d, k[0], 6, -198630844); 71 | d = ii(d, a, b, c, k[7], 10, 1126891415); 72 | c = ii(c, d, a, b, k[14], 15, -1416354905); 73 | b = ii(b, c, d, a, k[5], 21, -57434055); 74 | a = ii(a, b, c, d, k[12], 6, 1700485571); 75 | d = ii(d, a, b, c, k[3], 10, -1894986606); 76 | c = ii(c, d, a, b, k[10], 15, -1051523); 77 | b = ii(b, c, d, a, k[1], 21, -2054922799); 78 | a = ii(a, b, c, d, k[8], 6, 1873313359); 79 | d = ii(d, a, b, c, k[15], 10, -30611744); 80 | c = ii(c, d, a, b, k[6], 15, -1560198380); 81 | b = ii(b, c, d, a, k[13], 21, 1309151649); 82 | a = ii(a, b, c, d, k[4], 6, -145523070); 83 | d = ii(d, a, b, c, k[11], 10, -1120210379); 84 | c = ii(c, d, a, b, k[2], 15, 718787259); 85 | b = ii(b, c, d, a, k[9], 21, -343485551); 86 | 87 | x[0] = add32(a, x[0]); 88 | x[1] = add32(b, x[1]); 89 | x[2] = add32(c, x[2]); 90 | x[3] = add32(d, x[3]); 91 | 92 | } 93 | 94 | function cmn(q, a, b, x, s, t) { 95 | a = add32(add32(a, q), add32(x, t)); 96 | return add32((a << s) | (a >>> (32 - s)), b); 97 | } 98 | 99 | function ff(a, b, c, d, x, s, t) { 100 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 101 | } 102 | 103 | function gg(a, b, c, d, x, s, t) { 104 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 105 | } 106 | 107 | function hh(a, b, c, d, x, s, t) { 108 | return cmn(b ^ c ^ d, a, b, x, s, t); 109 | } 110 | 111 | function ii(a, b, c, d, x, s, t) { 112 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 113 | } 114 | 115 | function md51(s) { 116 | var n = s.length, 117 | state = [1732584193, -271733879, -1732584194, 271733878], 118 | i; 119 | 120 | for (i = 64; i <= s.length; i += 64) { 121 | md5cycle(state, md5blk(s.substring(i - 64, i))); 122 | } 123 | s = s.substring(i - 64); 124 | var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 125 | for (i = 0; i < s.length; i++) { 126 | tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); 127 | } 128 | tail[i >> 2] |= 0x80 << ((i % 4) << 3); 129 | if (i > 55) { 130 | md5cycle(state, tail); 131 | for (i = 0; i < 16; i++) { 132 | tail[i] = 0; 133 | } 134 | } 135 | tail[14] = n * 8; 136 | md5cycle(state, tail); 137 | return state; 138 | } 139 | 140 | /* there needs to be support for Unicode here, 141 | * unless we pretend that we can redefine the MD-5 142 | * algorithm for multi-byte characters (perhaps 143 | * by adding every four 16-bit characters and 144 | * shortening the sum to 32 bits). Otherwise 145 | * I suggest performing MD-5 as if every character 146 | * was two bytes--e.g., 0040 0025 = @%--but then 147 | * how will an ordinary MD-5 sum be matched? 148 | * There is no way to standardize text to something 149 | * like UTF-8 before transformation; speed cost is 150 | * utterly prohibitive. The JavaScript standard 151 | * itself needs to look at this: it should start 152 | * providing access to strings as preformed UTF-8 153 | * 8-bit unsigned value arrays. 154 | */ 155 | 156 | function md5blk(s) { /* I figured global was faster. */ 157 | var md5blks = [], 158 | i; /* Andy King said do it this way. */ 159 | for (i = 0; i < 64; i += 4) { 160 | md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); 161 | } 162 | return md5blks; 163 | } 164 | 165 | var hex_chr = '0123456789abcdef'.split(''); 166 | 167 | function rhex(n) { 168 | var s = '', j = 0; 169 | for (; j < 4; j++) { 170 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; 171 | } 172 | return s; 173 | } 174 | 175 | function hex(x) { 176 | for (var i = 0; i < x.length; i++) { 177 | x[i] = rhex(x[i]); 178 | } 179 | return x.join(''); 180 | } 181 | 182 | function md5(s) { 183 | return hex(md51(s)); 184 | } 185 | 186 | /* this function is much faster, 187 | so if possible we use it. Some IEs 188 | are the only ones I know of that 189 | need the idiotic second function, 190 | generated by an if clause. */ 191 | 192 | var add32 = function(a, b) { 193 | return (a + b) & 0xFFFFFFFF; 194 | }; 195 | 196 | if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { 197 | add32 = function (x, y) { 198 | var lsw = (x & 0xFFFF) + (y & 0xFFFF), 199 | msw = (x >> 16) + (y >> 16) + (lsw >> 16); 200 | return (msw << 16) | (lsw & 0xFFFF); 201 | }; 202 | } 203 | 204 | // Publish ONLY the md5 function 205 | 206 | return function() { 207 | return { 208 | encrypt : md5 209 | }; 210 | }; 211 | }); 212 | 213 | }( define )); 214 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/logger/ExternalLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * 5 | * @description 6 | * 7 | * Uses LogEnhancer functionality to publish instance 8 | * 9 | */ 10 | (function () 11 | { 12 | "use strict"; 13 | 14 | define([ 15 | "utils/logger/LogEnhancer", 16 | "utils/BrowserDetect" 17 | ], 18 | function (LogEnhancer, BrowserDetect) 19 | { 20 | /** 21 | * Determines if the requested console logging method is available, since it is not with IE. 22 | * 23 | * @param {Function} method The request console logging method. 24 | * @returns {object} Indicates if the console logging method is available. 25 | * @private 26 | */ 27 | var prepareLogToConsole = function (method) 28 | { 29 | var console = window.console, 30 | isFunction = function (fn) 31 | { 32 | return(typeof (fn) == typeof (Function)); 33 | }, 34 | isAvailableConsoleFor = function (method) 35 | { 36 | var isPhantomJS = BrowserDetect.browser != "PhantomJS"; 37 | 38 | // NOTE: Tried using this for less logging in the console/terminal, but then logging in IDE is 39 | // wiped out as well return console && console[method] && isFunction(console[method]) && isPhantomJS; 40 | 41 | return console && console[method] && isFunction(console[method]); 42 | }, 43 | logFn = function (message) 44 | { 45 | if(isAvailableConsoleFor(method)) 46 | { 47 | try 48 | { 49 | console[method](message); 50 | 51 | } 52 | catch(e) 53 | {} 54 | } 55 | }; 56 | 57 | return logFn; 58 | }, 59 | $log = { 60 | log : prepareLogToConsole("log"), 61 | info : prepareLogToConsole("info"), 62 | warn : prepareLogToConsole("warn"), 63 | debug: prepareLogToConsole("debug"), 64 | error: prepareLogToConsole("error") 65 | }; 66 | 67 | // Publish instance of $log simulator; with enhanced functionality 68 | return new LogEnhancer($log); 69 | }); 70 | 71 | })(); 72 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/logger/LogDecorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * 5 | * @description 6 | * 7 | * Used within AngularJS to decorate/enhance the AngularJS $log service. 8 | * 9 | * 10 | */ 11 | 12 | (function () 13 | { 14 | "use strict"; 15 | 16 | /** 17 | * Register the class with RequireJS. 18 | */ 19 | define(['utils/logger/LogEnhancer'], function (enhanceLoggerFn) 20 | { 21 | /** 22 | * Decorate the $log to use inject the LogEnhancer features. 23 | * 24 | * @param {object} $provide The log console. 25 | * @returns {object} promise. 26 | * @private 27 | */ 28 | var LogDecorator = function ($provide) 29 | { 30 | // Register our $log decorator with AngularJS $provider 31 | 32 | $provide.decorator('$log', ["$delegate", 33 | function ($delegate) 34 | { 35 | // NOTE: the LogEnhancer module returns a FUNCTION that we named `enhanceLoggerFn` 36 | // All the details of how the `enchancement` works is encapsulated in LogEnhancer! 37 | 38 | enhanceLoggerFn($delegate); 39 | 40 | return $delegate; 41 | } 42 | ]); 43 | }; 44 | 45 | return [ "$provide", LogDecorator ]; 46 | }); 47 | 48 | })(); 49 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/logger/LogEnhancer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * 5 | * @description 6 | * 7 | * Used within AngularJS to enhance functionality within the AngularJS $log service. 8 | */ 9 | (function (){ 10 | "use strict"; 11 | 12 | /** 13 | * Register the class with RequireJS. 14 | */ 15 | define([ 16 | "utils/supplant", 17 | "utils/makeTryCatch", 18 | "utils/DateTime", 19 | "utils/BrowserDetect" 20 | ], 21 | function (supplant, makeTryCatch, DateTime, BrowserDetect) 22 | { 23 | /** 24 | * Constructor function 25 | */ 26 | var enhanceLogger = function ($log) 27 | { 28 | var separator = "::", 29 | 30 | /** 31 | * Capture the original $log functions; for use in enhancedLogFn() 32 | */ 33 | _$log = (function ($log) 34 | { 35 | return { 36 | log: $log.log, 37 | info: $log.info, 38 | warn: $log.warn, 39 | debug: $log.debug, 40 | error: $log.error 41 | }; 42 | })($log), 43 | 44 | /** 45 | * Chrome Dev tools supports color logging 46 | * @see https://developers.google.com/chrome-developer-tools/docs/console#styling_console_output_with_css 47 | */ 48 | colorify = function (message, colorCSS) 49 | { 50 | var isChrome = (BrowserDetect.browser == "Chrome"), 51 | canColorize = isChrome && (colorCSS !== undefined); 52 | 53 | return canColorize ? ["%c" + message, colorCSS] : [message]; 54 | }, 55 | 56 | /** 57 | * Partial application to pre-capture a logger function 58 | */ 59 | prepareLogFn = function (logFn, className, colorCSS) 60 | { 61 | /** 62 | * Invoke the specified `logFn` with the supplant functionality... 63 | */ 64 | var enhancedLogFn = function () 65 | { 66 | try 67 | { 68 | var args = Array.prototype.slice.call(arguments), 69 | now = DateTime.formattedNow(); 70 | 71 | // prepend a timestamp and optional classname to the original output message 72 | args[0] = supplant("{0} - {1}{2}", [now, className, args[0]]); 73 | args = colorify(supplant.apply(null, args), colorCSS); 74 | 75 | logFn.apply(null, args); 76 | } 77 | catch(error) 78 | { 79 | $log.error("LogEnhancer ERROR: " + error); 80 | } 81 | 82 | }; 83 | 84 | // Only needed to support angular-mocks expectations 85 | enhancedLogFn.logs = []; 86 | 87 | return enhancedLogFn; 88 | }, 89 | 90 | /** 91 | * Support to generate class-specific logger instance with classname only 92 | */ 93 | getInstance = function (className, colorCSS, customSeparator) 94 | { 95 | className = (className !== undefined) ? className + (customSeparator || separator) : ""; 96 | 97 | var instance = { 98 | log: prepareLogFn(_$log.log, className, colorCSS), 99 | info: prepareLogFn(_$log.info, className, colorCSS), 100 | warn: prepareLogFn(_$log.warn, className, colorCSS), 101 | debug: prepareLogFn(_$log.debug, className, colorCSS), 102 | error: prepareLogFn(_$log.error, className) // NO styling of ERROR messages 103 | }; 104 | 105 | if(angular.isDefined(angular.makeTryCatch)) 106 | { 107 | // Attach instance specific tryCatch() functionality... 108 | instance.tryCatch = angular.makeTryCatch(instance.error, instance); 109 | } 110 | 111 | return instance; 112 | }; 113 | 114 | // Add special method to AngularJS $log 115 | $log.getInstance = getInstance; 116 | 117 | return $log; 118 | }; 119 | 120 | return enhanceLogger; 121 | }); 122 | 123 | })(); 124 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/makeTryCatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * 5 | * Often AngularJS users need to easy capture and log exceptions (with stacktraces) for easy debugging. 6 | * When developers use Promise(s) as return values for their APIs, exceptions and rejects also need to be logged. 7 | * 8 | * `makeTryCatch()` makes it easy to log exceptions and promise rejections. This sample demonstrates the various use 9 | * cases for how `tryCatch()` can be applied to dramatically simplified error logging. 10 | * 11 | */ 12 | 13 | (function (angular) 14 | { 15 | "use strict"; 16 | 17 | define( [], function() 18 | { 19 | /** 20 | * Implement a tryCatch() method that logs exceptions for method invocations AND 21 | * promise rejection activity. 22 | * 23 | * @param notifyFn Function used to log.debug exception information 24 | * @param scope Object Receiver for the notifyFn invocation 25 | * 26 | * @return Function used to guard and invoke the targeted actionFn 27 | */ 28 | angular.makeTryCatch = function (notifyFn, scope) 29 | { 30 | /** 31 | * Report error (with stack trace if possible) to the logger function 32 | */ 33 | var reportError = function (reason) 34 | { 35 | if(notifyFn != null) 36 | { 37 | var error = (reason && reason.stack) ? reason : null, 38 | message = reason != null ? String(reason) : ""; 39 | 40 | if(error != null) 41 | { 42 | message = error.message + "\n" + error.stack; 43 | } 44 | 45 | notifyFn.apply(scope, [message]); 46 | } 47 | 48 | return reason; 49 | }, 50 | /** 51 | * Publish the tryCatch() guard 'n report function 52 | */ 53 | tryCatch = function (actionFn, scope, args) 54 | { 55 | try 56 | { 57 | // Invoke the targeted `actionFn` 58 | var result = angular.isFunction(actionFn) ? actionFn.apply(scope, args || []) : String(actionFn), 59 | promise = (angular.isObject(result) && result.then) ? result : null; 60 | 61 | if(promise != null) 62 | { 63 | // Catch and report any promise rejection reason... 64 | promise.then(null, reportError); 65 | } 66 | 67 | actionFn = null; 68 | return result; 69 | 70 | } 71 | catch(e) 72 | { 73 | actionFn = null; 74 | throw reportError(e); 75 | } 76 | 77 | }; 78 | 79 | return tryCatch; 80 | }; 81 | 82 | return angular.makeTryCatch; 83 | 84 | }); 85 | 86 | }(angular)); 87 | -------------------------------------------------------------------------------- /client/src/mindspace/utils/supplant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Thomas Burleson 3 | * @date November, 2013 4 | * @description 5 | * 6 | * String supplant global utility (similar to but more powerful than sprintf() ). 7 | * 8 | * Usages: 9 | * 10 | * var user = { 11 | * first : "Thomas", 12 | * last : "Burleson", 13 | * address : { 14 | * city : "West Des Moines", 15 | * state: "Iowa" 16 | * }, 17 | * contact : { 18 | * email : "ThomasBurleson@Gmail.com" 19 | * url : "http://www.gridlinked.info" 20 | * } 21 | * }, 22 | * message = "Hello Mr. {first} {last}. How's life in {address.city}, {address.state} ?"; 23 | * 24 | * return supplant( message, user ); 25 | * 26 | * 27 | * @author Thomas Burleson 28 | * 29 | */ 30 | (function( define ) { 31 | "use strict"; 32 | 33 | define( [], function ( ) 34 | { 35 | // supplant() method from Crockfords `Remedial Javascript` 36 | 37 | var supplant = function( template, values, pattern ) { 38 | pattern = pattern || /\{([^\{\}]*)\}/g; 39 | 40 | return template.replace(pattern, function(a, b) { 41 | var p = b.split('.'), 42 | r = values; 43 | 44 | try { 45 | for (var s in p) { r = r[p[s]]; } 46 | } catch(e){ 47 | r = a; 48 | } 49 | 50 | return (typeof r === 'string' || typeof r === 'number') ? r : a; 51 | }); 52 | }; 53 | 54 | 55 | // supplant() method from Crockfords `Remedial Javascript` 56 | Function.prototype.method = function (name, func) { 57 | this.prototype[name] = func; 58 | return this; 59 | }; 60 | 61 | String.method("supplant", function( values, pattern ) { 62 | var self = this; 63 | return supplant(self, values, pattern); 64 | }); 65 | 66 | 67 | // Publish this global function... 68 | return String.supplant = supplant; 69 | 70 | }); 71 | 72 | }( define )); 73 | -------------------------------------------------------------------------------- /client/src/quizzer/authentication/AuthenticateModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ****************************************************************************************************** 3 | * 4 | * QuizModule 5 | * 6 | * Defines controllers and services for the Authentication Module Quiz 7 | * 8 | * @author Thomas Burleson 9 | * @date December 2013 10 | * 11 | * ****************************************************************************************************** 12 | */ 13 | 14 | (function ( define, angular ) { 15 | "use strict"; 16 | 17 | define([ 18 | 'auth/Session', 19 | 'auth/Authenticator', 20 | 'auth/SessionController', 21 | 'auth/LoginController' 22 | ], 23 | function ( Session, Authenticator, SessionController, LoginController ) 24 | { 25 | var moduleName = "quizzer.Authenticate"; 26 | 27 | angular 28 | .module( moduleName, [ ] ) 29 | .service( "session", Session ) 30 | .service( "authenticator", Authenticator ) 31 | .controller( "SessionController", SessionController ) 32 | .controller( "LoginController", LoginController ); 33 | 34 | return moduleName; 35 | }); 36 | 37 | 38 | }( define, angular )); 39 | 40 | -------------------------------------------------------------------------------- /client/src/quizzer/authentication/Authenticator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ****************************************************************************************************** 3 | * 4 | * Authenticator 5 | * 6 | * Data service proxy to Authentication API that authorizes and authenticates the specified user. 7 | * 8 | * @author Thomas Burleson 9 | * @date December, 2013 10 | * 11 | * 12 | * ****************************************************************************************************** 13 | */ 14 | (function ( define ) { 15 | "use strict"; 16 | 17 | define([ 18 | 'utils/supplant', 19 | 'utils/createGuid', 20 | 'utils/crypto/md5' 21 | ], 22 | function ( supplant, createGuid, md5 ) 23 | { 24 | var Authenticator = function ( $http, $q, $log ) 25 | { 26 | $log = $log.getInstance( "Authenticator" ); 27 | 28 | /** 29 | * Util function to build a resolved promise 30 | * @returns {promise|*|promise} 31 | */ 32 | var makeResolved = function( response ) 33 | { 34 | var dfd = $q.defer(); 35 | dfd.resolve( response ); 36 | 37 | return dfd.promise; 38 | }, 39 | makeRejected = function( fault ) 40 | { 41 | var dfd = $q.defer(); 42 | dfd.reject( fault ); 43 | 44 | return dfd.promise; 45 | }, 46 | 47 | /** 48 | * Request user authentication 49 | * @return Promise 50 | */ 51 | loginUser = function( email, password ) 52 | { 53 | $log.debug( 54 | "loginUser( email={0}, password={1} )", 55 | [ email, password ] 56 | ); 57 | 58 | // Normally we have remote REST services... 59 | // return $http.post( URL.LOGIN, { email : email, password : md5.encrypt(password) } ); 60 | 61 | return ( email === "" ) ? 62 | makeRejected( "A valid email is required!" ) : 63 | makeResolved({ session : createGuid(), email : email }); 64 | }, 65 | 66 | /** 67 | * Logout user 68 | * @return Promise 69 | */ 70 | logoutUser = function() 71 | { 72 | $log.debug( "logoutUser()" ); 73 | 74 | // Normally we have remote REST services... 75 | // return $http.get( URL.LOGOUT ); 76 | 77 | return makeResolved({ 78 | session : null 79 | }); 80 | }, 81 | 82 | /** 83 | * Change user password 84 | * @return Promise 85 | */ 86 | changePassword = function( email, newPassword, password ) 87 | { 88 | 89 | $log.debug( "changePassword ( email={0}, newPassword={1}", [email, newPasword]); 90 | 91 | // return $http.post( URL.PASSWORD_CHANGE, { 92 | // userName : email, 93 | // oldPassword : md5.encrypt(password ), 94 | // newPassword : md5.encrypt(newPassword) 95 | // }); 96 | 97 | return makeResolved( { 98 | email : email, 99 | password : newPassword 100 | }); 101 | }, 102 | 103 | /** 104 | * Reset user password 105 | * @return Promise 106 | */ 107 | resetPassword = function( email, password, hint ) 108 | { 109 | $log.debug( "resetPassword( password={0}, hint={1}", [password, hint] ); 110 | 111 | // return $http.post( URL.PASSWORD_RESET, { 112 | // userName : email, 113 | // newPassword : md5.encrypt(password), 114 | // passwordHint : md5.encrypt(hint) 115 | // }); 116 | 117 | return changePassword( email, password ); 118 | }; 119 | 120 | 121 | // Publish Authentication delegate instance/object with desired API 122 | 123 | return { 124 | 125 | login : loginUser, 126 | logout : logoutUser, 127 | changePassword : changePassword, 128 | resetPassword : resetPassword 129 | 130 | }; 131 | 132 | }; 133 | 134 | return [ "$http", "$q", "$log", Authenticator ]; 135 | 136 | }); 137 | 138 | }( define )); 139 | -------------------------------------------------------------------------------- /client/src/quizzer/authentication/LoginController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This LoginController module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the LoginController class with RequireJS 15 | */ 16 | define( [ 'utils/supplant' ], function ( supplant ) 17 | { 18 | var SERVER_NOT_RESPONDING = "The Quizzler server is not responding", 19 | UNABLE_TO_CONNECT = 'Unable to connect to secure Quizzler dataservices', 20 | TIMEOUT_RESPONSE = 'Dataservice did not respond and timed out.', 21 | PAGE_NOT_FOUND = '404 Not Found', 22 | 23 | /** 24 | * Constructor function used by AngularJS to create instances of 25 | * a service, factory, or controller. 26 | * 27 | * @constructor 28 | */ 29 | LoginController = function( session, authenticator, $scope, $q, $log, $location ) 30 | { 31 | $log = $log.getInstance( "LoginController" ); 32 | $log.debug( "constructor() "); 33 | 34 | var announceNA = function() 35 | { 36 | var message = "This feature is not yet available!"; 37 | 38 | $log.error( message ); 39 | //$window.alert( message ); 40 | }, 41 | /** 42 | * Mutator or accessor to easily set the errorMessage and title 43 | */ 44 | errorMessage = function( msg ) 45 | { 46 | $scope.hasError = (msg !== ""); 47 | $scope.errorMessage = $scope.errorMessage || ''; 48 | 49 | // Allows errorMessage() to be accessor or mutator 50 | if ( !angular.isUndefined(msg ) ) { 51 | $scope.errorMessage = msg || '' ; 52 | $scope.title = UNABLE_TO_CONNECT; 53 | } 54 | 55 | return $scope.errorMessage; 56 | }, 57 | 58 | /** 59 | * Delegate login process to $authenticator and `wait` for a response 60 | * 61 | */ 62 | onLogin = function () 63 | { 64 | $log.debug( "onLogin( email={email}, password={password} )", $scope ); 65 | 66 | $log.tryCatch( function() 67 | { 68 | return authenticator 69 | .login( $scope.email, $scope.password ) 70 | .then( function onResult_login( response ) 71 | { 72 | $log.debug( "onResult_login( sessionID={session}" ,response ); 73 | 74 | session.sessionID = response.session; 75 | session.account = { 76 | userName : $scope.email, 77 | password : $scope.password, 78 | email : $scope.email 79 | }; 80 | 81 | errorMessage( "" ); 82 | 83 | // Navigate to the Quiz 84 | // TODO - uses constants file for view navigations... 85 | 86 | $location.path( '/quiz' ); 87 | 88 | return session; 89 | }, 90 | function onFault_login( fault ) 91 | { 92 | fault = fault || SERVER_NOT_RESPONDING; 93 | fault = supplant( String(fault), [ "onLogin()" ] ); 94 | 95 | $log.error( fault.toString() ); 96 | 97 | // force clear any previously valid session... 98 | session.sessionID = null; 99 | errorMessage( fault.toString() ); 100 | 101 | if ( fault == TIMEOUT_RESPONSE ) { errorMessage( SERVER_NOT_RESPONDING ); } 102 | if ( fault == PAGE_NOT_FOUND ) { errorMessage( PAGE_NOT_FOUND ); } 103 | 104 | return $q.reject( fault ); 105 | }); 106 | }); 107 | }, 108 | onLogout = function() 109 | { 110 | $log.debug( "onLogout( )" ); 111 | 112 | $log.tryCatch( function() 113 | { 114 | return authenticator 115 | .logout( ) 116 | .then( function onResult_logout( ) 117 | { 118 | $log.debug( "onResult_logout()" ); 119 | 120 | $scope.sessionID = null; 121 | session.sessionID = null; 122 | 123 | errorMessage( "" ); 124 | 125 | return session; 126 | }, 127 | function onFault_login( fault ) 128 | { 129 | fault = fault || SERVER_NOT_RESPONDING; 130 | $log.error( fault.toString() ); 131 | 132 | // force clear any previously valid session... 133 | session.sessionID = null; 134 | errorMessage( fault ); 135 | 136 | if ( fault == TIMEOUT_RESPONSE ) { errorMessage( SERVER_NOT_RESPONDING ); } 137 | if ( fault == PAGE_NOT_FOUND ) { errorMessage( PAGE_NOT_FOUND ); } 138 | 139 | return $q.rejected( session ); 140 | }); 141 | }); 142 | }; 143 | 144 | 145 | $scope.email = "ThomasBurleson@gmail.com"; 146 | $scope.password = "none"; 147 | $scope.sessionID = session.sessionID; 148 | $scope.errorMessage = ""; 149 | 150 | $scope.submit = onLogin; 151 | $scope.logout = onLogout; 152 | $scope.register = announceNA; 153 | 154 | }; 155 | 156 | // Register as global constructor function 157 | 158 | return [ "session", "authenticator", "$scope", "$q", "$log", "$location", LoginController ]; 159 | 160 | }); 161 | 162 | 163 | }( define )); 164 | -------------------------------------------------------------------------------- /client/src/quizzer/authentication/Session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This Session module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function (define ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the Session class with RequireJS 15 | */ 16 | define( [], function ( ) { 17 | 18 | var validate = function ( target, defaultVal ) 19 | { 20 | return target || defaultVal; 21 | }, 22 | onClear = function( all ) 23 | { 24 | _session.account.userName = validate( all, false ) ? '' : _session.account.userName; 25 | _session.account.password = ''; 26 | _session.account.email = ''; 27 | _session.sessionID = null; 28 | 29 | // TODO - refactor since these are specific to the `quiz` module 30 | 31 | _session.quiz = undefined; 32 | _session.score = undefined; 33 | _session.selectedQuiz = 1; 34 | 35 | return _session; 36 | }, 37 | _session = { 38 | account : { 39 | userName : '', 40 | password : '', 41 | email : '' 42 | }, 43 | 44 | sessionID : null, 45 | clear : onClear, 46 | logout : onClear, 47 | 48 | selectedQuiz : 1 49 | }; 50 | 51 | 52 | /** 53 | * Publishes a constructor function which returns the `session` singleton instances 54 | * 55 | * @returns Hashmap 56 | * @constructor 57 | */ 58 | return function () { 59 | return _session; 60 | }; 61 | 62 | }); 63 | 64 | 65 | }( define )); 66 | -------------------------------------------------------------------------------- /client/src/quizzer/authentication/SessionController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This LoginController module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the SessionController class with RequireJS 15 | */ 16 | define( [ 'utils/supplant' ], function ( supplant ) 17 | { 18 | var VIEW_LOGIN = "/login", 19 | 20 | /** 21 | * SessionController 22 | * @constructor 23 | */ 24 | SessionController = function( session, $rootScope, $log, $location ) 25 | { 26 | /** 27 | * AutoRouteToLogin() 28 | */ 29 | var validateSession = function() 30 | { 31 | if ( session && !session.sessionID ) 32 | { 33 | if ( $location.path() != VIEW_LOGIN ) 34 | { 35 | $log.debug( "session is invalid - routing to '{0}' ", [ VIEW_LOGIN ] ); 36 | $location.path( VIEW_LOGIN ); 37 | } 38 | } 39 | }; 40 | 41 | $log = $log.getInstance( "SessionController" ); 42 | $log.debug( "constructor() "); 43 | 44 | // TODO - remember the bookmark url... and reroute to original bookmark AFTER login finishes 45 | // TODO - instead of reroute to Login... simply show the Login overlay WITHOUT changing $location 46 | 47 | // Make sure that we always have a valid session 48 | 49 | $rootScope.$on('$routeChangeSuccess', function() 50 | { 51 | validateSession(); 52 | }); 53 | 54 | // Watch the sessionID and auto route to the Login view 55 | // if logout() is invoked... 56 | 57 | $rootScope.$watch( function getSession() 58 | { 59 | return session.sessionID; 60 | 61 | }, validateSession ); 62 | 63 | 64 | }; 65 | 66 | // Register as global constructor function 67 | 68 | return [ "session", "$rootScope", "$log", "$location", SessionController ]; 69 | 70 | }); 71 | 72 | 73 | }( define )); 74 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/QuizModule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ****************************************************************************************************** 3 | * 4 | * QuizModule 5 | * 6 | * Defines controllers and services for the Online Quiz 7 | * 8 | * @author Thomas Burleson 9 | * @date December 2013 10 | * 11 | * ****************************************************************************************************** 12 | */ 13 | 14 | (function ( define, angular ) { 15 | "use strict"; 16 | 17 | define([ 18 | 'quiz/delegates/QuizDelegate', 19 | 'quiz/controllers/TestController', 20 | 'quiz/controllers/ScoreController' 21 | ], 22 | function ( QuizDelegate, TestController, ScoreController ) 23 | { 24 | var moduleName = "quizzer.Quiz"; 25 | 26 | angular.module( moduleName, [ ] ) 27 | .service( "quizDelegate", QuizDelegate ) 28 | .controller( "TestController", TestController ) 29 | .controller( "ScoreController", ScoreController ); 30 | 31 | return moduleName; 32 | }); 33 | 34 | }( define, angular )); 35 | 36 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/RouteManager.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * ****************************************************************************************************** 4 | * 5 | * QuizModule 6 | * 7 | * Defines controllers and services for the Online Quiz 8 | * 9 | * @author Thomas Burleson 10 | * @date December 2013 11 | * 12 | * ****************************************************************************************************** 13 | */ 14 | 15 | (function ( define ) { 16 | "use strict"; 17 | 18 | 19 | define([ 20 | 'utils/logger/ExternalLogger', 21 | 'auth/LoginController', 22 | 'quiz/controllers/TestController', 23 | 'quiz/controllers/ScoreController' 24 | ], 25 | function ( $log, LoginController, TestController, ScoreController ) 26 | { 27 | /** 28 | * Route management constructor () 29 | * - to be used in angular.config() 30 | * 31 | * @see bootstrap.js 32 | */ 33 | var RouteManager = function ( $routeProvider ) 34 | { 35 | $log.debug( "Configuring $routeProvider..."); 36 | 37 | $routeProvider 38 | .when( '/login', { 39 | templateUrl : "./assets/views/login.tpl.html", 40 | controller : "LoginController" 41 | }) 42 | .when( '/quiz/:question?', { 43 | templateUrl : "./assets/views/quiz.tpl.html", 44 | controller : "TestController" 45 | }) 46 | .when( '/scoring', { 47 | templateUrl : "./assets/views/score.tpl.html", 48 | controller : "ScoreController" 49 | }) 50 | .otherwise({ 51 | redirectTo : '/login' 52 | }); 53 | 54 | }; 55 | 56 | $log = $log.getInstance( "RouteManager" ); 57 | 58 | return ["$routeProvider", RouteManager ]; 59 | }); 60 | 61 | 62 | }( define )); 63 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/builders/QuizBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This QuizBuilder module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define, _ ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the QuizBuilder class with RequireJS 15 | */ 16 | define([ 17 | 'utils/supplant', 18 | 'utils/logger/ExternalLogger' 19 | ], 20 | function ( supplant, $log ) 21 | { 22 | /** 23 | * Builder enables construction of a `quiz` model instance from JSON 24 | * e.g. 25 | * 26 | * Question : { 27 | * 28 | * question : "Which is not an advantage of using a closure?", 29 | * choices : [ 30 | * "Prevent pollution of global scope", 31 | * "Encapsulation", 32 | * "Private properties and methods", 33 | * "Allow conditional use of ‘strict mode" 34 | * ], 35 | * answer : 3 // value is 1-based index 36 | * 37 | * } 38 | * 39 | * @constructor 40 | */ 41 | var QuizBuilder = function( ) 42 | { 43 | $log = $log.getInstance( "QuizBuilder" ); 44 | 45 | /** 46 | * buildFromJSON() creates a `smart` Quiz instance from JSON data 47 | * 48 | * @param data JSON data representing a quiz 49 | * @returns {Object} quiz 50 | */ 51 | var buildFromJSON = function( data ) 52 | { 53 | var quiz = null, 54 | index = -1, 55 | numQuestions = 0, 56 | 57 | /** 58 | * Accessor to current question 59 | * @returns {Question} 60 | */ 61 | currentQuestion = function() 62 | { 63 | var inRange = (numQuestions > 0) && (index > -1) && (index < (numQuestions - 1)); 64 | 65 | return inRange ? quiz.questions[ index ] : null; 66 | }, 67 | 68 | /** 69 | * Find question at specified index 70 | * @returns {Question} 71 | */ 72 | findAtIndex = function( val ) 73 | { 74 | var inRange, question; 75 | 76 | // external world uses 1-based, here is 0-based. 77 | val = val - 1; 78 | 79 | inRange = (numQuestions > 0) && (val > -1) && (val < numQuestions); 80 | question = inRange ? quiz.questions[ val ] : null; 81 | index = inRange ? val : index; 82 | 83 | return question; 84 | }, 85 | 86 | /** 87 | * Pre-check to determine if another question is still 88 | * available/unanswered. 89 | * 90 | * @returns {number|boolean} 91 | */ 92 | hasNext = function() 93 | { 94 | return numQuestions && (index < (numQuestions - 1) ); 95 | }, 96 | 97 | /** 98 | * Increment to the next question and return reference 99 | * @returns {*} 100 | */ 101 | nextQuestion = function() 102 | { 103 | return hasNext() ? quiz.questions[ ++index ] : null; 104 | }, 105 | 106 | /** 107 | * Clear the tester's answers and any score summary information 108 | */ 109 | resetQuiz = function() 110 | { 111 | // Start again with first question.. 112 | index = -1; 113 | 114 | // NOTE: Since the answers are temporarily part of each question, 115 | // we should clear user-given answers ! 116 | 117 | _.every( quiz.questions, function( question ) 118 | { 119 | // Clear `selected` value.. 120 | if ( question.hasOwnProperty("selected") ) 121 | { 122 | question.selected = undefined; 123 | } 124 | }); 125 | 126 | }, 127 | 128 | /** 129 | * Massage the questions to `inject` an index for output like: 130 | * 131 | * ) 132 | * 133 | * @returns {Quiz} 134 | */ 135 | addQuestionIndices = function( quiz ) 136 | { 137 | _.each(quiz.questions, function(question, index) { 138 | // Inject Question # value 139 | question.index = index + 1; 140 | }); 141 | 142 | return quiz; 143 | }, 144 | 145 | /** 146 | * Create instance of a Quiz object 147 | * @returns {*} 148 | */ 149 | buildQuizInstance = function() 150 | { 151 | $log.debug( "buildQuizInstance()" ); 152 | 153 | numQuestions = data.questions.length; 154 | 155 | // Create instance of a Quiz object 156 | 157 | quiz = { 158 | 159 | // properties 160 | 161 | uid : data.uid, 162 | name : data.name, 163 | current : currentQuestion, 164 | questions : [].concat( data.questions ), 165 | 166 | // methods 167 | 168 | calculateScore : undefined, 169 | reset : resetQuiz, 170 | hasNext : hasNext, 171 | nextQuestion : nextQuestion, 172 | seekQuestion : findAtIndex 173 | 174 | }; 175 | 176 | return addQuestionIndices(quiz); 177 | }; 178 | 179 | 180 | $log.debug( "buildFromJSON()" ); 181 | 182 | if ( data == null ) { data = {}; } 183 | if ( data.questions == null ) { data.questsions = [ ]; } 184 | 185 | 186 | return buildQuizInstance(); 187 | }; 188 | 189 | // Publish API to build Quiz instances from JSON data 190 | 191 | return { 192 | 193 | fromJSON : buildFromJSON 194 | }; 195 | 196 | 197 | }; 198 | 199 | // Register as global constructor function 200 | 201 | return [ "$log", QuizBuilder ]; 202 | 203 | }); 204 | 205 | 206 | }( define, _ )); 207 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/builders/ScoreBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This QuizBuilder module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define, _ ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the QuizBuilder class with RequireJS 15 | */ 16 | define([ 17 | 'utils/supplant', 18 | 'utils/logger/ExternalLogger' 19 | ], 20 | function ( supplant, $log ) 21 | { 22 | /** 23 | * Labels table used to convert numbered lists to `lettered` lists 24 | * @type {Array} 25 | */ 26 | var LIST_LABELS = [ "A", "B", "C", "D", "E", "F", "G", "H" ], 27 | 28 | /** 29 | * Constructor function used by AngularJS to create instances of 30 | * a service, factory, or controller. 31 | * 32 | * @constructor 33 | */ 34 | ScoreBuilder = function( ) 35 | { 36 | $log = $log.getInstance( "ScoreBuilder" ); 37 | 38 | /** 39 | * Partial application to capture the quiz instance and 40 | * allow subsequent calculation of the score. 41 | * 42 | * @param quiz 43 | * @returns {Function} 44 | */ 45 | var calculateScoreDetails = function( quiz ) 46 | { 47 | var answers = { 48 | expected : [ ], 49 | given : [ ] 50 | }, 51 | 52 | /** 53 | * Gather all user-given answers into flat array 54 | */ 55 | gatherAnswers = function() 56 | { 57 | $log.debug( "gatherAnswers()" ); 58 | 59 | answers.expected = _.pluck( quiz.questions, "answer" ), // Expected answers 60 | answers.given = _.pluck( quiz.questions, "selected" ); // User-provided answers, @see calculateTotal() 61 | 62 | // NOTE: Since the answers are temporarily part of each question, 63 | // we should clear user-given answers ! 64 | 65 | _.every( quiz.questions, function( question ) 66 | { 67 | // Clear `selected` value.. 68 | if ( question.hasOwnProperty("selected") ) 69 | { 70 | delete question.selected; 71 | } 72 | }); 73 | }, 74 | 75 | /** 76 | * 77 | * @returns {number} 78 | */ 79 | calculateTotal = function() 80 | { 81 | var j, 82 | total = 0, 83 | count = answers.expected.length, 84 | expected = answers.expected, 85 | given = answers.given; 86 | 87 | $log.debug( "calculateTotal()" ); 88 | 89 | // Loop to check if given answer matches expected... 90 | for ( j = 0; j < count; j++ ) 91 | { 92 | if ( j >= given.length ) { 93 | break; 94 | } 95 | if ( expected[j] == given[j] ) { 96 | total += 1; 97 | } 98 | } 99 | 100 | // Make sure that total score is not > 100% 101 | return Math.min( 100, Math.ceil( (total/count)*100 )); 102 | }, 103 | 104 | /** 105 | * 106 | * @returns {Array} 107 | */ 108 | buildReviewList = function() 109 | { 110 | var j, 111 | list = [], 112 | isCorrect= false, 113 | question = null, 114 | expected = answers.expected, 115 | given = answers.given, 116 | count = expected.length, 117 | /** 118 | * For the specified index, lookup the `choice` 119 | * for the current question. 120 | * @param index 121 | * @returns {*} String 122 | */ 123 | choiceAt = function( index ) 124 | { 125 | return question.choices[ index ]; 126 | }, 127 | /** 128 | * Convert index value to "letter" label 129 | * @param index 130 | * @returns {*} Letter character 131 | */ 132 | labelAt = function( index ) 133 | { 134 | return LIST_LABELS[ index ]; 135 | }; 136 | 137 | $log.debug( "buildReviewList()" ); 138 | 139 | // Loop all answers to build list of `review status` object 140 | for ( j = 0; j < count; j++ ) 141 | { 142 | 143 | question = quiz.questions[j]; 144 | isCorrect = expected[j] == given[j]; 145 | 146 | 147 | // Build list of `score` items 148 | 149 | list.push({ 150 | 151 | index : j + 1, 152 | title : question["question"], 153 | details : question["details"], 154 | correct : isCorrect, 155 | expected : supplant( "{0}. {1}", [ labelAt(expected[j]), choiceAt( expected[j] ) ] ), 156 | answered : isCorrect ? supplant( "{0}. {1}", [ labelAt(given[j]), choiceAt( given[j] ) ] ) : 157 | (given[j] != null) ? supplant( "{0}. {1}", [ labelAt(given[j]), choiceAt( given[j] ) ] ) : "- no answer given -" 158 | }); 159 | 160 | } 161 | 162 | return list; 163 | }, 164 | 165 | /** 166 | * Normally this feature would be server-side... 167 | * for now include it here in the Quiz model 168 | */ 169 | buildScoreInstance = function () 170 | { 171 | var score; 172 | 173 | $log.debug( "buildScoreInstance()" ); 174 | 175 | gatherAnswers(); 176 | 177 | // Now calculate `total score` and build a list of `score` items 178 | score = { 179 | quizName : quiz.name, 180 | totalScore : calculateTotal(), 181 | items : buildReviewList() 182 | }; 183 | 184 | $log.debug( supplant( "calculateScore() == {totalScore} ", score) ); 185 | 186 | return score; 187 | }; 188 | 189 | return buildScoreInstance(); 190 | }; 191 | 192 | // Publish API to build Quiz instances from JSON data 193 | 194 | return { 195 | 196 | calculateScore : calculateScoreDetails 197 | }; 198 | 199 | 200 | }; 201 | 202 | // Register as global constructor function 203 | 204 | return [ "$log", ScoreBuilder ]; 205 | 206 | }); 207 | 208 | 209 | }( define, _ )); 210 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/controllers/ScoreController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This ScoreController module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the ScoreController class with RequireJS 15 | */ 16 | define( [ 'utils/supplant' ], function ( supplant ) 17 | { 18 | /** 19 | * Constructor function used by AngularJS to create instances of 20 | * a service, factory, or controller. 21 | * 22 | * @constructor 23 | */ 24 | var ScoreController = function( $scope, session, $log ) 25 | { 26 | $log = $log.getInstance( "ScoreController" ); 27 | $log.debug( "constructor() "); 28 | 29 | // Configure presentation model data for view consumption 30 | 31 | $scope.title = session.score ? session.score.quizName : 0; 32 | $scope.grade = session.score ? session.score.totalScore : 0; 33 | $scope.scores = session.score ? session.score.items : [ ]; 34 | $scope.email = session.account.email; 35 | 36 | 37 | $scope.logout = session.logout; 38 | }; 39 | 40 | // Register as global constructor function 41 | 42 | return [ "$scope", "session", "$log", ScoreController ]; 43 | 44 | }); 45 | 46 | 47 | }( define )); 48 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/controllers/TestController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This TestController module uses RequireJS to `define` a AngularJS constructor function 4 | * with its dependencies. 5 | * 6 | * @author Thomas Burleson 7 | * @date December, 2013 8 | * 9 | */ 10 | (function( define ) { 11 | "use strict"; 12 | 13 | /** 14 | * Register the TestController class with RequireJS 15 | */ 16 | define( [ 'utils/supplant' ], function ( supplant ) 17 | { 18 | var VIEW_QUESTION = "/quiz/{index}", 19 | VIEW_SCORING = "/scoring", 20 | ANSWER_NEEDED = "Before you can continue, please select your answer!", 21 | 22 | /** 23 | * Constructor function used by AngularJS to create instances of 24 | * a service, factory, or controller. 25 | * 26 | * @constructor 27 | */ 28 | TestController = function( $scope, session, quizDelegate, $location, $routeParams, $log, $window ) 29 | { 30 | $log = $log.getInstance( "TestController" ); 31 | $log.debug( "constructor() "); 32 | 33 | var quiz = session.quiz, 34 | /** 35 | * Navigate to and display the next question 36 | */ 37 | nextQuestion = function() 38 | { 39 | var question = null, 40 | url = ""; 41 | 42 | $log.debug( "nextQuestion( )" ); 43 | 44 | // Fail if the user did NOT select an answer ? 45 | 46 | if ( $scope.challenge && ($scope.challenge.selected === undefined) ) 47 | { 48 | $log.warn( ANSWER_NEEDED ); 49 | $window.alert( ANSWER_NEEDED ); 50 | return; 51 | } 52 | 53 | // Lookup the next question to build and navigate to the URL 54 | 55 | question = quiz.nextQuestion(); 56 | url = supplant( VIEW_QUESTION, question ); 57 | 58 | $log.debug( "Navigating to the '{0}' view...", [ url ] ); 59 | $location.path( url ); 60 | 61 | }, 62 | /** 63 | * Submit the quiz and answers and build the test score details 64 | */ 65 | submitTest = function() 66 | { 67 | $log.debug( "submitTest()" ); 68 | 69 | $log.tryCatch( function() 70 | { 71 | return quizDelegate 72 | .submitQuiz( quiz ) 73 | .then( function( score ) 74 | { 75 | // Cache score information for use by ScoreController 76 | // Navigate to `Score Results` view 77 | 78 | $log.debug( supplant("onResult_submitTest( Test Score = {totalScore} )", [score] )); 79 | 80 | session.score = score; 81 | session.quiz = null; 82 | 83 | $log.debug( "Navigating to the '/scoring' view..." ); 84 | $location.path( VIEW_SCORING ); 85 | 86 | }); 87 | }); 88 | 89 | }, 90 | /** 91 | * Auto-load the quiz and prepare to show the first question... 92 | */ 93 | loadQuiz = function( quizID ) 94 | { 95 | $log.debug( supplant( "loadQuiz( quiz ID = {0} )", [ quizID ] )); 96 | 97 | $log.tryCatch( function() 98 | { 99 | // Load the quiz and save to the session cache 100 | // NOTE: do not publish the entire `quiz` to scope... to much visibility 101 | 102 | return quizDelegate 103 | .loadQuiz( quizID ) 104 | .then( function( instance ) 105 | { 106 | $log.debug( supplant( "onResult_loadQuiz( quizID = {0} )", [ instance.uid ] )); 107 | 108 | // Save the quiz to the session cache 109 | session.quiz = quiz = instance; 110 | $scope.quizName = quiz.name; 111 | 112 | nextQuestion(); 113 | }); 114 | }); 115 | }, 116 | 117 | /** 118 | * Do we already have the question loaded `into` the view ? 119 | * @param qIndex Integer index of the question if the questions list 120 | * @returns {*|boolean} 121 | */ 122 | questionAlreadyLoaded = function( qIndex ) 123 | { 124 | return $scope.challenge && ($scope.challenge.index === qIndex ); 125 | }, 126 | 127 | /** 128 | * Check if specific question is `bookmarked` and should be loaded immediately 129 | * 130 | */ 131 | loadBookMarkedQuestion = function( qIndex ) 132 | { 133 | var question = null; 134 | 135 | // Use specified index or default to 1st question 136 | 137 | qIndex = qIndex || $routeParams.question || 1; 138 | 139 | $scope.next = nextQuestion; 140 | $scope.btnTitle = "Continue"; 141 | 142 | // If we can lookup a question in the quiz, find that question and publish to $scope ? 143 | 144 | if ( quiz && angular.isDefined( qIndex ) && !questionAlreadyLoaded( qIndex ) ) 145 | { 146 | $log.debug( "loadBookMarkedQuestion( index = {0} )", [ qIndex ] ); 147 | 148 | question = quiz.seekQuestion( qIndex ); 149 | 150 | $log.debug( " current question = {index} ", question ); 151 | 152 | // The last question will `submit` the quiz answers... 153 | 154 | $scope.next = quiz.hasNext() ? nextQuestion : submitTest; 155 | $scope.btnTitle = quiz.hasNext() ? "Continue" : "Submit"; 156 | } 157 | 158 | return question; 159 | }; 160 | 161 | 162 | // Load selected quiz and configure presentation model 163 | // data for view consumption 164 | 165 | if ( session.sessionID && ( !quiz || quiz.uid != session.selectedQuiz ) ) 166 | { 167 | loadQuiz( session.selectedQuiz ); 168 | } 169 | 170 | 171 | $scope.quizName = quiz ? quiz.name : ""; 172 | $scope.challenge = loadBookMarkedQuestion(); 173 | 174 | }; 175 | 176 | // Register as global constructor function 177 | 178 | return [ "$scope", "session", "quizDelegate", "$location", "$routeParams", "$log", "$window", TestController ]; 179 | 180 | }); 181 | 182 | 183 | }( define )); 184 | -------------------------------------------------------------------------------- /client/src/quizzer/quiz/delegates/QuizDelegate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ****************************************************************************************************** 3 | * 4 | * QuizDelegate 5 | * 6 | * Data service proxy to QuizDelegate API that loads Quiz JSON data and builds quiz instances 7 | * 8 | * @author Thomas Burleson 9 | * @date December, 2013 10 | * 11 | * 12 | * ****************************************************************************************************** 13 | */ 14 | (function ( define ) { 15 | "use strict"; 16 | 17 | var QUIZ_TEMPLATE = "./assets/data/quiz_{id}.json"; 18 | 19 | define([ 20 | 'utils/supplant', 21 | 'utils/Factory', 22 | 'quiz/builders/QuizBuilder', 23 | 'quiz/builders/ScoreBuilder' 24 | ], 25 | function ( supplant, Factory, QuizBuilder, ScoreBuilder ) 26 | { 27 | /** 28 | * Builder used to create Quiz instance from JSON data 29 | * @type {Quiz} 30 | */ 31 | var quizBuilder = Factory.instanceOf( QuizBuilder ), 32 | /** 33 | * Builder used to create Score instances from a specified Quiz instance 34 | * @type {Score} 35 | */ 36 | scoreBuilder = Factory.instanceOf( ScoreBuilder), 37 | 38 | /** 39 | * QuizDelegat 40 | * @constructor 41 | */ 42 | QuizDelegate = function ( $http, $q, $log ) 43 | { 44 | $log = $log.getInstance( "QuizDelegate" ); 45 | 46 | /** 47 | * Util function to build a resolved promise; 48 | * ...resolved with specified value 49 | * 50 | * @returns {promise|*|promise} 51 | */ 52 | var makeResolved = function( response ) 53 | { 54 | var dfd = $q.defer(); 55 | dfd.resolve( response ); 56 | 57 | return dfd.promise; 58 | }, 59 | 60 | /** 61 | * Load Quiz questions/choices/answers data 62 | * 63 | * @param quizID is the Quiz ID that should be loaded 64 | * @return Promise 65 | */ 66 | loadByID = function( quizID ) 67 | { 68 | var LOAD_URL = supplant( QUIZ_TEMPLATE, { id : quizID } ); 69 | 70 | $log.debug( 71 | "loadQuiz( quizID={0} )", 72 | [ quizID ] 73 | ); 74 | 75 | // Loads quiz JSON from local data file and delivers a quiz instance 76 | 77 | return $http 78 | .get( LOAD_URL ) 79 | .then( function( response ) 80 | { 81 | return quizBuilder.fromJSON( response.data ); 82 | }); 83 | }, 84 | 85 | /** 86 | * Calculate user test score for the specified quiz 87 | * NOTE: this is currently performed client-side for now... 88 | * 89 | * @return Promise 90 | */ 91 | submitQuiz = function( quiz, email ) 92 | { 93 | $log.debug( "submitQuiz()" ); 94 | 95 | // Normally we have remote REST services... 96 | // return $http.post( URL.SUBMIT_QUIZ, { quiz : quiz, who : email } ); 97 | 98 | // For now, ask the quiz to calculate its own score... 99 | 100 | return makeResolved( scoreBuilder.calculateScore( quiz ) ); 101 | }; 102 | 103 | 104 | // Publish Authentication delegate instance/object with desired API 105 | 106 | return { 107 | 108 | loadQuiz : loadByID, 109 | submitQuiz : submitQuiz 110 | 111 | }; 112 | 113 | }; 114 | 115 | return [ "$http", "$q", "$log", QuizDelegate ]; 116 | }); 117 | 118 | }( define )); 119 | -------------------------------------------------------------------------------- /client/test/config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Jasmine boot.js for browser runners 4 | * 5 | * Exposes external/global interface, builds the Jasmine environment and executes it. 6 | * 7 | * 8 | * 9 | */ 10 | (function( jasmine, requirejs ) { 11 | "use strict"; 12 | 13 | var jasmineEnv = null, 14 | configureJasmine = function () 15 | { 16 | var htmlReporter = new jasmine.HtmlReporter(), 17 | jasmineEnv = jasmine.getEnv(), 18 | filterFn = function (spec) 19 | { 20 | return htmlReporter.specFilter(spec); 21 | }; 22 | 23 | jasmineEnv.VERBOSE = true; 24 | jasmineEnv.updateInterval = 1000; 25 | jasmineEnv.specFilter = filterFn; 26 | jasmineEnv.addReporter(htmlReporter); 27 | 28 | return jasmineEnv; 29 | }; 30 | 31 | 32 | // **************************************************** 33 | // Prepare the onLoad interceptor 34 | // **************************************************** 35 | 36 | /** 37 | * Head hook our window `onload` handler to start the 38 | * requireJS bootstrap... 39 | */ 40 | window.onload = (function( handler ) 41 | { 42 | var interceptor = function() 43 | { 44 | if ( handler ) handler(); 45 | startRequireJS(); 46 | }; 47 | 48 | jasmineEnv = configureJasmine( jasmine); 49 | 50 | return interceptor; 51 | 52 | })( window.onload ); 53 | 54 | 55 | // **************************************************** 56 | // Startup with RequireJS 57 | // **************************************************** 58 | 59 | function startRequireJS () 60 | { 61 | 62 | requirejs.config({ 63 | 64 | baseUrl: '../src', 65 | 66 | paths: { 67 | 68 | 'jquery' : '../vendor/jquery/jquery.min', 69 | 'angular' : '../vendor/angular/angular', 70 | 'ngRoute' : '../vendor/angular-route/angular-route', 71 | 'ngSanitize' : '../vendor/angular-sanitize/angular-sanitize', 72 | 73 | // Configure alias to full paths 74 | 75 | 'auth' : './quizzer/authentication', 76 | 'quiz' : './quizzer/quiz', 77 | 'utils' : './mindspace/utils', 78 | 79 | // Special library to run AngularJS Jasmine tests with LIVE $http 80 | 81 | 'test' : '../test/spec/quizzer', 82 | 83 | // Special RequireJS plugin for "text!..." usages 84 | 85 | 'text' : '../vendor/_custom/require/text' 86 | }, 87 | 88 | shim: { 89 | 'angular': 90 | { 91 | exports : 'angular' 92 | } 93 | }, 94 | 95 | priority: 'angular' 96 | }); 97 | 98 | 99 | // Manually specify all the Spec Test files... 100 | 101 | var dependencies = [ 102 | 'angular', 103 | 'utils/logger/LogDecorator', 104 | 'test/authentication/AuthenticatorSpec', 105 | 'test/authentication/LoginControllerSpec', 106 | 'test/authentication/SessionSpec' 107 | ]; 108 | 109 | 110 | /** 111 | * Load all the Specs and then start the bootstrap engine... 112 | */ 113 | require( dependencies , function( angular, LogDecorator ) { 114 | 115 | // Prepare `test` module for all the specs (if needed) 116 | // Provide contextRoot for all `live` delegate testing 117 | 118 | angular.module('test.quizzler', [ ]) 119 | .config( LogDecorator ); 120 | 121 | // auto start test runner, once Require.js is done 122 | jasmineEnv.execute(); 123 | 124 | }); 125 | 126 | } 127 | 128 | 129 | 130 | }( jasmine, requirejs )); 131 | -------------------------------------------------------------------------------- /client/test/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NodeJS configuration used for the Karma Server 3 | * 4 | * This code does three (3) things: 5 | * 1 ) configures NodeJS Karma server with frameworks: Jasmine and RequireJS 6 | * 2 ) auto-loads the jQuery, AngularJS, and AngularJS Mock libs 7 | * 3 ) configures paths that should be included in the browser using 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/Proveyourself.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/docs/Proveyourself.pdf -------------------------------------------------------------------------------- /docs/Quizzler_Workflow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/docs/Quizzler_Workflow.jpg -------------------------------------------------------------------------------- /docs/Quizzler_Workflow_large.fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/docs/Quizzler_Workflow_large.fw.png -------------------------------------------------------------------------------- /docs/Quizzler_Workflow_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/docs/Quizzler_Workflow_large.jpg -------------------------------------------------------------------------------- /docs/quiz_comps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/docs/quiz_comps.jpg -------------------------------------------------------------------------------- /tools/webserver/Configure NodeJS Server.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThomasBurleson/angularjs-Quizzler/4c287529aff6e12e978293a46093780e2133a176/tools/webserver/Configure NodeJS Server.jpg -------------------------------------------------------------------------------- /tools/webserver/node_modules/httpServers.coffee: -------------------------------------------------------------------------------- 1 | # 2 | # ****************************************************************************************************** 3 | # 4 | # HTTP-Proxy development server 5 | # 6 | # This server listens on http://locahost:8080 and [based on request paths] proxies requests to local HTTP server 7 | # or remote server. 8 | # 9 | # Copyright (c) 2012 Mindspace, LLC 10 | # 11 | # npm install -g http-proxy 12 | # npm install -g coffee-script 13 | # 14 | # ****************************************************************************************************** 15 | # 16 | 17 | 18 | # ************************************************* 19 | # HTTP_PROXY Server 20 | # 21 | # This server will proxy all GET/HEAD requests to either a remote web server 22 | # or a local web server. This means that another HTTP server must be instantiated 23 | # to provide responses to delegated local requests. 24 | # 25 | # ************************************************* 26 | 27 | class HttpProxyServer 28 | 29 | sys = require( 'util' ) 30 | httpProxy = require( 'http-proxy' ) 31 | 32 | 33 | # An Http server implementation that uses a map of methods to decide 34 | # action routing. 35 | # 36 | # @param { 37 | # proxy_regexp : /^\/api\/json/ // RegExp to test URL to determine in proxy is needed 38 | # local_host : "127.0.0.1" // [or 127.0.0.1] 39 | # local_port : 8080 40 | # local_port : 8000 41 | # remote_host : "data.gridlinked.info" // domainName or IP for remote server 42 | # remote_port : 80 43 | # } 44 | # 45 | constructor : () -> 46 | @proxy = httpProxy.createServer( handleRequest.bind(@) ) 47 | return @ 48 | 49 | 50 | start : ( @config ) -> 51 | @proxy.listen( @config.local_port ) 52 | sys.puts( "HttpProxy Server running at http://localhost:#{@config.local_port}/" ) 53 | 54 | # Hidden server for fallback, non-proxied web assets 55 | return new HttpServer().start( @config, true ) 56 | 57 | 58 | # **************************** 59 | # Private methods 60 | # **************************** 61 | 62 | handleRequest = ( request, response, proxy ) -> 63 | needProxy = @config.proxy_enabled and @config.proxy_regexp.test( request.url ) 64 | config = 65 | port : if needProxy then @config.remote_port else @config.silent_port 66 | host : if needProxy then @config.remote_host else @config.local_host 67 | 68 | sys.puts( "Proxying request from `#{ request.url }` to `http://#{ config.host }:#{ config.port }#{ request.url }`." ) if needProxy 69 | 70 | request.headers.host = config.host 71 | proxy.proxyRequest( request, response, config ) 72 | return 73 | 74 | 75 | 76 | # ************************************************* 77 | # HTTP Server 78 | # ************************************************* 79 | 80 | class HttpServer 81 | 82 | sys = require( 'util' ) 83 | http = require( 'http' ) 84 | url = require( 'url' ) 85 | 86 | # An Http server implementation that uses a map of methods to decide 87 | # action routing. 88 | # 89 | # @param {Object} Map of method => Handler function 90 | # 91 | constructor : (@handlers) -> 92 | @handlers ||= { 93 | 'GET' : createServlet( StaticServlet ) 94 | 'HEAD' : createServlet( StaticServlet ) 95 | 'POST' : createServlet( StaticServlet ) 96 | } 97 | @server = http.createServer( handleRequest.bind(@) ) 98 | return @ 99 | 100 | 101 | start : ( @config, silent=false ) -> 102 | # !! Add fallback, local web server port; default == `local_port + 100` 103 | @config.silent_port ||= Number(@config.local_port) + 100 104 | 105 | @server.listen( @config.silent_port ) 106 | sys.puts( "Http Server running at http://localhost:#{@config.silent_port}/" ) if ( !silent ) 107 | return 108 | 109 | # **************************** 110 | # Private methods 111 | # **************************** 112 | 113 | createServlet = (Class) -> 114 | servlet = new Class() 115 | servlet.handleRequest.bind(servlet) 116 | 117 | 118 | parseURL = ( target ) -> 119 | parsed = url.parse( target ) 120 | parsed.pathname = url.resolve('/',parsed.pathname) 121 | return url.parse( url.format(parsed), true ) 122 | 123 | 124 | handleRequest = (req, res) -> 125 | logEntry = "#{req.method} #{req.url}" 126 | logEntry += " #{req.headers['user-agent']}" if (req.headers['user-agent']) 127 | 128 | sys.puts( logEntry ) 129 | req.url = parseURL(req.url) 130 | handler = @handlers[req.method] 131 | 132 | if ( !handler ) 133 | res.writeHead(501) 134 | res.end() 135 | return 136 | 137 | handler.call(@,req,res) 138 | 139 | 140 | 141 | 142 | # ************************************************* 143 | # Static Servlet 144 | # ************************************************* 145 | 146 | class StaticServlet 147 | 148 | sys = require( 'util' ) 149 | fs = require( 'fs' ) 150 | url = require( 'url' ) 151 | 152 | 153 | MimeMap = 154 | 'coffee': 'text/plain' 155 | 'm3u8' : 'text/plain' 156 | 'txt' : 'text/plain' 157 | 'html' : 'text/html' 158 | 'css' : 'text/css' 159 | 'xml' : 'application/xml' 160 | 'json' : 'application/json' 161 | 'js' : 'application/javascript' 162 | 'jpg' : 'image/jpeg' 163 | 'jpeg' : 'image/jpeg' 164 | 'gif' : 'image/gif' 165 | 'png' : 'image/png' 166 | 167 | 168 | handleRequest : (req, res) -> 169 | path = "./#{req.url.pathname}" 170 | .replace('//','/') 171 | .replace(/%(..)/, (match,hex) -> 172 | return String.fromCharCode( parseInt(hex,16) ) 173 | ) 174 | 175 | # # Remove querystrings from local 176 | # [path, qs_data] = path.split '?',2 177 | 178 | # qs = {} 179 | # qs_data ||= '' 180 | 181 | # for elms in qs_data.split '&' 182 | # el = elms.split '=', 2 183 | # qs[el[0]] = el[1] 184 | 185 | parts = path.split('/') 186 | 187 | return sendForbidden(req, res, path) if (parts[parts.length-1].charAt(0) is '.') 188 | 189 | fs.stat(path, (err, stat) -> 190 | return sendMissing( req, res, path ) if err 191 | return sendDirectory( req, res, path ) if stat.isDirectory() 192 | 193 | return sendFile( req, res, path ) 194 | ) 195 | 196 | 197 | # **************************************************** 198 | # Private Methods 199 | # **************************************************** 200 | 201 | 202 | 203 | # Output File contents; with correct mimetype header 204 | # 205 | sendFile = (req, res, path) -> 206 | cType = MimeMap[path.split('.').pop()] 207 | res.writeHead( 200, 'Content-Type': cType || 'text/html' ) 208 | res.end() if req.method is 'HEAD' 209 | 210 | file = fs.createReadStream(path) 211 | file.on( 'data', res.write.bind(res) ) 212 | file.on( 'close', () -> res.end() ) 213 | file.on( 'error', (error) -> sendError(req,res,error) ) 214 | return 215 | 216 | 217 | # Output HTML of directory content listing 218 | # 219 | sendDirectory = (req, res, path) -> 220 | if ( path.match(/[^\/]$/) ) 221 | req.url.pathname += '/' 222 | redirectUrl = url.format( 223 | url.parse(url.format(req.url)) 224 | ) 225 | return sendRedirect( req, res, redirectUrl ) 226 | 227 | fs.readdir( path, (err, files) => 228 | return sendError( req, res, error ) if err 229 | return writeDirectoryList( req, rees, path, []) if !files.length 230 | 231 | numFiles = files.length 232 | files.forEach( (fileName, index) -> 233 | fs.stat( path, (err, stat) -> 234 | return sendMissing( req, res, path ) if err 235 | files[index] = fileName + '/' if stat.isDirectory() 236 | 237 | return writeDirectoryList(req, res, path, files) if ( !(--numFiles) ) 238 | ) 239 | ) 240 | ) 241 | 242 | 243 | # Output 500 response 244 | # 245 | sendError = (req, res, path) -> 246 | content = """ 247 | 248 | 249 | 250 | Internal Server Error 251 | 252 | 253 |

    Internal Server Error

    254 |
    #{ escapeHtml(sys.inspect(error)) }#
    255 | 256 | 257 | """ 258 | res.writeHead( 500, 'Content-Type': 'text/html' ) 259 | res.write( content ) 260 | res.end() 261 | 262 | sys.puts('500 Internal Server Error'); 263 | sys.puts(sys.inspect(error)); 264 | return 265 | 266 | 267 | # Output 404 (Not Found) Response 268 | # 269 | sendMissing = (req, res, path) -> 270 | content = """ 271 | 272 | 273 | 274 | 404 Not Found 275 | 276 | 277 |

    Missing / Not Found

    278 |

    279 | The requested URL `#{ escapeHtml(path.substring(1)) }` 280 | was not found on this server. 281 |

    282 | 283 | 284 | """ 285 | res.writeHead( 404, 'Content-Type': 'text/html' ) 286 | res.write( content ) 287 | res.end() 288 | 289 | sys.puts("404 Not Found: #{path}"); 290 | return 291 | 292 | 293 | # Output 403 (Forbidden) response 294 | # 295 | sendForbidden = (req, res, path) -> 296 | content = """ 297 | 298 | 299 | 300 | 403 Forbidden 301 | 302 | 303 |

    Forbidden/h1> 304 |

    305 | You do not have permission to access 306 | `#{ escapeHtml(path.substring(1)) }` 307 | on this server. 308 |

    309 | 310 | 311 | """ 312 | res.writeHead( 403, 'Content-Type': 'text/html' ) 313 | res.write( content ) 314 | res.end() 315 | 316 | sys.puts("403 Forbidden: #{path}"); 317 | return 318 | 319 | # Output 301 (Redirect) response 320 | # 321 | sendRedirect = (req, res, redirectUrl) -> 322 | content = """ 323 | 324 | 325 | 326 | 301 Moved Permanently 327 | 328 | 329 |

    Moved Permanently/h1> 330 |

    331 | The document has moved to here 332 |

    333 | 334 | 335 | """ 336 | res.writeHead( 301, 337 | 'Content-Type': 'text/html' 338 | 'Location' : redirectUrl 339 | ) 340 | res.write( content ) 341 | res.end() 342 | 343 | sys.puts("301 Moved Permanently: #{redirectUrl}"); 344 | return 345 | 346 | 347 | 348 | # Output HTML listing of directory contents 349 | # 350 | writeDirectoryList = (req, res, path, files) -> 351 | res.writeHead( 200, 'Content-Type': 'text/html' ) 352 | return res.end() if (req.method is 'HEAD') 353 | 354 | rows = "" 355 | files.forEach( (name) -> 356 | if ( name.charAt(0) isnt '.' ) 357 | name = name.substring(0,name.length-1) if (name.charAt(name.length-1) is '/') 358 | rows += "
  • #{name}
  • " 359 | ) 360 | 361 | content = """ 362 | 363 | 364 | 365 | "#{ escapeHtml(path) }" 366 | 372 | 373 | 374 |

    Directory: #{ escapeHtml(path) }

    375 |
      376 | #{rows} 377 |
    378 | 379 | 380 | """ 381 | 382 | res.write( content ) 383 | res.end() 384 | return 385 | 386 | 387 | # ************************************ 388 | # Private Utility methods 389 | # ************************************ 390 | 391 | escapeHtml = (value) -> 392 | value.toString() 393 | .replace('<', '<') 394 | .replace('>', '>') 395 | .replace('"', '"') 396 | 397 | 398 | 399 | # ************************************************ 400 | # Exports classes required (2) servers 401 | # ************************************************ 402 | 403 | exports.StaticServlet = StaticServlet 404 | exports.HttpServer = HttpServer 405 | exports.HttpProxyServer = HttpProxyServer 406 | 407 | 408 | 409 | -------------------------------------------------------------------------------- /tools/webserver/npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ 'node', '/usr/local/bin/npm', 'install', 'httpServers' ] 3 | 2 info using npm@1.3.21 4 | 3 info using node@v0.10.24 5 | 4 verbose cache add [ 'httpServers', null ] 6 | 5 verbose cache add name=undefined spec="httpServers" args=["httpServers",null] 7 | 6 verbose parsed url { protocol: null, 8 | 6 verbose parsed url slashes: null, 9 | 6 verbose parsed url auth: null, 10 | 6 verbose parsed url host: null, 11 | 6 verbose parsed url port: null, 12 | 6 verbose parsed url hostname: null, 13 | 6 verbose parsed url hash: null, 14 | 6 verbose parsed url search: null, 15 | 6 verbose parsed url query: null, 16 | 6 verbose parsed url pathname: 'httpServers', 17 | 6 verbose parsed url path: 'httpServers', 18 | 6 verbose parsed url href: 'httpServers' } 19 | 7 silly lockFile 4e07e4ae-httpServers httpServers 20 | 8 verbose lock httpServers /Users/thomasburleson/.npm/4e07e4ae-httpServers.lock 21 | 9 silly lockFile 4e07e4ae-httpServers httpServers 22 | 10 silly lockFile 4e07e4ae-httpServers httpServers 23 | 11 verbose addNamed [ 'httpServers', '' ] 24 | 12 verbose addNamed [ null, '*' ] 25 | 13 silly lockFile 6632676d-httpServers httpServers@ 26 | 14 verbose lock httpServers@ /Users/thomasburleson/.npm/6632676d-httpServers.lock 27 | 15 silly addNameRange { name: 'httpServers', range: '*', hasData: false } 28 | 16 verbose url raw httpServers 29 | 17 verbose url resolving [ 'https://registry.npmjs.org/', './httpServers' ] 30 | 18 verbose url resolved https://registry.npmjs.org/httpServers 31 | 19 info trying registry request attempt 1 at 16:46:25 32 | 20 http GET https://registry.npmjs.org/httpServers 33 | 21 http 404 https://registry.npmjs.org/httpServers 34 | 22 silly registry.get cb [ 404, 35 | 22 silly registry.get { date: 'Wed, 29 Jan 2014 22:46:03 GMT', 36 | 22 silly registry.get server: 'CouchDB/1.5.0 (Erlang OTP/R15B03)', 37 | 22 silly registry.get 'content-type': 'application/json', 38 | 22 silly registry.get 'cache-control': 'max-age=0', 39 | 22 silly registry.get 'content-length': '52', 40 | 22 silly registry.get 'accept-ranges': 'bytes', 41 | 22 silly registry.get via: '1.1 varnish', 42 | 22 silly registry.get age: '0', 43 | 22 silly registry.get 'x-served-by': 'cache-c48-CHI', 44 | 22 silly registry.get 'x-cache': 'MISS', 45 | 22 silly registry.get 'x-cache-hits': '0', 46 | 22 silly registry.get 'x-timer': 'S1391035563.606622934,VS0,VE37', 47 | 22 silly registry.get 'keep-alive': 'timeout=10, max=50', 48 | 22 silly registry.get connection: 'Keep-Alive' } ] 49 | 23 silly lockFile 6632676d-httpServers httpServers@ 50 | 24 silly lockFile 6632676d-httpServers httpServers@ 51 | 25 error 404 'httpServers' is not in the npm registry. 52 | 25 error 404 You should bug the author to publish it 53 | 25 error 404 54 | 25 error 404 Note that you can also install from a 55 | 25 error 404 tarball, folder, or http url, or git url. 56 | 26 error System Darwin 13.0.2 57 | 27 error command "node" "/usr/local/bin/npm" "install" "httpServers" 58 | 28 error cwd /Users/thomasburleson/Documents/dev/javascript/samples/angular/angularjs-Quizzler/tools/webserver 59 | 29 error node -v v0.10.24 60 | 30 error npm -v 1.3.21 61 | 31 error code E404 62 | 32 verbose exit [ 1, true ] 63 | -------------------------------------------------------------------------------- /tools/webserver/run.coffee: -------------------------------------------------------------------------------- 1 | # ************************************************ 2 | # Build HTTP and HTTP_PROXY servers 3 | # 4 | # Note: to debug Node.js scripts, 5 | # see https://github.com/dannycoates/node-inspector 6 | # 7 | # Copyright 2012 Mindspace, LLC. 8 | # ************************************************ 9 | 10 | # Include the HTTP and HTTP Proxy classes 11 | # @see http://nodejs.org/docs/v0.4.2/api/modules.html 12 | # Routes all `matching` calls from local_port to remote_port. Only urls matching the 13 | # 14 | ext = require('httpServers' ) 15 | fs = require( 'fs' ) 16 | 17 | 18 | # Main application 19 | # 20 | main = (options) -> 21 | 22 | options ||= { 23 | 24 | 'proxy_enabled': false 25 | 'proxy_regexp' : /^\/app\/api/ 26 | 27 | 'local_port' : 8000 28 | 'local_host' : '127.0.0.1' 29 | 30 | 'remote_port' : 8080 31 | 'remote_host' : '166.78.24.115' 32 | 33 | # Only used to explicity define the local, hidden web server port 34 | #'silent_port' : 8000 35 | 36 | } 37 | 38 | # Primary server, proxies specific GETs to remote web 39 | # server or to local web server 40 | new ext.HttpProxyServer() .start( options ) 41 | 42 | return 43 | 44 | # Auto-start 45 | # 46 | main() --------------------------------------------------------------------------------