From 2556c2943845f7d1db27d57a02129b5be7ff6c88 Mon Sep 17 00:00:00 2001 From: Hugo Bessa Date: Wed, 17 Jun 2015 18:24:24 -0300 Subject: [PATCH 1/4] Adicionando a lightcalc --- lightcalc | 1 + 1 file changed, 1 insertion(+) create mode 160000 lightcalc diff --git a/lightcalc b/lightcalc new file mode 160000 index 0000000..20be75d --- /dev/null +++ b/lightcalc @@ -0,0 +1 @@ +Subproject commit 20be75dd788ffbc469dcb2f796130e28ea227087 From 176a40bb60b3db289e3a99bc7e3d5ca2e77f1208 Mon Sep 17 00:00:00 2001 From: Hugo Bessa Date: Wed, 17 Jun 2015 18:33:36 -0300 Subject: [PATCH 2/4] Deletando light calc --- lightcalc | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lightcalc diff --git a/lightcalc b/lightcalc deleted file mode 160000 index 20be75d..0000000 --- a/lightcalc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 20be75dd788ffbc469dcb2f796130e28ea227087 From 835a3669ed1feab4cb219713d9fb300b2d288d9a Mon Sep 17 00:00:00 2001 From: Hugo Bessa Date: Wed, 17 Jun 2015 18:35:24 -0300 Subject: [PATCH 3/4] Adicionando light calc --- lightcalc/.app.js | 1 + lightcalc/.bowerrc | 3 + lightcalc/.editorconfig | 10 + lightcalc/.foreverignore | 1 + lightcalc/.gitignore | 118 ++ lightcalc/.sailsrc | 5 + lightcalc/Gruntfile.js | 81 ++ lightcalc/Procfile | 1 + lightcalc/README.md | 37 + lightcalc/api/controllers/.gitkeep | 0 .../api/controllers/CalculatorController.js | 11 + .../api/controllers/OperationController.js | 11 + lightcalc/api/models/.gitkeep | 0 lightcalc/api/models/Calculator.js | 24 + lightcalc/api/models/Operation.js | 22 + lightcalc/api/policies/sessionAuth.js | 21 + lightcalc/api/responses/badRequest.js | 64 + lightcalc/api/responses/forbidden.js | 77 ++ lightcalc/api/responses/notFound.js | 82 ++ lightcalc/api/responses/ok.js | 48 + lightcalc/api/responses/serverError.js | 77 ++ lightcalc/api/services/.gitkeep | 0 lightcalc/app.js | 59 + lightcalc/assets/favicon.ico | Bin 0 -> 920 bytes lightcalc/assets/images/.gitkeep | 0 lightcalc/assets/images/ciro.png | Bin 0 -> 67003 bytes lightcalc/assets/index.html | 28 + lightcalc/assets/js/app.js | 1 + .../js/controllers/calculatorCreateCtrl.js | 18 + .../js/controllers/calculatorRetrieveCtrl.js | 218 ++++ lightcalc/assets/js/dependencies/sails.io.js | 1033 +++++++++++++++++ lightcalc/assets/js/directives/isNameFree.js | 30 + lightcalc/assets/js/routes.js | 12 + lightcalc/assets/js/services/calculators.js | 7 + lightcalc/assets/js/services/operations.js | 3 + .../assets/js/tests/CalculatorsCreate.test.js | 129 ++ .../assets/js/tests/OperationsCreate.test.js | 213 ++++ .../assets/js/tests/OperationsList.test.js | 67 ++ .../assets/js/tests/OperationsLoad.test.js | 102 ++ lightcalc/assets/robots.txt | 8 + lightcalc/assets/styles/core.less | 206 ++++ lightcalc/assets/templates/.gitkeep | 0 .../assets/templates/calculator/create.html | 27 + .../assets/templates/calculator/retrieve.html | 38 + lightcalc/bower.json | 26 + lightcalc/config/blueprints.js | 162 +++ lightcalc/config/bootstrap.js | 17 + lightcalc/config/connections.js | 94 ++ lightcalc/config/cors.js | 78 ++ lightcalc/config/csrf.js | 64 + lightcalc/config/env/development.js | 25 + lightcalc/config/env/production.js | 39 + lightcalc/config/globals.js | 63 + lightcalc/config/http.js | 87 ++ lightcalc/config/i18n.js | 57 + lightcalc/config/locales/_README.md | 28 + lightcalc/config/locales/de.json | 4 + lightcalc/config/locales/en.json | 4 + lightcalc/config/locales/es.json | 4 + lightcalc/config/locales/fr.json | 4 + lightcalc/config/log.js | 29 + lightcalc/config/models.js | 32 + lightcalc/config/policies.js | 51 + lightcalc/config/routes.js | 49 + lightcalc/config/session.js | 91 ++ lightcalc/config/sockets.js | 141 +++ lightcalc/config/views.js | 95 ++ lightcalc/karma.conf.js | 97 ++ lightcalc/package.json | 55 + lightcalc/tasks/README.md | 54 + lightcalc/tasks/config/clean.js | 21 + lightcalc/tasks/config/coffee.js | 38 + lightcalc/tasks/config/concat.js | 27 + lightcalc/tasks/config/copy.js | 38 + lightcalc/tasks/config/cssmin.js | 21 + lightcalc/tasks/config/jst.js | 41 + lightcalc/tasks/config/less.js | 28 + lightcalc/tasks/config/sails-linker.js | 267 +++++ lightcalc/tasks/config/sync.js | 27 + lightcalc/tasks/config/uglify.js | 22 + lightcalc/tasks/config/watch.js | 34 + lightcalc/tasks/pipeline.js | 77 ++ lightcalc/tasks/register/build.js | 8 + lightcalc/tasks/register/buildProd.js | 12 + lightcalc/tasks/register/compileAssets.js | 9 + lightcalc/tasks/register/default.js | 3 + lightcalc/tasks/register/linkAssets.js | 10 + lightcalc/tasks/register/linkAssetsBuild.js | 10 + .../tasks/register/linkAssetsBuildProd.js | 10 + lightcalc/tasks/register/prod.js | 14 + lightcalc/tasks/register/syncAssets.js | 8 + lightcalc/test/bootstrap.test.js | 46 + lightcalc/test/readme | 48 + .../controllers/CalculatorsController.test.js | 93 ++ .../controllers/OperationsController.test.js | 92 ++ lightcalc/views/403.ejs | 76 ++ lightcalc/views/404.ejs | 76 ++ lightcalc/views/500.ejs | 81 ++ lightcalc/views/home/index.ejs | 3 + lightcalc/views/layout.ejs | 109 ++ 100 files changed, 5692 insertions(+) create mode 100644 lightcalc/.app.js create mode 100644 lightcalc/.bowerrc create mode 100644 lightcalc/.editorconfig create mode 100644 lightcalc/.foreverignore create mode 100644 lightcalc/.gitignore create mode 100644 lightcalc/.sailsrc create mode 100644 lightcalc/Gruntfile.js create mode 100644 lightcalc/Procfile create mode 100644 lightcalc/README.md create mode 100644 lightcalc/api/controllers/.gitkeep create mode 100644 lightcalc/api/controllers/CalculatorController.js create mode 100644 lightcalc/api/controllers/OperationController.js create mode 100644 lightcalc/api/models/.gitkeep create mode 100644 lightcalc/api/models/Calculator.js create mode 100644 lightcalc/api/models/Operation.js create mode 100644 lightcalc/api/policies/sessionAuth.js create mode 100644 lightcalc/api/responses/badRequest.js create mode 100644 lightcalc/api/responses/forbidden.js create mode 100644 lightcalc/api/responses/notFound.js create mode 100644 lightcalc/api/responses/ok.js create mode 100644 lightcalc/api/responses/serverError.js create mode 100644 lightcalc/api/services/.gitkeep create mode 100644 lightcalc/app.js create mode 100644 lightcalc/assets/favicon.ico create mode 100644 lightcalc/assets/images/.gitkeep create mode 100644 lightcalc/assets/images/ciro.png create mode 100644 lightcalc/assets/index.html create mode 100644 lightcalc/assets/js/app.js create mode 100644 lightcalc/assets/js/controllers/calculatorCreateCtrl.js create mode 100644 lightcalc/assets/js/controllers/calculatorRetrieveCtrl.js create mode 100644 lightcalc/assets/js/dependencies/sails.io.js create mode 100644 lightcalc/assets/js/directives/isNameFree.js create mode 100644 lightcalc/assets/js/routes.js create mode 100644 lightcalc/assets/js/services/calculators.js create mode 100644 lightcalc/assets/js/services/operations.js create mode 100644 lightcalc/assets/js/tests/CalculatorsCreate.test.js create mode 100644 lightcalc/assets/js/tests/OperationsCreate.test.js create mode 100644 lightcalc/assets/js/tests/OperationsList.test.js create mode 100644 lightcalc/assets/js/tests/OperationsLoad.test.js create mode 100644 lightcalc/assets/robots.txt create mode 100644 lightcalc/assets/styles/core.less create mode 100644 lightcalc/assets/templates/.gitkeep create mode 100644 lightcalc/assets/templates/calculator/create.html create mode 100644 lightcalc/assets/templates/calculator/retrieve.html create mode 100644 lightcalc/bower.json create mode 100644 lightcalc/config/blueprints.js create mode 100644 lightcalc/config/bootstrap.js create mode 100644 lightcalc/config/connections.js create mode 100644 lightcalc/config/cors.js create mode 100644 lightcalc/config/csrf.js create mode 100644 lightcalc/config/env/development.js create mode 100644 lightcalc/config/env/production.js create mode 100644 lightcalc/config/globals.js create mode 100644 lightcalc/config/http.js create mode 100644 lightcalc/config/i18n.js create mode 100644 lightcalc/config/locales/_README.md create mode 100644 lightcalc/config/locales/de.json create mode 100644 lightcalc/config/locales/en.json create mode 100644 lightcalc/config/locales/es.json create mode 100644 lightcalc/config/locales/fr.json create mode 100644 lightcalc/config/log.js create mode 100644 lightcalc/config/models.js create mode 100644 lightcalc/config/policies.js create mode 100644 lightcalc/config/routes.js create mode 100644 lightcalc/config/session.js create mode 100644 lightcalc/config/sockets.js create mode 100644 lightcalc/config/views.js create mode 100644 lightcalc/karma.conf.js create mode 100644 lightcalc/package.json create mode 100644 lightcalc/tasks/README.md create mode 100644 lightcalc/tasks/config/clean.js create mode 100644 lightcalc/tasks/config/coffee.js create mode 100644 lightcalc/tasks/config/concat.js create mode 100644 lightcalc/tasks/config/copy.js create mode 100644 lightcalc/tasks/config/cssmin.js create mode 100644 lightcalc/tasks/config/jst.js create mode 100644 lightcalc/tasks/config/less.js create mode 100644 lightcalc/tasks/config/sails-linker.js create mode 100644 lightcalc/tasks/config/sync.js create mode 100644 lightcalc/tasks/config/uglify.js create mode 100644 lightcalc/tasks/config/watch.js create mode 100644 lightcalc/tasks/pipeline.js create mode 100644 lightcalc/tasks/register/build.js create mode 100644 lightcalc/tasks/register/buildProd.js create mode 100644 lightcalc/tasks/register/compileAssets.js create mode 100644 lightcalc/tasks/register/default.js create mode 100644 lightcalc/tasks/register/linkAssets.js create mode 100644 lightcalc/tasks/register/linkAssetsBuild.js create mode 100644 lightcalc/tasks/register/linkAssetsBuildProd.js create mode 100644 lightcalc/tasks/register/prod.js create mode 100644 lightcalc/tasks/register/syncAssets.js create mode 100644 lightcalc/test/bootstrap.test.js create mode 100644 lightcalc/test/readme create mode 100644 lightcalc/test/unit/controllers/CalculatorsController.test.js create mode 100644 lightcalc/test/unit/controllers/OperationsController.test.js create mode 100644 lightcalc/views/403.ejs create mode 100644 lightcalc/views/404.ejs create mode 100644 lightcalc/views/500.ejs create mode 100644 lightcalc/views/home/index.ejs create mode 100644 lightcalc/views/layout.ejs diff --git a/lightcalc/.app.js b/lightcalc/.app.js new file mode 100644 index 0000000..456cb13 --- /dev/null +++ b/lightcalc/.app.js @@ -0,0 +1 @@ +require('sails').lift(); diff --git a/lightcalc/.bowerrc b/lightcalc/.bowerrc new file mode 100644 index 0000000..b459ca8 --- /dev/null +++ b/lightcalc/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "assets/bower_components" +} \ No newline at end of file diff --git a/lightcalc/.editorconfig b/lightcalc/.editorconfig new file mode 100644 index 0000000..0f09989 --- /dev/null +++ b/lightcalc/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/lightcalc/.foreverignore b/lightcalc/.foreverignore new file mode 100644 index 0000000..009d521 --- /dev/null +++ b/lightcalc/.foreverignore @@ -0,0 +1 @@ +**/.tmp/** \ No newline at end of file diff --git a/lightcalc/.gitignore b/lightcalc/.gitignore new file mode 100644 index 0000000..a52eaec --- /dev/null +++ b/lightcalc/.gitignore @@ -0,0 +1,118 @@ +################################################ +############### .gitignore ################## +################################################ +# +# This file is only relevant if you are using git. +# +# Files which match the splat patterns below will +# be ignored by git. This keeps random crap and +# sensitive credentials from being uploaded to +# your repository. It allows you to configure your +# app for your machine without accidentally +# committing settings which will smash the local +# settings of other developers on your team. +# +# Some reasonable defaults are included below, +# but, of course, you should modify/extend/prune +# to fit your needs! +################################################ + + + + +################################################ +# Local Configuration +# +# Explicitly ignore files which contain: +# +# 1. Sensitive information you'd rather not push to +# your git repository. +# e.g., your personal API keys or passwords. +# +# 2. Environment-specific configuration +# Basically, anything that would be annoying +# to have to change every time you do a +# `git pull` +# e.g., your local development database, or +# the S3 bucket you're using for file uploads +# development. +# +################################################ + +config/local.js + + + + + +################################################ +# Dependencies +# +# When releasing a production app, you may +# consider including your node_modules and +# bower_components directory in your git repo, +# but during development, its best to exclude it, +# since different developers may be working on +# different kernels, where dependencies would +# need to be recompiled anyway. +# +# More on that here about node_modules dir: +# http://www.futurealoof.com/posts/nodemodules-in-git.html +# (credit Mikeal Rogers, @mikeal) +# +# About bower_components dir, you can see this: +# http://addyosmani.com/blog/checking-in-front-end-dependencies/ +# (credit Addy Osmani, @addyosmani) +# +################################################ + +node_modules +bower_components + + + + +################################################ +# Sails.js / Waterline / Grunt +# +# Files generated by Sails and Grunt, or related +# tasks and adapters. +################################################ +.tmp +dump.rdb + + + + + +################################################ +# Node.js / NPM +# +# Common files generated by Node, NPM, and the +# related ecosystem. +################################################ +lib-cov +*.seed +*.log +*.out +*.pid +npm-debug.log + + + + + +################################################ +# Miscellaneous +# +# Common files generated by text editors, +# operating systems, file systems, etc. +################################################ + +*~ +*# +.DS_STORE +.netbeans +nbproject +.idea +.node_history diff --git a/lightcalc/.sailsrc b/lightcalc/.sailsrc new file mode 100644 index 0000000..fa89f5e --- /dev/null +++ b/lightcalc/.sailsrc @@ -0,0 +1,5 @@ +{ + "generators": { + "modules": {} + } +} \ No newline at end of file diff --git a/lightcalc/Gruntfile.js b/lightcalc/Gruntfile.js new file mode 100644 index 0000000..f4b2289 --- /dev/null +++ b/lightcalc/Gruntfile.js @@ -0,0 +1,81 @@ +/** + * Gruntfile + * + * This Node script is executed when you run `grunt` or `sails lift`. + * It's purpose is to load the Grunt tasks in your project's `tasks` + * folder, and allow you to add and remove tasks as you see fit. + * For more information on how this works, check out the `README.md` + * file that was generated in your `tasks` folder. + * + * WARNING: + * Unless you know what you're doing, you shouldn't change this file. + * Check out the `tasks` directory instead. + */ + +module.exports = function(grunt) { + + + // Load the include-all library in order to require all of our grunt + // configurations and task registrations dynamically. + var includeAll; + try { + includeAll = require('include-all'); + } catch (e0) { + try { + includeAll = require('sails/node_modules/include-all'); + } + catch(e1) { + console.error('Could not find `include-all` module.'); + console.error('Skipping grunt tasks...'); + console.error('To fix this, please run:'); + console.error('npm install include-all --save`'); + console.error(); + + grunt.registerTask('default', []); + return; + } + } + + + /** + * Loads Grunt configuration modules from the specified + * relative path. These modules should export a function + * that, when run, should either load/configure or register + * a Grunt task. + */ + function loadTasks(relPath) { + return includeAll({ + dirname: require('path').resolve(__dirname, relPath), + filter: /(.+)\.js$/ + }) || {}; + } + + /** + * Invokes the function from a Grunt configuration module with + * a single argument - the `grunt` object. + */ + function invokeConfigFn(tasks) { + for (var taskName in tasks) { + if (tasks.hasOwnProperty(taskName)) { + tasks[taskName](grunt); + } + } + } + + + + + // Load task functions + var taskConfigurations = loadTasks('./tasks/config'), + registerDefinitions = loadTasks('./tasks/register'); + + // (ensure that a default task exists) + if (!registerDefinitions.default) { + registerDefinitions.default = function (grunt) { grunt.registerTask('default', []); }; + } + + // Run task functions to configure Grunt. + invokeConfigFn(taskConfigurations); + invokeConfigFn(registerDefinitions); + +}; diff --git a/lightcalc/Procfile b/lightcalc/Procfile new file mode 100644 index 0000000..207d22f --- /dev/null +++ b/lightcalc/Procfile @@ -0,0 +1 @@ +web: node app.js \ No newline at end of file diff --git a/lightcalc/README.md b/lightcalc/README.md new file mode 100644 index 0000000..92ae7b1 --- /dev/null +++ b/lightcalc/README.md @@ -0,0 +1,37 @@ +# lightcalc + +a [Sails](http://sailsjs.org) application that uses angularjs to make beautiful and light calculators with isolates operations histories. + +## Running application + +Install nodejs + +In the application folder, run "npm install" with the required permissions. + +OBS.: in Ubuntu, you haave to run "sudo npm install", for example + +After npm install all the project dependencies, run "bower install" to download the frontend depencies. + +For last, run "sails lift", and then the application will be running at http://localhost:1337. + + +## Running tests + +There are two sets of tests in the application: the backend tests, which test the REST api provided by sails, and the frontend tests, which test the angularjs application. + +### Backend tests + +To run the backend tests you have to install mocha globally running "npm install mocha -g" with the required permissions. + +After mocha is installed run the command "mocha test/bootstrap.test.js test/unit/**.*.test.js" in the project root folder. + +### Frontend tests + +To run the backend tests you have to install karma tests runner globally running "npm install karma -g" with the required permissions. + +After karma is installed run the command "karma start" in the project root folder. + + +## Demo version + +[http://lightcalc.herokuapp.com](http://lightcalc.herokuapp.com) \ No newline at end of file diff --git a/lightcalc/api/controllers/.gitkeep b/lightcalc/api/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lightcalc/api/controllers/CalculatorController.js b/lightcalc/api/controllers/CalculatorController.js new file mode 100644 index 0000000..b595df1 --- /dev/null +++ b/lightcalc/api/controllers/CalculatorController.js @@ -0,0 +1,11 @@ +/** + * CalculatorController + * + * @description :: Server-side logic for managing calculators + * @help :: See http://sailsjs.org/#!/documentation/concepts/Controllers + */ + +module.exports = { + +}; + diff --git a/lightcalc/api/controllers/OperationController.js b/lightcalc/api/controllers/OperationController.js new file mode 100644 index 0000000..cb556ae --- /dev/null +++ b/lightcalc/api/controllers/OperationController.js @@ -0,0 +1,11 @@ +/** + * OperationController + * + * @description :: Server-side logic for managing operations + * @help :: See http://sailsjs.org/#!/documentation/concepts/Controllers + */ + +module.exports = { + +}; + diff --git a/lightcalc/api/models/.gitkeep b/lightcalc/api/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lightcalc/api/models/Calculator.js b/lightcalc/api/models/Calculator.js new file mode 100644 index 0000000..25c8899 --- /dev/null +++ b/lightcalc/api/models/Calculator.js @@ -0,0 +1,24 @@ +/** +* Calculator.js +* +* @description :: TODO: You might write a short summary of how this model works and what it represents here. +* @docs :: http://sailsjs.org/#!documentation/models +*/ + +module.exports = { + attributes: { + name: { + type: 'STRING', + required: true, + unique: true, + alphanumeric: true, + minLength: 8, + maxLength: 40 + }, + operations: { + collection: 'operation', + via: 'calculator' + } + } +}; + diff --git a/lightcalc/api/models/Operation.js b/lightcalc/api/models/Operation.js new file mode 100644 index 0000000..d25cd61 --- /dev/null +++ b/lightcalc/api/models/Operation.js @@ -0,0 +1,22 @@ +/** +* Operation.js +* +* @description :: TODO: You might write a short summary of how this model works and what it represents here. +* @docs :: http://sailsjs.org/#!documentation/models +*/ + +module.exports = { + + attributes: { + calculator: { + model: 'calculator', + required: true + }, + + text: { + type: 'STRING', + required: true, + } + } +}; + diff --git a/lightcalc/api/policies/sessionAuth.js b/lightcalc/api/policies/sessionAuth.js new file mode 100644 index 0000000..8f9a264 --- /dev/null +++ b/lightcalc/api/policies/sessionAuth.js @@ -0,0 +1,21 @@ +/** + * sessionAuth + * + * @module :: Policy + * @description :: Simple policy to allow any authenticated user + * Assumes that your login action in one of your controllers sets `req.session.authenticated = true;` + * @docs :: http://sailsjs.org/#!/documentation/concepts/Policies + * + */ +module.exports = function(req, res, next) { + + // User is allowed, proceed to the next policy, + // or if this is the last policy, the controller + if (req.session.authenticated) { + return next(); + } + + // User is not allowed + // (default res.forbidden() behavior can be overridden in `config/403.js`) + return res.forbidden('You are not permitted to perform this action.'); +}; diff --git a/lightcalc/api/responses/badRequest.js b/lightcalc/api/responses/badRequest.js new file mode 100644 index 0000000..9f99b13 --- /dev/null +++ b/lightcalc/api/responses/badRequest.js @@ -0,0 +1,64 @@ +/** + * 400 (Bad Request) Handler + * + * Usage: + * return res.badRequest(); + * return res.badRequest(data); + * return res.badRequest(data, 'some/specific/badRequest/view'); + * + * e.g.: + * ``` + * return res.badRequest( + * 'Please choose a valid `password` (6-12 characters)', + * 'trial/signup' + * ); + * ``` + */ + +module.exports = function badRequest(data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(400); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); + } + else sails.log.verbose('Sending 400 ("Bad Request") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production') { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + if (req.wantsJSON) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: data }); + } + + // If no second argument provided, try to serve the implied view, + // but fall back to sending JSON(P) if no view can be inferred. + else return res.guessView({ data: data }, function couldNotGuessView () { + return res.jsonx(data); + }); + +}; + diff --git a/lightcalc/api/responses/forbidden.js b/lightcalc/api/responses/forbidden.js new file mode 100644 index 0000000..16e4d81 --- /dev/null +++ b/lightcalc/api/responses/forbidden.js @@ -0,0 +1,77 @@ +/** + * 403 (Forbidden) Handler + * + * Usage: + * return res.forbidden(); + * return res.forbidden(err); + * return res.forbidden(err, 'some/specific/forbidden/view'); + * + * e.g.: + * ``` + * return res.forbidden('Access denied.'); + * ``` + */ + +module.exports = function forbidden (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(403); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); + } + else sails.log.verbose('Sending 403 ("Forbidden") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production') { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + if (req.wantsJSON) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: data }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('403', { data: data }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/lightcalc/api/responses/notFound.js b/lightcalc/api/responses/notFound.js new file mode 100644 index 0000000..d14a200 --- /dev/null +++ b/lightcalc/api/responses/notFound.js @@ -0,0 +1,82 @@ +/** + * 404 (Not Found) Handler + * + * Usage: + * return res.notFound(); + * return res.notFound(err); + * return res.notFound(err, 'some/specific/notfound/view'); + * + * e.g.: + * ``` + * return res.notFound(); + * ``` + * + * NOTE: + * If a request doesn't match any explicit routes (i.e. `config/routes.js`) + * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` + * automatically. + */ + +module.exports = function notFound (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(404); + + // Log error to console + if (data !== undefined) { + sails.log.verbose('Sending 404 ("Not Found") response: \n',data); + } + else sails.log.verbose('Sending 404 ("Not Found") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production') { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + if (req.wantsJSON) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: data }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('404', { data: data }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/lightcalc/api/responses/ok.js b/lightcalc/api/responses/ok.js new file mode 100644 index 0000000..4351d2e --- /dev/null +++ b/lightcalc/api/responses/ok.js @@ -0,0 +1,48 @@ +/** + * 200 (OK) Response + * + * Usage: + * return res.ok(); + * return res.ok(data); + * return res.ok(data, 'auth/login'); + * + * @param {Object} data + * @param {String|Object} options + * - pass string to render specified view + */ + +module.exports = function sendOK (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + sails.log.silly('res.ok() :: Sending 200 ("OK") response'); + + // Set status code + res.status(200); + + // If appropriate, serve data as JSON(P) + if (req.wantsJSON) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: data }); + } + + // If no second argument provided, try to serve the implied view, + // but fall back to sending JSON(P) if no view can be inferred. + else return res.guessView({ data: data }, function couldNotGuessView () { + return res.jsonx(data); + }); + +}; diff --git a/lightcalc/api/responses/serverError.js b/lightcalc/api/responses/serverError.js new file mode 100644 index 0000000..e08b4ce --- /dev/null +++ b/lightcalc/api/responses/serverError.js @@ -0,0 +1,77 @@ +/** + * 500 (Server Error) Response + * + * Usage: + * return res.serverError(); + * return res.serverError(err); + * return res.serverError(err, 'some/specific/error/view'); + * + * NOTE: + * If something throws in a policy or controller, or an internal + * error is encountered, Sails will call `res.serverError()` + * automatically. + */ + +module.exports = function serverError (data, options) { + + // Get access to `req`, `res`, & `sails` + var req = this.req; + var res = this.res; + var sails = req._sails; + + // Set status code + res.status(500); + + // Log error to console + if (data !== undefined) { + sails.log.error('Sending 500 ("Server Error") response: \n',data); + } + else sails.log.error('Sending empty 500 ("Server Error") response'); + + // Only include errors in response if application environment + // is not set to 'production'. In production, we shouldn't + // send back any identifying information about errors. + if (sails.config.environment === 'production') { + data = undefined; + } + + // If the user-agent wants JSON, always respond with JSON + if (req.wantsJSON) { + return res.jsonx(data); + } + + // If second argument is a string, we take that to mean it refers to a view. + // If it was omitted, use an empty object (`{}`) + options = (typeof options === 'string') ? { view: options } : options || {}; + + // If a view was provided in options, serve it. + // Otherwise try to guess an appropriate view, or if that doesn't + // work, just send JSON. + if (options.view) { + return res.view(options.view, { data: data }); + } + + // If no second argument provided, try to serve the default view, + // but fall back to sending JSON(P) if any errors occur. + else return res.view('500', { data: data }, function (err, html) { + + // If a view error occured, fall back to JSON(P). + if (err) { + // + // Additionally: + // • If the view was missing, ignore the error but provide a verbose log. + if (err.code === 'E_VIEW_FAILED') { + sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ',err); + } + // Otherwise, if this was a more serious error, log to the console with the details. + else { + sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); + } + return res.jsonx(data); + } + + return res.send(html); + }); + +}; + diff --git a/lightcalc/api/services/.gitkeep b/lightcalc/api/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lightcalc/app.js b/lightcalc/app.js new file mode 100644 index 0000000..85c0af7 --- /dev/null +++ b/lightcalc/app.js @@ -0,0 +1,59 @@ +/** + * app.js + * + * Use `app.js` to run your app without `sails lift`. + * To start the server, run: `node app.js`. + * + * This is handy in situations where the sails CLI is not relevant or useful. + * + * For example: + * => `node app.js` + * => `forever start app.js` + * => `node debug app.js` + * => `modulus deploy` + * => `heroku scale` + * + * + * The same command-line arguments are supported, e.g.: + * `node app.js --silent --port=80 --prod` + */ + +// Ensure we're in the project directory, so relative paths work as expected +// no matter where we actually lift from. +process.chdir(__dirname); + +// Ensure a "sails" can be located: +(function() { + var sails; + try { + sails = require('sails'); + } catch (e) { + console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); + console.error('To do that, run `npm install sails`'); + console.error(''); + console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); + console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); + console.error('but if it doesn\'t, the app will run with the global sails instead!'); + return; + } + + // Try to get `rc` dependency + var rc; + try { + rc = require('rc'); + } catch (e0) { + try { + rc = require('sails/node_modules/rc'); + } catch (e1) { + console.error('Could not find dependency: `rc`.'); + console.error('Your `.sailsrc` file(s) will be ignored.'); + console.error('To resolve this, run:'); + console.error('npm install rc --save'); + rc = function () { return {}; }; + } + } + + + // Start server + sails.lift(rc('sails')); +})(); diff --git a/lightcalc/assets/favicon.ico b/lightcalc/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0092ec9cace7b2918865c11aea9c1ab1d84897f5 GIT binary patch literal 920 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 + + + + LightCalc + + + + +
+ +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lightcalc/assets/js/app.js b/lightcalc/assets/js/app.js new file mode 100644 index 0000000..202bdee --- /dev/null +++ b/lightcalc/assets/js/app.js @@ -0,0 +1 @@ +angular.module('LightCalc', ['ngRoute', 'ngResource', 'ngMessages', 'ui.bootstrap.showErrors']) \ No newline at end of file diff --git a/lightcalc/assets/js/controllers/calculatorCreateCtrl.js b/lightcalc/assets/js/controllers/calculatorCreateCtrl.js new file mode 100644 index 0000000..d8755fa --- /dev/null +++ b/lightcalc/assets/js/controllers/calculatorCreateCtrl.js @@ -0,0 +1,18 @@ +angular.module('LightCalc').controller('calculatorCreateCtrl', ['Calculators', '$scope', '$location', function(Calculators, $scope, $location){ + + $scope.calculatorForm = {name: ''}; + + $scope.submitForm = function(){ + $scope.$broadcast('show-errors-check-validity'); + + if ($scope.form.$valid) { + var calculator = Calculators.save($scope.calculatorForm); + calculator.$promise.then(function(data){ + $location.path('/'+data.name); + }); + + return calculator; + } + } + +}]); diff --git a/lightcalc/assets/js/controllers/calculatorRetrieveCtrl.js b/lightcalc/assets/js/controllers/calculatorRetrieveCtrl.js new file mode 100644 index 0000000..f3e4dea --- /dev/null +++ b/lightcalc/assets/js/controllers/calculatorRetrieveCtrl.js @@ -0,0 +1,218 @@ +angular.module('LightCalc').controller('calculatorRetrieveCtrl', ['Calculators', 'Operations', '$scope', '$routeParams', '$location', function(Calculators, Operations, $scope, $routeParams, $location){ +/* Controller handles calculations and binding*/ + var calculators = Calculators.get({name: $routeParams.name}); + calculators.$promise.then(function(data){ + if(calculators.length > 0){ + $scope.calculator = calculators[0]; + } else { + $location.path('/'); + } + }); + // Bound to the output display + + $scope.output = "0"; + + var newNumber = true; + + var partialNumber = ""; + var tokens = []; + + /* + * Runs every time a number button is clicked. + * Updates the output display and sets + * newNumber flag + */ + + var updateOutput = function(){ + var i = 0; + $scope.output = ""; + if(tokens.length > 0) { + if(tokens[0].type === 'operator'){ + i = 1; + tokens[1].value = -Math.abs(tokens[1].value); + } + + for (; i < tokens.length; i++){ + $scope.output += tokens[i].value; + if(i < tokens.length - 1 || partialNumber.length > 0){ + $scope.output += ' '; + } + } + } + + if(partialNumber.length > 0){ + $scope.output += partialNumber; + } + } + + var doBinaryOperation = function(symbol){ + newNumber = false; + if(partialNumber.length > 0){ + tokens.push({type: 'number', value: parseFloat(partialNumber)}); + partialNumber = ''; + } + if(tokens.length === 0){ + throw "Você tem que apertar algum dígito antes de realizar uma operação"; + } else if(tokens.length > 0){ + if(tokens[tokens.length - 1].type === 'number'){ + tokens.push({type: 'operator', value: symbol}); + } else { + tokens[tokens.length - 1].value = symbol; + } + updateOutput(); + } + } + + var calculate = function() { + var i, j, noDivMultTokens = [], value = 0; + + if(tokens[0].type === 'operator'){ + i = 1; + tokens[1].value = -Math.abs(tokens[1].value); + } else { + i = 0; + } + + j = i; + + for(;i < tokens.length; i++){ + if(tokens[i].value === '÷'){ + noDivMultTokens.pop(); + if(tokens[i+1] !== 0){ + noDivMultTokens.push({ + type:'number', + value: (tokens[i-1].value / tokens[i+1].value) + }); + i++; + } else { + throw "Divisão por zero"; + } + } else if(tokens[i].value === 'x'){ + noDivMultTokens.pop(); + noDivMultTokens.push({ + type:'number', + value: (tokens[i-1].value * tokens[i+1].value) + }); + i++; + } else { + noDivMultTokens.push(tokens[i]); + } + } + + value = noDivMultTokens[j].value; + + for(; j < noDivMultTokens.length; j++){ + if(noDivMultTokens[j].type === 'operator'){ + if(noDivMultTokens[j].value === '+'){ + value = value + noDivMultTokens[j+1].value; + } else { + value = value - noDivMultTokens[j+1].value; + } + } + } + + return value; + }; + + $scope.putDigit = function(btn) { + if(newNumber) + tokens = []; + if((tokens.length === 0 && partialNumber.length === 0)|| newNumber) { + if (btn === '.') + partialNumber = '0.'; + else + partialNumber = String(btn); + newNumber = false; + } else { + if(partialNumber.length === 0 && btn === '.') + partialNumber = '0.'; + else + partialNumber += String(btn); + } + + updateOutput(); + }; + + $scope.add = function() { + doBinaryOperation('+'); + }; + + $scope.subtract = function() { + if(tokens.length === 0 && partialNumber.length === 0){ + $scope.putDigit('-'); + } else { + doBinaryOperation('-'); + } + }; + + $scope.divide = function() { + doBinaryOperation('÷'); + }; + + $scope.multiply = function() { + doBinaryOperation('x'); + }; + + $scope.equals = function(){ + if(partialNumber.length > 0){ + tokens.push({type: 'number', value: parseFloat(partialNumber)}); + partialNumber = ''; + } + + var operation = Operations.save({ + calculator: $scope.calculator.id, + text: $scope.output, + }); + + operation.$promise.then(function(data){ + tokens = [{type: 'number', value: calculate()}]; + updateOutput(); + newNumber = true; + $scope.calculator.operations.push(data); + }).catch(function(){ + tokens = [{type: 'number', value: calculate()}]; + updateOutput(); + newNumber = true; + throw "Não foi possível salvar a operação. Verifique sua conexão com a internet."; + }); + + return operation; + } + + $scope.loadOperation = function(operation){ + tokens = []; + partialNumber = ''; + var operationTokens = operation.text.split(' '); + + for(var i in operationTokens){ + switch(operationTokens[i]){ + case '+': + tokens.push({type: 'operator', value: operationTokens[i]}); + break; + case '-': + tokens.push({type: 'operator', value: operationTokens[i]}); + break; + case 'x': + tokens.push({type: 'operator', value: operationTokens[i]}); + break; + case '÷': + tokens.push({type: 'operator', value: operationTokens[i]}); + break; + default: + tokens.push({type: 'number', value: parseFloat(operationTokens[i])}); + } + } + + updateOutput(); + } + + /* + * Initializes the appropriate values + * when the clear button is clicked. + */ + $scope.clear = function() { + tokens = []; + newNumber = true; + $scope.output = "0"; + }; +}]); \ No newline at end of file diff --git a/lightcalc/assets/js/dependencies/sails.io.js b/lightcalc/assets/js/dependencies/sails.io.js new file mode 100644 index 0000000..e05e36b --- /dev/null +++ b/lightcalc/assets/js/dependencies/sails.io.js @@ -0,0 +1,1033 @@ +// socket.io-1.2.1 +// (from http://socket.io/download/) +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.io=e()}}(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&!this.encoding){var pack=this.packetBuffer.shift();this.packet(pack)}};Manager.prototype.cleanup=function(){var sub;while(sub=this.subs.shift())sub.destroy();this.packetBuffer=[];this.encoding=false;this.decoder.destroy()};Manager.prototype.close=Manager.prototype.disconnect=function(){this.skipReconnect=true;this.readyState="closed";this.engine&&this.engine.close()};Manager.prototype.onclose=function(reason){debug("close");this.cleanup();this.readyState="closed";this.emit("close",reason);if(this._reconnection&&!this.skipReconnect){this.reconnect()}};Manager.prototype.reconnect=function(){if(this.reconnecting||this.skipReconnect)return this;var self=this;this.attempts++;if(this.attempts>this._reconnectionAttempts){debug("reconnect failed");this.emitAll("reconnect_failed");this.reconnecting=false}else{var delay=this.attempts*this.reconnectionDelay();delay=Math.min(delay,this.reconnectionDelayMax());debug("will wait %dms before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){if(self.skipReconnect)return;debug("attempting reconnect");self.emitAll("reconnect_attempt",self.attempts);self.emitAll("reconnecting",self.attempts);if(self.skipReconnect)return;self.open(function(err){if(err){debug("reconnect attempt error");self.reconnecting=false;self.reconnect();self.emitAll("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.attempts;this.attempts=0;this.reconnecting=false;this.emitAll("reconnect",attempt)}},{"./on":4,"./socket":5,"./url":6,"component-bind":7,"component-emitter":8,debug:9,"engine.io-client":10,indexof:39,"object-component":40,"socket.io-parser":43}],4:[function(_dereq_,module,exports){module.exports=on;function on(obj,ev,fn){obj.on(ev,fn);return{destroy:function(){obj.removeListener(ev,fn)}}}},{}],5:[function(_dereq_,module,exports){var parser=_dereq_("socket.io-parser");var Emitter=_dereq_("component-emitter");var toArray=_dereq_("to-array");var on=_dereq_("./on");var bind=_dereq_("component-bind");var debug=_dereq_("debug")("socket.io-client:socket");var hasBin=_dereq_("has-binary");module.exports=exports=Socket;var events={connect:1,connect_error:1,connect_timeout:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1};var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};if(this.io.autoConnect)this.open();this.receiveBuffer=[];this.sendBuffer=[];this.connected=false;this.disconnected=true}Emitter(Socket.prototype);Socket.prototype.subEvents=function(){if(this.subs)return;var io=this.io;this.subs=[on(io,"open",bind(this,"onopen")),on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose"))]};Socket.prototype.open=Socket.prototype.connect=function(){if(this.connected)return this;this.subEvents();this.io.open();if("open"==this.io.readyState)this.onopen();return this};Socket.prototype.send=function(){var args=toArray(arguments);args.unshift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(events.hasOwnProperty(ev)){emit.apply(this,arguments);return this}var args=toArray(arguments);var parserType=parser.EVENT;if(hasBin(args)){parserType=parser.BINARY_EVENT}var packet={type:parserType,data:args};if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}if(this.connected){this.packet(packet)}else{this.sendBuffer.push(packet)}return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.packet(packet)};Socket.prototype.onopen=function(){debug("transport is open - connecting");if("/"!=this.nsp){this.packet({type:parser.CONNECT})}};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.connected=false;this.disconnected=true;this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.BINARY_EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.BINARY_ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(null!=packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.receiveBuffer.push(args)}};Socket.prototype.ack=function(id){var self=this;var sent=false;return function(){if(sent)return;sent=true;var args=toArray(arguments);debug("sending ack %j",args);var type=hasBin(args)?parser.BINARY_ACK:parser.ACK;self.packet({type:type,id:id,data:args})}};Socket.prototype.onack=function(packet){debug("calling ack %s with %j",packet.id,packet.data);var fn=this.acks[packet.id];fn.apply(this,packet.data);delete this.acks[packet.id]};Socket.prototype.onconnect=function(){this.connected=true;this.disconnected=false;this.emit("connect");this.emitBuffered()};Socket.prototype.emitBuffered=function(){var i;for(i=0;i=hour)return(ms/hour).toFixed(1)+"h";if(ms>=min)return(ms/min).toFixed(1)+"m";if(ms>=sec)return(ms/sec|0)+"s";return ms+"ms"};debug.enabled=function(name){for(var i=0,len=debug.skips.length;i';iframe=document.createElement(html)}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId;iframe.src="javascript:0"}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();data=data.replace(rEscapedNewline,"\\\n");this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-inherit":20}],16:[function(_dereq_,module,exports){(function(global){var XMLHttpRequest=_dereq_("xmlhttprequest");var Polling=_dereq_("./polling");var Emitter=_dereq_("component-emitter");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling-xhr");module.exports=XHR;module.exports.Request=Request;function empty(){}function XHR(opts){Polling.call(this,opts);if(global.location){var isSSL="https:"==location.protocol;var port=location.port;if(!port){port=isSSL?443:80}this.xd=opts.hostname!=global.location.hostname||port!=opts.port;this.xs=opts.secure!=isSSL}}inherit(XHR,Polling);XHR.prototype.supportsBinary=true;XHR.prototype.request=function(opts){opts=opts||{};opts.uri=this.uri();opts.xd=this.xd;opts.xs=this.xs;opts.agent=this.agent||false;opts.supportsBinary=this.supportsBinary;opts.enablesXDR=this.enablesXDR;return new Request(opts)};XHR.prototype.doWrite=function(data,fn){var isBinary=typeof data!=="string"&&data!==undefined;var req=this.request({method:"POST",data:data,isBinary:isBinary});var self=this;req.on("success",fn);req.on("error",function(err){self.onError("xhr post error",err)});this.sendXhr=req};XHR.prototype.doPoll=function(){debug("xhr poll");var req=this.request();var self=this;req.on("data",function(data){self.onData(data)});req.on("error",function(err){self.onError("xhr poll error",err)});this.pollXhr=req};function Request(opts){this.method=opts.method||"GET";this.uri=opts.uri;this.xd=!!opts.xd;this.xs=!!opts.xs;this.async=false!==opts.async;this.data=undefined!=opts.data?opts.data:null;this.agent=opts.agent;this.isBinary=opts.isBinary;this.supportsBinary=opts.supportsBinary;this.enablesXDR=opts.enablesXDR;this.create()}Emitter(Request.prototype);Request.prototype.create=function(){var xhr=this.xhr=new XMLHttpRequest({agent:this.agent,xdomain:this.xd,xscheme:this.xs,enablesXDR:this.enablesXDR});var self=this;try{debug("xhr open %s: %s",this.method,this.uri);xhr.open(this.method,this.uri,this.async);if(this.supportsBinary){xhr.responseType="arraybuffer"}if("POST"==this.method){try{if(this.isBinary){xhr.setRequestHeader("Content-type","application/octet-stream")}else{xhr.setRequestHeader("Content-type","text/plain;charset=UTF-8")}}catch(e){}}if("withCredentials"in xhr){xhr.withCredentials=true}if(this.hasXDR()){xhr.onload=function(){self.onLoad()};xhr.onerror=function(){self.onError(xhr.responseText)}}else{xhr.onreadystatechange=function(){if(4!=xhr.readyState)return;if(200==xhr.status||1223==xhr.status){self.onLoad()}else{setTimeout(function(){self.onError(xhr.status)},0)}}}debug("xhr data %s",this.data);xhr.send(this.data)}catch(e){setTimeout(function(){self.onError(e)},0);return}if(global.document){this.index=Request.requestsCount++;Request.requests[this.index]=this}};Request.prototype.onSuccess=function(){this.emit("success");this.cleanup()};Request.prototype.onData=function(data){this.emit("data",data);this.onSuccess()};Request.prototype.onError=function(err){this.emit("error",err);this.cleanup()};Request.prototype.cleanup=function(){if("undefined"==typeof this.xhr||null===this.xhr){return}if(this.hasXDR()){this.xhr.onload=this.xhr.onerror=empty}else{this.xhr.onreadystatechange=empty}try{this.xhr.abort()}catch(e){}if(global.document){delete Request.requests[this.index]}this.xhr=null};Request.prototype.onLoad=function(){var data;try{var contentType;try{contentType=this.xhr.getResponseHeader("Content-Type").split(";")[0]}catch(e){}if(contentType==="application/octet-stream"){data=this.xhr.response}else{if(!this.supportsBinary){data=this.xhr.responseText}else{data="ok"}}}catch(e){this.onError(e)}if(null!=data){this.onData(data)}};Request.prototype.hasXDR=function(){return"undefined"!==typeof global.XDomainRequest&&!this.xs&&this.enablesXDR};Request.prototype.abort=function(){this.cleanup()};if(global.document){Request.requestsCount=0;Request.requests={};if(global.attachEvent){global.attachEvent("onunload",unloadHandler)}else if(global.addEventListener){global.addEventListener("beforeunload",unloadHandler,false)}}function unloadHandler(){for(var i in Request.requests){if(Request.requests.hasOwnProperty(i)){Request.requests[i].abort()}}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./polling":17,"component-emitter":8,"component-inherit":20,debug:21,xmlhttprequest:19}],17:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parseqs=_dereq_("parseqs");var parser=_dereq_("engine.io-parser");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:polling");module.exports=Polling;var hasXHR2=function(){var XMLHttpRequest=_dereq_("xmlhttprequest");var xhr=new XMLHttpRequest({xdomain:false});return null!=xhr.responseType}();function Polling(opts){var forceBase64=opts&&opts.forceBase64;if(!hasXHR2||forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(Polling,Transport);Polling.prototype.name="polling";Polling.prototype.doOpen=function(){this.poll()};Polling.prototype.pause=function(onPause){var pending=0;var self=this;this.readyState="pausing";function pause(){debug("paused");self.readyState="paused";onPause()}if(this.polling||!this.writable){var total=0;if(this.polling){debug("we are currently polling - waiting to pause");total++;this.once("pollComplete",function(){debug("pre-pause polling complete");--total||pause()})}if(!this.writable){debug("we are currently writing - waiting to pause");total++;this.once("drain",function(){debug("pre-pause writing complete");--total||pause()})}}else{pause()}};Polling.prototype.poll=function(){debug("polling");this.polling=true;this.doPoll();this.emit("poll")};Polling.prototype.onData=function(data){var self=this;debug("polling got data %s",data);var callback=function(packet,index,total){if("opening"==self.readyState){self.onOpen()}if("close"==packet.type){self.onClose();return false}self.onPacket(packet)};parser.decodePayload(data,this.socket.binaryType,callback);if("closed"!=this.readyState){this.polling=false;this.emit("pollComplete");if("open"==this.readyState){this.poll()}else{debug('ignoring poll - transport state "%s"',this.readyState)}}};Polling.prototype.doClose=function(){var self=this;function close(){debug("writing close packet");self.write([{type:"close"}])}if("open"==this.readyState){debug("transport open - closing");close()}else{debug("transport not open - deferring close");this.once("open",close)}};Polling.prototype.write=function(packets){var self=this;this.writable=false;var callbackfn=function(){self.writable=true;self.emit("drain")};var self=this;parser.encodePayload(packets,this.supportsBinary,function(data){self.doWrite(data,callbackfn)})};Polling.prototype.uri=function(){var query=this.query||{};var schema=this.secure?"https":"http";var port="";if(false!==this.timestampRequests){query[this.timestampParam]=+new Date+"-"+Transport.timestamps++}if(!this.supportsBinary&&!query.sid){query.b64=1}query=parseqs.encode(query);if(this.port&&("https"==schema&&this.port!=443||"http"==schema&&this.port!=80)){port=":"+this.port}if(query.length){query="?"+query}return schema+"://"+this.hostname+port+this.path+query}},{"../transport":13,"component-inherit":20,debug:21,"engine.io-parser":24,parseqs:32,xmlhttprequest:19}],18:[function(_dereq_,module,exports){var Transport=_dereq_("../transport");var parser=_dereq_("engine.io-parser");var parseqs=_dereq_("parseqs");var inherit=_dereq_("component-inherit");var debug=_dereq_("debug")("engine.io-client:websocket");var WebSocket=_dereq_("ws");module.exports=WS;function WS(opts){var forceBase64=opts&&opts.forceBase64;if(forceBase64){this.supportsBinary=false}Transport.call(this,opts)}inherit(WS,Transport);WS.prototype.name="websocket";WS.prototype.supportsBinary=true;WS.prototype.doOpen=function(){if(!this.check()){return}var self=this;var uri=this.uri();var protocols=void 0;var opts={agent:this.agent};this.ws=new WebSocket(uri,protocols,opts);if(this.ws.binaryType===undefined){this.supportsBinary=false}this.ws.binaryType="arraybuffer";this.addEventListeners()};WS.prototype.addEventListeners=function(){var self=this;this.ws.onopen=function(){self.onOpen()};this.ws.onclose=function(){self.onClose()};this.ws.onmessage=function(ev){self.onData(ev.data)};this.ws.onerror=function(e){self.onError("websocket error",e)}};if("undefined"!=typeof navigator&&/iPad|iPhone|iPod/i.test(navigator.userAgent)){WS.prototype.onData=function(data){var self=this;setTimeout(function(){Transport.prototype.onData.call(self,data)},0)}}WS.prototype.write=function(packets){var self=this;this.writable=false;for(var i=0,l=packets.length;i=31}exports.formatters.j=function(v){return JSON.stringify(v)};function formatArgs(){var args=arguments;var useColors=this.useColors;args[0]=(useColors?"%c":"")+this.namespace+(useColors?" %c":" ")+args[0]+(useColors?"%c ":" ")+"+"+exports.humanize(this.diff);if(!useColors)return args;var c="color: "+this.color;args=[args[0],c,"color: inherit"].concat(Array.prototype.slice.call(args,1));var index=0;var lastC=0;args[0].replace(/%[a-z%]/g,function(match){if("%"===match)return;index++;if("%c"===match){lastC=index}});args.splice(lastC,0,c);return args}function log(){return"object"==typeof console&&"function"==typeof console.log&&Function.prototype.apply.call(console.log,console,arguments)}function save(namespaces){try{if(null==namespaces){localStorage.removeItem("debug")}else{localStorage.debug=namespaces}}catch(e){}}function load(){var r;try{r=localStorage.debug}catch(e){}return r}exports.enable(load())},{"./debug":22}],22:[function(_dereq_,module,exports){exports=module.exports=debug;exports.coerce=coerce;exports.disable=disable;exports.enable=enable;exports.enabled=enabled;exports.humanize=_dereq_("ms");exports.names=[];exports.skips=[];exports.formatters={};var prevColor=0;var prevTime;function selectColor(){return exports.colors[prevColor++%exports.colors.length]}function debug(namespace){function disabled(){}disabled.enabled=false;function enabled(){var self=enabled;var curr=+new Date;var ms=curr-(prevTime||curr);self.diff=ms;self.prev=prevTime;self.curr=curr;prevTime=curr;if(null==self.useColors)self.useColors=exports.useColors();if(null==self.color&&self.useColors)self.color=selectColor();var args=Array.prototype.slice.call(arguments);args[0]=exports.coerce(args[0]);if("string"!==typeof args[0]){args=["%o"].concat(args)}var index=0;args[0]=args[0].replace(/%([a-z%])/g,function(match,format){if(match==="%")return match;index++;var formatter=exports.formatters[format];if("function"===typeof formatter){var val=args[index];match=formatter.call(self,val);args.splice(index,1);index--}return match});if("function"===typeof exports.formatArgs){args=exports.formatArgs.apply(self,args)}var logFn=enabled.log||exports.log||console.log.bind(console);logFn.apply(self,args)}enabled.enabled=true;var fn=exports.enabled(namespace)?enabled:disabled;fn.namespace=namespace;return fn}function enable(namespaces){exports.save(namespaces);var split=(namespaces||"").split(/[\s,]+/);var len=split.length;for(var i=0;i=d)return Math.round(ms/d)+"d";if(ms>=h)return Math.round(ms/h)+"h";if(ms>=m)return Math.round(ms/m)+"m";if(ms>=s)return Math.round(ms/s)+"s";return ms+"ms"}function long(ms){return plural(ms,d,"day")||plural(ms,h,"hour")||plural(ms,m,"minute")||plural(ms,s,"second")||ms+" ms"}function plural(ms,n,name){if(ms1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}}var asArray=new Uint8Array(data);var type=asArray[0];var rest=sliceBuffer(data,1);if(Blob&&binaryType==="blob"){rest=new Blob([rest])}return{type:packetslist[type],data:rest}};exports.decodeBase64Packet=function(msg,binaryType){var type=packetslist[msg.charAt(0)];if(!global.ArrayBuffer){return{type:type,data:{base64:true,data:msg.substr(1)}}}var data=base64encoder.decode(msg.substr(1));if(binaryType==="blob"&&Blob){data=new Blob([data])}return{type:type,data:data}};exports.encodePayload=function(packets,supportsBinary,callback){if(typeof supportsBinary=="function"){callback=supportsBinary;supportsBinary=null}if(supportsBinary){if(Blob&&!isAndroid){return exports.encodePayloadAsBlob(packets,callback)}return exports.encodePayloadAsArrayBuffer(packets,callback)}if(!packets.length){return callback("0:")}function setLengthHeader(message){return message.length+":"+message}function encodeOne(packet,doneCallback){exports.encodePacket(packet,supportsBinary,true,function(message){doneCallback(null,setLengthHeader(message))})}map(packets,encodeOne,function(err,results){return callback(results.join(""))})};function map(ary,each,done){var result=new Array(ary.length);var next=after(ary.length,done);var eachWithIndex=function(i,el,cb){each(el,function(error,msg){result[i]=msg;cb(error,result)})};for(var i=0;i0){var tailArray=new Uint8Array(bufferTail);var isString=tailArray[0]===0;var msgLength="";for(var i=1;;i++){if(tailArray[i]==255)break;if(msgLength.length>310){numberTooLong=true;break}msgLength+=tailArray[i]}if(numberTooLong)return callback(err,0,1);bufferTail=sliceBuffer(bufferTail,2+msgLength.length);msgLength=parseInt(msgLength);var msg=sliceBuffer(bufferTail,0,msgLength);if(isString){try{msg=String.fromCharCode.apply(null,new Uint8Array(msg))}catch(e){var typed=new Uint8Array(msg);msg="";for(var i=0;ibytes){end=bytes}if(start>=bytes||start>=end||bytes===0){return new ArrayBuffer(0)}var abv=new Uint8Array(arraybuffer);var result=new Uint8Array(end-start);for(var i=start,ii=0;i>2];base64+=chars[(bytes[i]&3)<<4|bytes[i+1]>>4];base64+=chars[(bytes[i+1]&15)<<2|bytes[i+2]>>6];base64+=chars[bytes[i+2]&63]}if(len%3===2){base64=base64.substring(0,base64.length-1)+"="}else if(len%3===1){base64=base64.substring(0,base64.length-2)+"=="}return base64};exports.decode=function(base64){var bufferLength=base64.length*.75,len=base64.length,i,p=0,encoded1,encoded2,encoded3,encoded4;if(base64[base64.length-1]==="="){bufferLength--;if(base64[base64.length-2]==="="){bufferLength--}}var arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer);for(i=0;i>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer}})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},{}],29:[function(_dereq_,module,exports){(function(global){var BlobBuilder=global.BlobBuilder||global.WebKitBlobBuilder||global.MSBlobBuilder||global.MozBlobBuilder;var blobSupported=function(){try{var b=new Blob(["hi"]);return b.size==2}catch(e){return false}}();var blobBuilderSupported=BlobBuilder&&BlobBuilder.prototype.append&&BlobBuilder.prototype.getBlob;function BlobBuilderConstructor(ary,options){options=options||{};var bb=new BlobBuilder;for(var i=0;i=55296&&value<=56319&&counter65535){value-=65536;output+=stringFromCharCode(value>>>10&1023|55296);value=56320|value&1023}output+=stringFromCharCode(value)}return output}function createByte(codePoint,shift){return stringFromCharCode(codePoint>>shift&63|128)}function encodeCodePoint(codePoint){if((codePoint&4294967168)==0){return stringFromCharCode(codePoint)}var symbol="";if((codePoint&4294965248)==0){symbol=stringFromCharCode(codePoint>>6&31|192)}else if((codePoint&4294901760)==0){symbol=stringFromCharCode(codePoint>>12&15|224);symbol+=createByte(codePoint,6)}else if((codePoint&4292870144)==0){symbol=stringFromCharCode(codePoint>>18&7|240);symbol+=createByte(codePoint,12);symbol+=createByte(codePoint,6)}symbol+=stringFromCharCode(codePoint&63|128);return symbol}function utf8encode(string){var codePoints=ucs2decode(string);var length=codePoints.length;var index=-1;var codePoint;var byteString="";while(++index=byteCount){throw Error("Invalid byte index")}var continuationByte=byteArray[byteIndex]&255;byteIndex++;if((continuationByte&192)==128){return continuationByte&63}throw Error("Invalid continuation byte")}function decodeSymbol(){var byte1;var byte2;var byte3;var byte4;var codePoint;if(byteIndex>byteCount){throw Error("Invalid byte index")}if(byteIndex==byteCount){return false}byte1=byteArray[byteIndex]&255;byteIndex++;if((byte1&128)==0){return byte1}if((byte1&224)==192){var byte2=readContinuationByte();codePoint=(byte1&31)<<6|byte2;if(codePoint>=128){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&240)==224){byte2=readContinuationByte();byte3=readContinuationByte();codePoint=(byte1&15)<<12|byte2<<6|byte3;if(codePoint>=2048){return codePoint}else{throw Error("Invalid continuation byte")}}if((byte1&248)==240){byte2=readContinuationByte();byte3=readContinuationByte();byte4=readContinuationByte();codePoint=(byte1&15)<<18|byte2<<12|byte3<<6|byte4;if(codePoint>=65536&&codePoint<=1114111){return codePoint}}throw Error("Invalid UTF-8 detected")}var byteArray;var byteCount;var byteIndex;function utf8decode(byteString){byteArray=ucs2decode(byteString);byteCount=byteArray.length;byteIndex=0;var codePoints=[];var tmp;while((tmp=decodeSymbol())!==false){codePoints.push(tmp)}return ucs2encode(codePoints)}var utf8={version:"2.0.0",encode:utf8encode,decode:utf8decode};if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){define(function(){return utf8})}else if(freeExports&&!freeExports.nodeType){if(freeModule){freeModule.exports=utf8}else{var object={};var hasOwnProperty=object.hasOwnProperty;for(var key in utf8){hasOwnProperty.call(utf8,key)&&(freeExports[key]=utf8[key])}}}else{root.utf8=utf8}})(this)}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],31:[function(_dereq_,module,exports){(function(global){var rvalidchars=/^[\],:{}\s]*$/;var rvalidescape=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rvalidtokens=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rvalidbraces=/(?:^|:|,)(?:\s*\[)+/g;var rtrimLeft=/^\s+/;var rtrimRight=/\s+$/;module.exports=function parsejson(data){if("string"!=typeof data||!data){return null}data=data.replace(rtrimLeft,"").replace(rtrimRight,"");if(global.JSON&&JSON.parse){return JSON.parse(data)}if(rvalidchars.test(data.replace(rvalidescape,"@").replace(rvalidtokens,"]").replace(rvalidbraces,""))){return new Function("return "+data)()}}}).call(this,typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],32:[function(_dereq_,module,exports){exports.encode=function(obj){var str="";for(var i in obj){if(obj.hasOwnProperty(i)){if(str.length)str+="&";str+=encodeURIComponent(i)+"="+encodeURIComponent(obj[i])}}return str};exports.decode=function(qs){var qry={};var pairs=qs.split("&");for(var i=0,l=pairs.length;i1)))/4)-floor((year-1901+month)/100)+floor((year-1601+month)/400)}}if(!(isProperty={}.hasOwnProperty)){isProperty=function(property){var members={},constructor;if((members.__proto__=null,members.__proto__={toString:1},members).toString!=getClass){isProperty=function(property){var original=this.__proto__,result=property in(this.__proto__=null,this);this.__proto__=original;return result}}else{constructor=members.constructor;isProperty=function(property){var parent=(this.constructor||constructor).prototype;return property in this&&!(property in parent&&this[property]===parent[property])}}members=null;return isProperty.call(this,property)}}var PrimitiveTypes={"boolean":1,number:1,string:1,undefined:1};var isHostType=function(object,property){var type=typeof object[property];return type=="object"?!!object[property]:!PrimitiveTypes[type]};forEach=function(object,callback){var size=0,Properties,members,property;(Properties=function(){this.valueOf=0}).prototype.valueOf=0;members=new Properties;for(property in members){if(isProperty.call(members,property)){size++}}Properties=members=null;if(!size){members=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"];forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,length;var hasProperty=!isFunction&&typeof object.constructor!="function"&&isHostType(object,"hasOwnProperty")?object.hasOwnProperty:isProperty;for(property in object){if(!(isFunction&&property=="prototype")&&hasProperty.call(object,property)){callback(property)}}for(length=members.length;property=members[--length];hasProperty.call(object,property)&&callback(property));}}else if(size==2){forEach=function(object,callback){var members={},isFunction=getClass.call(object)==functionClass,property;for(property in object){if(!(isFunction&&property=="prototype")&&!isProperty.call(members,property)&&(members[property]=1)&&isProperty.call(object,property)){callback(property)}}}}else{forEach=function(object,callback){var isFunction=getClass.call(object)==functionClass,property,isConstructor;for(property in object){if(!(isFunction&&property=="prototype")&&isProperty.call(object,property)&&!(isConstructor=property==="constructor")){callback(property)}}if(isConstructor||isProperty.call(object,property="constructor")){callback(property)}}}return forEach(object,callback)};if(!has("json-stringify")){var Escapes={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"};var leadingZeroes="000000";var toPaddedString=function(width,value){return(leadingZeroes+(value||0)).slice(-width)};var unicodePrefix="\\u00";var quote=function(value){var result='"',index=0,length=value.length,isLarge=length>10&&charIndexBuggy,symbols;if(isLarge){symbols=value.split("")}for(;index-1/0&&value<1/0){if(getDay){date=floor(value/864e5);for(year=floor(date/365.2425)+1970-1;getDay(year+1,0)<=date;year++);for(month=floor((date-getDay(year,0))/30.42);getDay(year,month+1)<=date;month++);date=1+date-getDay(year,month);time=(value%864e5+864e5)%864e5;hours=floor(time/36e5)%24;minutes=floor(time/6e4)%60;seconds=floor(time/1e3)%60;milliseconds=time%1e3}else{year=value.getUTCFullYear();month=value.getUTCMonth();date=value.getUTCDate();hours=value.getUTCHours();minutes=value.getUTCMinutes();seconds=value.getUTCSeconds();milliseconds=value.getUTCMilliseconds()}value=(year<=0||year>=1e4?(year<0?"-":"+")+toPaddedString(6,year<0?-year:year):toPaddedString(4,year))+"-"+toPaddedString(2,month+1)+"-"+toPaddedString(2,date)+"T"+toPaddedString(2,hours)+":"+toPaddedString(2,minutes)+":"+toPaddedString(2,seconds)+"."+toPaddedString(3,milliseconds)+"Z"}else{value=null}}else if(typeof value.toJSON=="function"&&(className!=numberClass&&className!=stringClass&&className!=arrayClass||isProperty.call(value,"toJSON"))){value=value.toJSON(property)}}if(callback){value=callback.call(object,property,value)}if(value===null){return"null"}className=getClass.call(value);if(className==booleanClass){return""+value}else if(className==numberClass){return value>-1/0&&value<1/0?""+value:"null"}else if(className==stringClass){return quote(""+value)}if(typeof value=="object"){for(length=stack.length;length--;){if(stack[length]===value){throw TypeError()}}stack.push(value);results=[];prefix=indentation;indentation+=whitespace;if(className==arrayClass){for(index=0,length=value.length;index0){for(whitespace="",width>10&&(width=10);whitespace.length=48&&charCode<=57||charCode>=97&&charCode<=102||charCode>=65&&charCode<=70)){abort()}}value+=fromCharCode("0x"+source.slice(begin,Index));break;default:abort()}}else{if(charCode==34){break}charCode=source.charCodeAt(Index);begin=Index;while(charCode>=32&&charCode!=92&&charCode!=34){charCode=source.charCodeAt(++Index)}value+=source.slice(begin,Index)}}if(source.charCodeAt(Index)==34){Index++;return value}abort();default:begin=Index;if(charCode==45){isSigned=true;charCode=source.charCodeAt(++Index)}if(charCode>=48&&charCode<=57){if(charCode==48&&(charCode=source.charCodeAt(Index+1),charCode>=48&&charCode<=57)){abort()}isSigned=false;for(;Index=48&&charCode<=57);Index++);if(source.charCodeAt(Index)==46){position=++Index;for(;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}charCode=source.charCodeAt(Index);if(charCode==101||charCode==69){charCode=source.charCodeAt(++Index);if(charCode==43||charCode==45){Index++}for(position=Index;position=48&&charCode<=57);position++);if(position==Index){abort()}Index=position}return+source.slice(begin,Index)}if(isSigned){abort()}if(source.slice(Index,Index+4)=="true"){Index+=4;return true}else if(source.slice(Index,Index+5)=="false"){Index+=5;return false}else if(source.slice(Index,Index+4)=="null"){Index+=4;return null}abort()}}return"$"};var get=function(value){var results,hasMembers;if(value=="$"){abort()}if(typeof value=="string"){if((charIndexBuggy?value.charAt(0):value[0])=="@"){return value.slice(1)}if(value=="["){results=[];for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="]"){break}if(hasMembers){if(value==","){value=lex();if(value=="]"){abort()}}else{abort()}}if(value==","){abort()}results.push(get(value))}return results}else if(value=="{"){results={};for(;;hasMembers||(hasMembers=true)){value=lex();if(value=="}"){break}if(hasMembers){if(value==","){value=lex();if(value=="}"){abort()}}else{abort()}}if(value==","||typeof value!="string"||(charIndexBuggy?value.charAt(0):value[0])!="@"||lex()!=":"){abort()}results[value.slice(1)]=get(lex())}return results}abort()}return value};var update=function(source,property,callback){var element=walk(source,property,callback);if(element===undef){delete source[property]}else{source[property]=element}};var walk=function(source,property,callback){var value=source[property],length;if(typeof value=="object"&&value){if(getClass.call(value)==arrayClass){for(length=value.length;length--;){update(value,length,callback)}}else{forEach(value,function(property){update(value,property,callback)})}}return callback.call(source,property,value)};JSON3.parse=function(source,callback){var result,value;Index=0;Source=""+source;result=get(lex());if(lex()!="$"){abort()}Index=Source=null;return callback&&getClass.call(callback)==functionClass?walk((value={},value[""]=result,value),"",callback):result}}}if(isLoader){define(function(){return JSON3})}})(this)},{}],47:[function(_dereq_,module,exports){module.exports=toArray;function toArray(list,index){var array=[];index=index||0;for(var i=index||0;i :body + * => :statusCode + * => :headers + * + * @constructor + */ + + function JWR(responseCtx) { + this.body = responseCtx.body || {}; + this.headers = responseCtx.headers || {}; + this.statusCode = responseCtx.statusCode || 200; + if (this.statusCode < 200 || this.statusCode >= 400) { + this.error = this.body || this.statusCode; + } + } + JWR.prototype.toString = function() { + return '[ResponseFromSails]' + ' -- ' + + 'Status: ' + this.statusCode + ' -- ' + + 'Headers: ' + this.headers + ' -- ' + + 'Body: ' + this.body; + }; + JWR.prototype.toPOJO = function() { + return { + body: this.body, + headers: this.headers, + statusCode: this.statusCode + }; + }; + JWR.prototype.pipe = function() { + // TODO: look at substack's stuff + return new Error('Client-side streaming support not implemented yet.'); + }; + + + /** + * @api private + * @param {SailsSocket} socket [description] + * @param {Object} requestCtx [description] + */ + + function _emitFrom(socket, requestCtx) { + + if (!socket._raw) { + throw new Error('Failed to emit from socket- raw SIO socket is missing.'); + } + + // Since callback is embedded in requestCtx, + // retrieve it and delete the key before continuing. + var cb = requestCtx.cb; + delete requestCtx.cb; + + // Name of the appropriate socket.io listener on the server + // ( === the request method or "verb", e.g. 'get', 'post', 'put', etc. ) + var sailsEndpoint = requestCtx.method; + + socket._raw.emit(sailsEndpoint, requestCtx, function serverResponded(responseCtx) { + + // Send back (emulatedHTTPBody, jsonWebSocketResponse) + if (cb) { + cb(responseCtx.body, new JWR(responseCtx)); + } + }); + } + + ////////////////////////////////////////////////////////////// + ///// //////////////////////// + ////////////////////////////////////////////////////////////// + + + + // Version note: + // + // `io.SocketNamespace.prototype` doesn't exist in sio 1.0. + // + // Rather than adding methods to the prototype for the Socket instance that is returned + // when the browser connects with `io.connect()`, we create our own constructor, `SailsSocket`. + // This makes our solution more future-proof and helps us work better w/ the Socket.io team + // when changes are rolled out in the future. To get a `SailsSocket`, you can run: + // ``` + // io.sails.connect(); + // ``` + + + + /** + * SailsSocket + * + * A wrapper for an underlying Socket instance that communicates directly + * to the Socket.io server running inside of Sails. + * + * If no `socket` option is provied, SailsSocket will function as a mock. It will queue socket + * requests and event handler bindings, replaying them when the raw underlying socket actually + * connects. This is handy when we don't necessarily have the valid configuration to know + * WHICH SERVER to talk to yet, etc. It is also used by `io.socket` for your convenience. + * + * @constructor + */ + + function SailsSocket (opts){ + var self = this; + opts = opts||{}; + + // Absorb opts + self.useCORSRouteToGetCookie = opts.useCORSRouteToGetCookie; + self.url = opts.url; + self.multiplex = opts.multiplex; + self.transports = opts.transports; + + // Set up "eventQueue" to hold event handlers which have not been set on the actual raw socket yet. + self.eventQueue = {}; + + // Listen for special `parseError` event sent from sockets hook on the backend + // if an error occurs but a valid callback was not received from the client + // (i.e. so the server had no other way to send back the error information) + self.on('sails:parseError', function (err){ + consolog('Sails encountered an error parsing a socket message sent from this client, and did not have access to a callback function to respond with.'); + consolog('Error details:',err); + }); + + // TODO: + // Listen for a special private message on any connected that allows the server + // to set the environment (giving us 100% certainty that we guessed right) + // However, note that the `console.log`s called before and after connection + // are still forced to rely on our existing heuristics (to disable, tack #production + // onto the URL used to fetch this file.) + } + + + /** + * Start connecting this socket. + * + * @api private + */ + SailsSocket.prototype._connect = function (){ + var self = this; + + // Apply `io.sails` config as defaults + // (now that at least one tick has elapsed) + self.useCORSRouteToGetCookie = self.useCORSRouteToGetCookie||io.sails.useCORSRouteToGetCookie; + self.url = self.url||io.sails.url; + self.transports = self.transports || io.sails.transports; + + // Ensure URL has no trailing slash + self.url = self.url ? self.url.replace(/(\/)$/, '') : undefined; + + // Mix the current SDK version into the query string in + // the connection request to the server: + if (typeof self.query !== 'string') self.query = SDK_INFO.versionString; + else self.query += '&' + SDK_INFO.versionString; + + // Determine whether this is a cross-origin socket by examining the + // hostname and port on the `window.location` object. + var isXOrigin = (function (){ + + // If `window` doesn't exist (i.e. being used from node.js), then it's + // always "cross-domain". + if (typeof window === 'undefined' || typeof window.location === 'undefined') { + return false; + } + + // If `self.url` (aka "target") is falsy, then we don't need to worry about it. + if (typeof self.url !== 'string') { return false; } + + // Get information about the "target" (`self.url`) + var targetProtocol = (function (){ + try { + targetProtocol = self.url.match(/^([a-z]+:\/\/)/i)[1].toLowerCase(); + } + catch (e) {} + targetProtocol = targetProtocol || 'http://'; + return targetProtocol; + })(); + var isTargetSSL = !!self.url.match('^https'); + var targetPort = (function (){ + try { + return self.url.match(/^[a-z]+:\/\/[^:]*:([0-9]*)/i)[1]; + } + catch (e){} + return isTargetSSL ? '443' : '80'; + })(); + var targetAfterProtocol = self.url.replace(/^([a-z]+:\/\/)/i, ''); + + + // If target protocol is different than the actual protocol, + // then we'll consider this cross-origin. + if (targetProtocol.replace(/[:\/]/g, '') !== window.location.protocol.replace(/[:\/]/g,'')) { + return true; + } + + + // If target hostname is different than actual hostname, we'll consider this cross-origin. + var hasSameHostname = targetAfterProtocol.search(window.location.hostname) !== 0; + if (!hasSameHostname) { + return true; + } + + // If no actual port is explicitly set on the `window.location` object, + // we'll assume either 80 or 443. + var isLocationSSL = window.location.protocol.match(/https/i); + var locationPort = (window.location.port+'') || (isLocationSSL ? '443' : '80'); + + // Finally, if ports don't match, we'll consider this cross-origin. + if (targetPort !== locationPort) { + return true; + } + + // Otherwise, it's the same origin. + return false; + + })(); + + + // Prepare to start connecting the socket + (function selfInvoking (cb){ + + // If this is an attempt at a cross-origin or cross-port + // socket connection, send a JSONP request first to ensure + // that a valid cookie is available. This can be disabled + // by setting `io.sails.useCORSRouteToGetCookie` to false. + // + // Otherwise, skip the stuff below. + if (!(self.useCORSRouteToGetCookie && isXOrigin)) { + return cb(); + } + + // Figure out the x-origin CORS route + // (Sails provides a default) + var xOriginCookieURL = self.url; + if (typeof self.useCORSRouteToGetCookie === 'string') { + xOriginCookieURL += self.useCORSRouteToGetCookie; + } + else { + xOriginCookieURL += '/__getcookie'; + } + + + // Make the AJAX request (CORS) + if (typeof window !== 'undefined') { + jsonp({ + url: xOriginCookieURL, + method: 'GET' + }, cb); + return; + } + + // If there's no `window` object, we must be running in Node.js + // so just require the request module and send the HTTP request that + // way. + var mikealsReq = require('request'); + mikealsReq.get(xOriginCookieURL, function(err, httpResponse, body) { + if (err) { + consolog( + 'Failed to connect socket (failed to get cookie)', + 'Error:', err + ); + return; + } + cb(); + }); + + })(function goAheadAndActuallyConnect() { + + // Now that we're ready to connect, create a raw underlying Socket + // using Socket.io and save it as `_raw` (this will start it connecting) + self._raw = io(self.url, self); + + // Replay event bindings from the eager socket + self.replay(); + + + /** + * 'connect' event is triggered when the socket establishes a connection + * successfully. + */ + self.on('connect', function socketConnected() { + + consolog.noPrefix( + '\n' + + '\n' + + // ' |> ' + '\n' + + // ' \\___/ '+️ + // '\n'+ + ' |> Now connected to Sails.' + '\n' + + '\\___/ For help, see: http://bit.ly/1DmTvgK' + '\n' + + ' (using '+io.sails.sdk.platform+' SDK @v'+io.sails.sdk.version+')'+ '\n' + + '\n'+ + '\n'+ + // '\n'+ + '' + // ' ⚓︎ (development mode)' + // 'e.g. to send a GET request to Sails via WebSockets, run:'+ '\n' + + // '`io.socket.get("/foo", function serverRespondedWith (body, jwr) { console.log(body); })`'+ '\n' + + ); + }); + + self.on('disconnect', function() { + self.connectionLostTimestamp = (new Date()).getTime(); + consolog('===================================='); + consolog('Socket was disconnected from Sails.'); + consolog('Usually, this is due to one of the following reasons:' + '\n' + + ' -> the server ' + (self.url ? self.url + ' ' : '') + 'was taken down' + '\n' + + ' -> your browser lost internet connectivity'); + consolog('===================================='); + }); + + self.on('reconnecting', function(numAttempts) { + consolog( + '\n'+ + ' Socket is trying to reconnect to Sails...\n'+ + '_-|>_- (attempt #' + numAttempts + ')'+'\n'+ + '\n' + ); + }); + + self.on('reconnect', function(transport, numAttempts) { + var msSinceConnectionLost = ((new Date()).getTime() - self.connectionLostTimestamp); + var numSecsOffline = (msSinceConnectionLost / 1000); + consolog( + '\n'+ + ' |> Socket reconnected successfully after'+'\n'+ + '\\___/ being offline for ~' + numSecsOffline + ' seconds.'+'\n'+ + '\n' + ); + }); + + // 'error' event is triggered if connection can not be established. + // (usually because of a failed authorization, which is in turn + // usually due to a missing or invalid cookie) + self.on('error', function failedToConnect(err) { + + // TODO: + // handle failed connections due to failed authorization + // in a smarter way (probably can listen for a different event) + + // A bug in Socket.io 0.9.x causes `connect_failed` + // and `reconnect_failed` not to fire. + // Check out the discussion in github issues for details: + // https://github.com/LearnBoost/socket.io/issues/652 + // io.socket.on('connect_failed', function () { + // consolog('io.socket emitted `connect_failed`'); + // }); + // io.socket.on('reconnect_failed', function () { + // consolog('io.socket emitted `reconnect_failed`'); + // }); + + consolog( + 'Failed to connect socket (probably due to failed authorization on server)', + 'Error:', err + ); + }); + }); + + }; + + + /** + * Disconnect the underlying socket. + * + * @api public + */ + SailsSocket.prototype.disconnect = function (){ + if (!this._raw) { + throw new Error('Cannot disconnect- socket is already disconnected'); + } + return this._raw.disconnect(); + }; + + + + /** + * isConnected + * + * @api private + * @return {Boolean} whether the socket is connected and able to + * communicate w/ the server. + */ + + SailsSocket.prototype.isConnected = function () { + if (!this._raw) { + return false; + } + + return !!this._raw.connected; + }; + + + + /** + * [replay description] + * @return {[type]} [description] + */ + SailsSocket.prototype.replay = function (){ + var self = this; + + // Pass events and a reference to the request queue + // off to the self._raw for consumption + for (var evName in self.eventQueue) { + for (var i in self.eventQueue[evName]) { + self._raw.on(evName, self.eventQueue[evName][i]); + } + } + + // Bind a one-time function to run the request queue + // when the self._raw connects. + if ( !self.isConnected() ) { + var alreadyRanRequestQueue = false; + self._raw.on('connect', function whenRawSocketConnects() { + if (alreadyRanRequestQueue) return; + runRequestQueue(self); + alreadyRanRequestQueue = true; + }); + } + // Or run it immediately if self._raw is already connected + else { + runRequestQueue(self); + } + + return self; + }; + + + /** + * Chainable method to bind an event to the socket. + * + * @param {String} evName [event name] + * @param {Function} fn [event handler function] + * @return {SailsSocket} + */ + SailsSocket.prototype.on = function (evName, fn){ + + // Bind the event to the raw underlying socket if possible. + if (this._raw) { + this._raw.on(evName, fn); + return this; + } + + // Otherwise queue the event binding. + if (!this.eventQueue[evName]) { + this.eventQueue[evName] = [fn]; + } + else { + this.eventQueue[evName].push(fn); + } + + return this; + }; + + /** + * Chainable method to unbind an event from the socket. + * + * @param {String} evName [event name] + * @param {Function} fn [event handler function] + * @return {SailsSocket} + */ + SailsSocket.prototype.off = function (evName, fn){ + + // Bind the event to the raw underlying socket if possible. + if (this._raw) { + this._raw.off(evName, fn); + return this; + } + + // Otherwise queue the event binding. + if (this.eventQueue[evName] && this.eventQueue[evName].indexOf(fn) > -1) { + this.eventQueue[evName].splice(this.eventQueue[evName].indexOf(fn), 1); + } + + return this; + }; + + + /** + * Chainable method to unbind all events from the socket. + * + * @return {SailsSocket} + */ + SailsSocket.prototype.removeAllListeners = function (){ + + // Bind the event to the raw underlying socket if possible. + if (this._raw) { + this._raw.removeAllListeners(); + return this; + } + + // Otherwise queue the event binding. + this.eventQueue = {}; + + return this; + }; + + /** + * Simulate a GET request to sails + * e.g. + * `socket.get('/user/3', Stats.populate)` + * + * @api public + * @param {String} url :: destination URL + * @param {Object} params :: parameters to send with the request [optional] + * @param {Function} cb :: callback function to call when finished [optional] + */ + + SailsSocket.prototype.get = function(url, data, cb) { + + // `data` is optional + if (typeof data === 'function') { + cb = data; + data = {}; + } + + return this.request({ + method: 'get', + params: data, + url: url + }, cb); + }; + + + + /** + * Simulate a POST request to sails + * e.g. + * `socket.post('/event', newMeeting, $spinner.hide)` + * + * @api public + * @param {String} url :: destination URL + * @param {Object} params :: parameters to send with the request [optional] + * @param {Function} cb :: callback function to call when finished [optional] + */ + + SailsSocket.prototype.post = function(url, data, cb) { + + // `data` is optional + if (typeof data === 'function') { + cb = data; + data = {}; + } + + return this.request({ + method: 'post', + data: data, + url: url + }, cb); + }; + + + + /** + * Simulate a PUT request to sails + * e.g. + * `socket.post('/event/3', changedFields, $spinner.hide)` + * + * @api public + * @param {String} url :: destination URL + * @param {Object} params :: parameters to send with the request [optional] + * @param {Function} cb :: callback function to call when finished [optional] + */ + + SailsSocket.prototype.put = function(url, data, cb) { + + // `data` is optional + if (typeof data === 'function') { + cb = data; + data = {}; + } + + return this.request({ + method: 'put', + params: data, + url: url + }, cb); + }; + + + + /** + * Simulate a DELETE request to sails + * e.g. + * `socket.delete('/event', $spinner.hide)` + * + * @api public + * @param {String} url :: destination URL + * @param {Object} params :: parameters to send with the request [optional] + * @param {Function} cb :: callback function to call when finished [optional] + */ + + SailsSocket.prototype['delete'] = function(url, data, cb) { + + // `data` is optional + if (typeof data === 'function') { + cb = data; + data = {}; + } + + return this.request({ + method: 'delete', + params: data, + url: url + }, cb); + }; + + + + /** + * Simulate an HTTP request to sails + * e.g. + * ``` + * socket.request({ + * url:'/user', + * params: {}, + * method: 'POST', + * headers: {} + * }, function (responseBody, JWR) { + * // ... + * }); + * ``` + * + * @api public + * @option {String} url :: destination URL + * @option {Object} params :: parameters to send with the request [optional] + * @option {Object} headers:: headers to send with the request [optional] + * @option {Function} cb :: callback function to call when finished [optional] + * @option {String} method :: HTTP request method [optional] + */ + + SailsSocket.prototype.request = function(options, cb) { + + var usage = + 'Usage:\n'+ + 'socket.request( options, [fnToCallWhenComplete] )\n\n'+ + 'options.url :: e.g. "/foo/bar"'+'\n'+ + 'options.method :: e.g. "get", "post", "put", or "delete", etc.'+'\n'+ + 'options.params :: e.g. { emailAddress: "mike@sailsjs.org" }'+'\n'+ + 'options.headers :: e.g. { "x-my-custom-header": "some string" }'; + // Old usage: + // var usage = 'Usage:\n socket.'+(options.method||'request')+'('+ + // ' destinationURL, [dataToSend], [fnToCallWhenComplete] )'; + + + // Validate options and callback + if (typeof options !== 'object' || typeof options.url !== 'string') { + throw new Error('Invalid or missing URL!\n' + usage); + } + if (options.method && typeof options.method !== 'string') { + throw new Error('Invalid `method` provided (should be a string like "post" or "put")\n' + usage); + } + if (options.headers && typeof options.headers !== 'object') { + throw new Error('Invalid `headers` provided (should be an object with string values)\n' + usage); + } + if (options.params && typeof options.params !== 'object') { + throw new Error('Invalid `params` provided (should be an object with string values)\n' + usage); + } + if (cb && typeof cb !== 'function') { + throw new Error('Invalid callback function!\n' + usage); + } + + + // Build a simulated request object + // (and sanitize/marshal options along the way) + var requestCtx = { + + method: options.method.toLowerCase() || 'get', + + headers: options.headers || {}, + + data: options.params || options.data || {}, + + // Remove trailing slashes and spaces to make packets smaller. + url: options.url.replace(/^(.+)\/*\s*$/, '$1'), + + cb: cb + }; + + // If this socket is not connected yet, queue up this request + // instead of sending it. + // (so it can be replayed when the socket comes online.) + if ( ! this.isConnected() ) { + + // If no queue array exists for this socket yet, create it. + this.requestQueue = this.requestQueue || []; + this.requestQueue.push(requestCtx); + return; + } + + + // Otherwise, our socket is ok! + // Send the request. + _emitFrom(this, requestCtx); + }; + + + + /** + * Socket.prototype._request + * + * Simulate HTTP over Socket.io. + * + * @api private + * @param {[type]} options [description] + * @param {Function} cb [description] + */ + SailsSocket.prototype._request = function(options, cb) { + throw new Error('`_request()` was a private API deprecated as of v0.11 of the sails.io.js client. Use `.request()` instead.'); + }; + + + + // Set a `sails` object that may be used for configuration before the + // first socket connects (i.e. to prevent auto-connect) + io.sails = { + + // Whether to automatically connect a socket and save it as `io.socket`. + autoConnect: true, + + // The route (path) to hit to get a x-origin (CORS) cookie + // (or true to use the default: '/__getcookie') + useCORSRouteToGetCookie: true, + + // The environment we're running in. + // (logs are not displayed when this is set to 'production') + // + // Defaults to development unless this script was fetched from a URL + // that ends in `*.min.js` or '#production' (may also be manually overridden.) + // + environment: urlThisScriptWasFetchedFrom.match(/(\#production|\.min\.js)/g) ? 'production' : 'development', + + // The version of this sails.io.js client SDK + sdk: SDK_INFO, + + // Transports to use when communicating with the server, in the order they will be tried + transports: ['polling', 'websocket'] + }; + + + + /** + * Add `io.sails.connect` function as a wrapper for the built-in `io()` aka `io.connect()` + * method, returning a SailsSocket. This special function respects the configured io.sails + * connection URL, as well as sending other identifying information (most importantly, the + * current version of this SDK). + * + * @param {String} url [optional] + * @param {Object} opts [optional] + * @return {Socket} + */ + io.sails.connect = function(url, opts) { + opts = opts || {}; + + // If explicit connection url is specified, save it to options + opts.url = url || opts.url || undefined; + + // Instantiate and return a new SailsSocket- and try to connect immediately. + var socket = new SailsSocket(opts); + socket._connect(); + return socket; + }; + + + + // io.socket + // + // The eager instance of Socket which will automatically try to connect + // using the host that this js file was served from. + // + // This can be disabled or configured by setting properties on `io.sails.*` within the + // first cycle of the event loop. + // + + + // Build `io.socket` so it exists + // (this does not start the connection process) + io.socket = new SailsSocket(); + + // In the mean time, this eager socket will be queue events bound by the user + // before the first cycle of the event loop (using `.on()`), which will later + // be rebound on the raw underlying socket. + + // If configured to do so, start auto-connecting after the first cycle of the event loop + // has completed (to allow time for this behavior to be configured/disabled + // by specifying properties on `io.sails`) + setTimeout(function() { + + // If autoConnect is disabled, delete the eager socket (io.socket) and bail out. + if (!io.sails.autoConnect) { + delete io.socket; + return; + } + + // consolog('Eagerly auto-connecting socket to Sails... (requests will be queued in the mean-time)'); + io.socket._connect(); + + + }, 0); // + + + // Return the `io` object. + return io; + } + + + // Add CommonJS support to allow this client SDK to be used from Node.js. + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = SailsIOClient; + return SailsIOClient; + } + + // Otherwise, try to instantiate the client: + // In case you're wrapping the socket.io client to prevent pollution of the + // global namespace, you can replace the global `io` with your own `io` here: + return SailsIOClient(); + +})(); diff --git a/lightcalc/assets/js/directives/isNameFree.js b/lightcalc/assets/js/directives/isNameFree.js new file mode 100644 index 0000000..cf364d0 --- /dev/null +++ b/lightcalc/assets/js/directives/isNameFree.js @@ -0,0 +1,30 @@ +angular.module('LightCalc').directive('isNameFree', ['Calculators', '$q', '$timeout',function(Calculators, $q, $timeout) { + return { + require: 'ngModel', + link: function(scope, elm, attrs, ctrl) { + + ctrl.$asyncValidators.isNameFree = function(modelValue, viewValue) { + + if (ctrl.$isEmpty(modelValue)) { + // consider empty model valid + return $q.when(); + } + + var def = $q.defer(); + + var calculators = Calculators.get({name: modelValue}); + calculators.$promise.then(function(){ + if (calculators.length === 0) { + // The username is available + def.resolve(); + } else { + def.reject(); + } + }) + + + return def.promise; + }; + } + }; +}]); \ No newline at end of file diff --git a/lightcalc/assets/js/routes.js b/lightcalc/assets/js/routes.js new file mode 100644 index 0000000..1b330e8 --- /dev/null +++ b/lightcalc/assets/js/routes.js @@ -0,0 +1,12 @@ +angular.module('LightCalc').config(['$routeProvider', function($routeProvider){ + $routeProvider + .when('/', { + templateUrl: 'templates/calculator/create.html', + controller: 'calculatorCreateCtrl' + }) + .when('/:name/', { + templateUrl: '/templates/calculator/retrieve.html', + controller: 'calculatorRetrieveCtrl' + }) + .otherwise({redirectTo: '/'}); +}]); \ No newline at end of file diff --git a/lightcalc/assets/js/services/calculators.js b/lightcalc/assets/js/services/calculators.js new file mode 100644 index 0000000..7e1d8d4 --- /dev/null +++ b/lightcalc/assets/js/services/calculators.js @@ -0,0 +1,7 @@ +angular.module('LightCalc').factory('Calculators', ['$resource', function CalculatorsFactory($resource){ + return $resource('/calculator/:slug/',{},{ + get: { + isArray: true + } + }); +}]); \ No newline at end of file diff --git a/lightcalc/assets/js/services/operations.js b/lightcalc/assets/js/services/operations.js new file mode 100644 index 0000000..d8f0034 --- /dev/null +++ b/lightcalc/assets/js/services/operations.js @@ -0,0 +1,3 @@ +angular.module('LightCalc').factory('Operations', ['$resource', function OperationsFactory($resource){ + return $resource('/operation/:slug/'); +}]); \ No newline at end of file diff --git a/lightcalc/assets/js/tests/CalculatorsCreate.test.js b/lightcalc/assets/js/tests/CalculatorsCreate.test.js new file mode 100644 index 0000000..60703d2 --- /dev/null +++ b/lightcalc/assets/js/tests/CalculatorsCreate.test.js @@ -0,0 +1,129 @@ +describe('calculatorCreateCtrl', function(){ + var scope, location, httpBackend; + + beforeEach(function(){ + module('LightCalc'); + module('MyTemplates'); + }); + + beforeEach(inject(function($rootScope, $controller, $location, $httpBackend){ + scope = $rootScope.$new(); + location = $location; + location.path('/'); + + $controller('calculatorCreateCtrl', { + "$scope": scope + }); + httpBackend = $httpBackend; + httpBackend.when('POST', '/calculator').respond(201, { + "operations": [], + "name": "teste123123", + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + "id": 40 + }); + + //httpBackend.when('GET', '/calculator').respond([]); + httpBackend.when('GET', '/calculator?name=').respond(200, []); + httpBackend.when('GET', '/calculator?name=teste123123').respond(200, []); + httpBackend.when('GET', '/calculator?name=teste').respond(200, []); + httpBackend.when('GET', '/calculator?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa').respond(200, []); + httpBackend.when('GET', '/calculator?name=hugobessa').respond([{ + "operations": [], + "name": "hugobessa", + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + "id": 40 + }]); + httpBackend.when('GET', '/calculator').respond([]); + + + })); + + describe('submitForm', function(){ + + it('should redirect the user to the calculator if the name is valid', function(done) { + inject(function($templateCache, $compile){ + templateHtml = $templateCache.get('assets/templates/calculator/create.html'); + formElem = angular.element("
" + templateHtml + "
"); + + $compile(formElem)(scope); + scope.$apply(); + scope.form.$setDirty(); + + scope.calculatorForm.name = 'teste123123'; + + resp = scope.submitForm(); + scope.$apply(); + httpBackend.flush(); + resp.$promise.then(function(){ + location.path().should.equals('/teste123123'); + done(); + }) + scope.$root.$digest(); + }); + }); + + it('should fail if the name has less than 8 chars', inject(function($templateCache, $compile){ + templateHtml = $templateCache.get('assets/templates/calculator/create.html'); + formElem = angular.element("
" + templateHtml + "
"); + + scope.calculatorForm.name = 'teste'; + + $compile(formElem)(scope); + scope.$apply(); + + scope.submitForm(); + + (!!scope.form.$error.minlength).should.equal(true); + })); + + it('should fail if the name has more than 40 chars', inject(function($templateCache, $compile){ + templateHtml = $templateCache.get('assets/templates/calculator/create.html'); + formElem = angular.element("
" + templateHtml + "
"); + + scope.calculatorForm.name = new Array( 40 + 2 ).join('a'); + + $compile(formElem)(scope); + scope.$apply(); + + scope.submitForm(); + + (!!scope.form.$error.maxlength).should.equal(true); + })); + + it('should fail if the name is not alphanumeric', inject(function($templateCache, $compile){ + templateHtml = $templateCache.get('assets/templates/calculator/create.html'); + formElem = angular.element("
" + templateHtml + "
"); + + scope.calculatorForm.name = ' asdas'; + + $compile(formElem)(scope); + scope.$apply(); + + scope.submitForm(); + (!!scope.form.$error.pattern).should.equal(true); + })); + + it('should fail if the name already exists', function(done){ + //this.timeout(6000); + inject(function($templateCache, $compile, $timeout){ + templateHtml = $templateCache.get('assets/templates/calculator/create.html'); + formElem = angular.element("
" + templateHtml + "
"); + + $compile(formElem)(scope); + scope.calculatorForm.name = 'hugobessa'; + var promise = scope.form.name.$asyncValidators.isNameFree(scope.calculatorForm.name, scope.calculatorForm.name); + scope.$apply(); + httpBackend.flush(); + + promise.finally(function(data){ + scope.submitForm(); + (!!scope.form.$error.isNameFree).should.equal(true); + done(); + }); + scope.$apply(); + }); + }); + }); +}); \ No newline at end of file diff --git a/lightcalc/assets/js/tests/OperationsCreate.test.js b/lightcalc/assets/js/tests/OperationsCreate.test.js new file mode 100644 index 0000000..a8aef63 --- /dev/null +++ b/lightcalc/assets/js/tests/OperationsCreate.test.js @@ -0,0 +1,213 @@ +describe('calculatorRetrieveCtrl', function(){ + var scope, location, httpBackend; + + beforeEach(function(){ + module('LightCalc'); + module('MyTemplates'); + }); + + beforeEach(function(done){ + inject(function($rootScope, $controller, $location, $httpBackend){ + scope = $rootScope.$new(); + location = $location; + location.path('/hugobessa'); + + $controller('calculatorRetrieveCtrl', { + "$scope": scope, + "$location": location, + "$routeParams": {name: 'hugobessa'} + }); + + httpBackend = $httpBackend; + httpBackend.when('GET', '/calculator?name=hugobessa').respond([{ + "operations": [], + "name": "hugobessa", + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + "id": 40 + }]); + + + httpBackend.when('POST', '/operation', { + 'text': '2 + 5 - 4 ÷ 2 x 7', + 'calculator': 40 + }).respond({ + "id": 400, + "text": '2 + 5 - 4 ÷ 2 x 7', + "calculator": { + "operations": [400], + "name": "hugobessa", + "createdAt": "2015-06-10T05:19:54.810Z", + "updatedAt": "2015-06-10T05:19:54.810Z", + "id": 40 + }, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }); + + httpBackend.when('POST', '/operation', { + 'text': '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + 'calculator': 40 + }).respond({ + "id": 400, + "text": '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + "calculator": { + "operations": [400], + "name": "hugobessa", + "createdAt": "2015-06-10T05:19:54.810Z", + "updatedAt": "2015-06-10T05:19:54.810Z", + "id": 40 + }, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }); + + scope.$apply(); + httpBackend.flush(); + + scope.$watch(function(){ + return scope.calculator; + }, function(nv, ov){ + if(nv !== undefined){ + done(); + } + }); + + scope.$apply() + + + }); + }); + + describe('Write on the keyboard', function(){ + it('should show operation 2 + 5 - 4/2 * 7 on the screen', function(){ + scope.putDigit(2); + scope.add(2); + scope.putDigit(5); + scope.subtract(); + scope.putDigit(4); + scope.divide(); + scope.putDigit(2); + scope.multiply(); + scope.putDigit(7); + + scope.output.should.equals('2 + 5 - 4 ÷ 2 x 7'); + }); + + it('should show operation with decimal values 2 + 23.4 - 4.4/2.12 * 7.5 on the screen', function(){ + scope.putDigit(2); + scope.add(); + scope.putDigit(2); + scope.putDigit(3); + scope.putDigit('.'); + scope.putDigit(4); + scope.subtract(); + scope.putDigit(4); + scope.putDigit('.'); + scope.putDigit(4); + scope.divide(); + scope.putDigit(2); + scope.putDigit('.'); + scope.putDigit(1); + scope.putDigit(2); + scope.multiply(); + scope.putDigit(7); + scope.putDigit('.'); + scope.putDigit(5); + + scope.output.should.equals('2 + 23.4 - 4.4 ÷ 2.12 x 7.5'); + }); + + it('should show operation with decimal values 2 + 23.4 - 4.4/2.12 * 7.5 on the screen and then, whene clear is called, show 0', function(){ + scope.putDigit(2); + scope.add(); + scope.putDigit(2); + scope.putDigit(3); + scope.putDigit('.'); + scope.putDigit(4); + scope.subtract(); + scope.putDigit(4); + scope.putDigit('.'); + scope.putDigit(4); + scope.divide(); + scope.putDigit(2); + scope.putDigit('.'); + scope.putDigit(1); + scope.putDigit(2); + scope.multiply(); + scope.putDigit(7); + scope.putDigit('.'); + scope.putDigit(5); + + scope.output.should.equals('2 + 23.4 - 4.4 ÷ 2.12 x 7.5'); + + scope.clear(); + + scope.output.should.equals('0'); + }); + }); + + describe('create operation', function(){ + + it('should calculate 2 + 5 - 4 / 2 * 7 and show the result, -7, on the screen', function(done){ + + scope.putDigit(2); + scope.add(); + scope.putDigit(5); + scope.subtract(); + scope.putDigit(4); + scope.divide(); + scope.putDigit(2); + scope.multiply(); + scope.putDigit(7); + + var resp = scope.equals(); + scope.$apply(); + httpBackend.flush(); + + resp.$promise.then(function(){ + scope.output.should.equals('-7'); + done(); + }); + + scope.$apply(); + + }); + + it('should calculate 2 + 23.4 - 4.4/2.12 * 7.5 and show the result, 9.499999999999998, on the screen', function(done){ + + scope.putDigit(2); + scope.add(); + scope.putDigit(2); + scope.putDigit(3); + scope.putDigit('.'); + scope.putDigit(4); + scope.subtract(); + scope.putDigit(4); + scope.putDigit('.'); + scope.putDigit(4); + scope.divide(); + scope.putDigit(2); + scope.putDigit('.'); + scope.putDigit(1); + scope.putDigit(2); + scope.multiply(); + scope.putDigit(7); + scope.putDigit('.'); + scope.putDigit(5); + + + var resp = scope.equals(); + scope.$apply(); + httpBackend.flush(); + + resp.$promise.then(function(){ + scope.output.should.equals('9.499999999999998'); + done(); + }); + + scope.$apply(); + + }); + }); +}) \ No newline at end of file diff --git a/lightcalc/assets/js/tests/OperationsList.test.js b/lightcalc/assets/js/tests/OperationsList.test.js new file mode 100644 index 0000000..1d5cda5 --- /dev/null +++ b/lightcalc/assets/js/tests/OperationsList.test.js @@ -0,0 +1,67 @@ +describe('calculatorRetrieveCtrl', function(){ + var scope, location, httpBackend; + + beforeEach(function(){ + module('LightCalc'); + module('MyTemplates'); + }); + + beforeEach(function(done){ + inject(function($rootScope, $controller, $location, $httpBackend){ + scope = $rootScope.$new(); + location = $location; + location.path('/hugobessa'); + + $controller('calculatorRetrieveCtrl', { + "$scope": scope, + "$location": location, + "$routeParams": {name: 'hugobessa'} + }); + + httpBackend = $httpBackend; + httpBackend.when('GET', '/calculator?name=hugobessa').respond([{ + "operations": [ + { + "id": 400, + "text": '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + "calculator": 40, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }, + + { + "id": 401, + "text": '2 + 5 - 4 ÷ 2 x 7', + "calculator": 40, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }, + ], + "name": "hugobessa", + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + "id": 40 + }]); + + scope.$apply(); + httpBackend.flush(); + + scope.$watch(function(){ + return scope.calculator; + }, function(nv, ov){ + if(nv !== undefined){ + done(); + } + }); + + scope.$apply(); + }); + }); + + describe('Check list of operations', function(){ + it('should show operations 2 + 23.4 - 4.4 ÷ 2.12 x 7.5 and 2 + 5 - 4/2 * 7 on the operations list', function(){ + scope.calculator.operations[0].text.should.equals('2 + 23.4 - 4.4 ÷ 2.12 x 7.5'); + scope.calculator.operations[1].text.should.equals('2 + 5 - 4 ÷ 2 x 7'); + }); + }); +}); \ No newline at end of file diff --git a/lightcalc/assets/js/tests/OperationsLoad.test.js b/lightcalc/assets/js/tests/OperationsLoad.test.js new file mode 100644 index 0000000..b642327 --- /dev/null +++ b/lightcalc/assets/js/tests/OperationsLoad.test.js @@ -0,0 +1,102 @@ +describe('calculatorRetrieveCtrl', function(){ + var scope, location, httpBackend; + + beforeEach(function(){ + module('LightCalc'); + module('MyTemplates'); + }); + + beforeEach(function(done){ + inject(function($rootScope, $controller, $location, $httpBackend){ + scope = $rootScope.$new(); + location = $location; + location.path('/hugobessa'); + + $controller('calculatorRetrieveCtrl', { + "$scope": scope, + "$location": location, + "$routeParams": {name: 'hugobessa'} + }); + + httpBackend = $httpBackend; + httpBackend.when('GET', '/calculator?name=hugobessa').respond([{ + "operations": [ + { + "id": 400, + "text": '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + "calculator": 40, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }, + + { + "id": 401, + "text": '2 + 5 - 4 ÷ 2 x 7', + "calculator": 40, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }, + ], + "name": "hugobessa", + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + "id": 40 + }]); + + httpBackend.when('POST', '/operation', { + 'text': '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + 'calculator': 40 + }).respond({ + "id": 400, + "text": '2 + 23.4 - 4.4 ÷ 2.12 x 7.5', + "calculator": { + "operations": [400], + "name": "hugobessa", + "createdAt": "2015-06-10T05:19:54.810Z", + "updatedAt": "2015-06-10T05:19:54.810Z", + "id": 40 + }, + "createdAt": "2015-06-12T05:19:54.810Z", + "updatedAt": "2015-06-12T05:19:54.810Z", + }); + + scope.$apply(); + httpBackend.flush(); + + scope.$watch(function(){ + return scope.calculator; + }, function(nv, ov){ + if(nv !== undefined){ + done(); + } + }); + + scope.$apply(); + }); + }); + + describe('Load operation', function(){ + it('should show operation 2 + 23.4 - 4.4 ÷ 2.12 x 7.5 on the screen', function(){ + scope.loadOperation(scope.calculator.operations[0]); + + scope.output.should.equals('2 + 23.4 - 4.4 ÷ 2.12 x 7.5'); + }); + + it('should show operation 2 + 23.4 - 4.4 ÷ 2.12 x 7.5 result, 9.499999999999998, on the screen', function(done){ + scope.loadOperation(scope.calculator.operations[0]); + + scope.output.should.equals('2 + 23.4 - 4.4 ÷ 2.12 x 7.5'); + + var resp = scope.equals(); + scope.$apply(); + httpBackend.flush(); + + resp.$promise.then(function(){ + scope.output.should.equals('9.499999999999998'); + done(); + }); + + scope.$apply(); + }); + }); +}); \ No newline at end of file diff --git a/lightcalc/assets/robots.txt b/lightcalc/assets/robots.txt new file mode 100644 index 0000000..33cd27d --- /dev/null +++ b/lightcalc/assets/robots.txt @@ -0,0 +1,8 @@ +# The robots.txt file is used to control how search engines index your live URLs. +# See http://www.robotstxt.org/wc/norobots.html for more information. + + + +# To prevent search engines from seeing the site altogether, uncomment the next two lines: +# User-Agent: * +# Disallow: / diff --git a/lightcalc/assets/styles/core.less b/lightcalc/assets/styles/core.less new file mode 100644 index 0000000..785b014 --- /dev/null +++ b/lightcalc/assets/styles/core.less @@ -0,0 +1,206 @@ +/* Basic reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + + /* Better text styling */ + font: bold 14px Arial, sans-serif; +} + +/* Finally adding some IE9 fallbacks for gradients to finish things up */ + +/* A nice BG gradient */ +html { + height: 100%; + background: white; + background: radial-gradient(circle, #fff 20%, #ccc); + background-size: cover; +} + +body { + background: transparent; +} + +/* Using box shadows to create 3D effects */ + +.form-group .help-block p.error { + display: none; +} + +.form-group.has-error .help-block p.error { + display: block; +} + +.container { + margin: 100px auto; + width: 575px; + height: auto; + padding: 0; +} + +.operations { + width: 250px; + height: 264px; + float: left; + overflow: auto; + padding: 20px 20px 9px; + background: #F1FF92; + color: #888; + box-shadow: 0px 4px #009de4, 0px 10px 15px rgba(0, 0, 0, 0.2); + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} + +.operations > ul { + list-style: none; +} + +.operations > ul > li { + width: 100%; + padding: 10px; + background: white; + box-shadow: 0px 4px 5px rgba(0, 0, 0, 0.2); + margin-bottom: 10px; + cursor: pointer; +} + +.operations > h4 { + font-weight: 700; +} + +.calculator { + width: 325px; + height: auto; + float: right; + + padding: 20px 20px 9px; + + background: #9dd2ea; + background: linear-gradient(#9dd2ea, #8bceec); + border-bottom-right-radius: 6px; + border-top-right-radius: 6px; + box-shadow: 0px 4px #009de4, 0px 10px 15px rgba(0, 0, 0, 0.2); +} + +/* Top portion */ +.top span.clear { + float: left; +} + +/* Inset shadow on the screen to create indent */ +.top .screen { + position: relative; + height: 40px; + width: 212px; + + float: right; + + padding: 0 10px; + + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; + box-shadow: inset 0px 4px rgba(0, 0, 0, 0.2); + + overflow: hidden; +} + +.top .screen .numbers { + position: absolute; + right: 10px; + white-space: nowrap; + letter-spacing: 1px; + line-height: 40px; + color: white; + font-size: 17px; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); +} + +/* Clear floats */ +.keys, .top {overflow: hidden;} + +/* Applying same to the keys */ +.keys span, .top span.clear { + float: left; + position: relative; + top: 0; + + cursor: pointer; + + width: 66px; + height: 36px; + + background: white; + border-radius: 3px; + box-shadow: 0px 4px rgba(0, 0, 0, 0.2); + + margin: 0 7px 11px 0; + + color: #888; + line-height: 36px; + text-align: center; + + /* prevent selection of text inside keys */ + user-select: none; + + /* Smoothing out hover and active states using css3 transitions */ + transition: all 0.2s ease; +} + +/* Remove right margins from operator keys */ +/* style different type of keys (operators/evaluate/clear) differently */ +.keys span.operator { + background: #FFF0F5; + margin-right: 0; +} + +.keys span.eval { + background: #f1ff92; + box-shadow: 0px 4px #9da853; + color: #888e5f; +} + +.top span.clear { + background: #ff9fa8; + box-shadow: 0px 4px #ff7c87; + color: white; +} + +/* Some hover effects */ +.keys span:hover { + background: #9c89f6; + box-shadow: 0px 4px #6b54d3; + color: white; +} + +.keys span.eval:hover { + background: #abb850; + box-shadow: 0px 4px #717a33; + color: #ffffff; +} + +.top span.clear:hover { + background: #f68991; + box-shadow: 0px 4px #d3545d; + color: white; +} + +/* Simulating "pressed" effect on active state of the keys by removing the box-shadow and moving the keys down a bit */ +.keys span:active { + box-shadow: 0px 0px #6b54d3; + top: 4px; +} + +.keys span.eval:active { + box-shadow: 0px 0px #717a33; + top: 4px; +} + +.top span.clear:active { + top: 4px; + box-shadow: 0px 0px #d3545d; +} + +.uppercase { + text-transform: uppercase; +} \ No newline at end of file diff --git a/lightcalc/assets/templates/.gitkeep b/lightcalc/assets/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lightcalc/assets/templates/calculator/create.html b/lightcalc/assets/templates/calculator/create.html new file mode 100644 index 0000000..84a8f9b --- /dev/null +++ b/lightcalc/assets/templates/calculator/create.html @@ -0,0 +1,27 @@ +
+

Crie uma calculadora para você! Basta dar um nome a ela!

+
+
+
+ + + +

+ O nome da calculadora é obrigatório + O nome da calculadora deve ter pelo menos 8 caracteres + O nome da calculadora deve ter no máximo 40 caracteres + O nome da calculadora pode conter letras maiúsculas e minúsculas + O nome da calculadora solicitado não está disponível +

+
+
+
+
+ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/lightcalc/assets/templates/calculator/retrieve.html b/lightcalc/assets/templates/calculator/retrieve.html new file mode 100644 index 0000000..19283bf --- /dev/null +++ b/lightcalc/assets/templates/calculator/retrieve.html @@ -0,0 +1,38 @@ +
+

Calculadora {{calculator.name}}

+
+

OPERAÇÕES

+
    +
  • {{operation.text}}
  • +
+
+
+ + +
+ C +
{{output}}
+
+ +
+ + 7 + 8 + 9 + + + 4 + 5 + 6 + - + 1 + 2 + 3 + ÷ + 0 + . + = + x +
+ +
+
\ No newline at end of file diff --git a/lightcalc/bower.json b/lightcalc/bower.json new file mode 100644 index 0000000..26ad53e --- /dev/null +++ b/lightcalc/bower.json @@ -0,0 +1,26 @@ +{ + "name": "LightCalc", + "version": "0.0.0", + "homepage": "https://github.com/hugobessa/lightcalc", + "authors": [ + "Hugo Bessa " + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "angular-resource": "~1.4.0", + "angular-messages": "~1.4.0", + "bootstrap": "~3.3.4", + "angular": "~1.4.0", + "jquery": "~2.1.4", + "angular-route": "~1.4.0", + "angular-show-errors": "~2.4.3", + "angular-mocks": "~1.4.1" + } +} diff --git a/lightcalc/config/blueprints.js b/lightcalc/config/blueprints.js new file mode 100644 index 0000000..8bd34f2 --- /dev/null +++ b/lightcalc/config/blueprints.js @@ -0,0 +1,162 @@ +/** + * Blueprint API Configuration + * (sails.config.blueprints) + * + * These settings are for the global configuration of blueprint routes and + * request options (which impact the behavior of blueprint actions). + * + * You may also override any of these settings on a per-controller basis + * by defining a '_config' key in your controller defintion, and assigning it + * a configuration object with overrides for the settings in this file. + * A lot of the configuration options below affect so-called "CRUD methods", + * or your controllers' `find`, `create`, `update`, and `destroy` actions. + * + * It's important to realize that, even if you haven't defined these yourself, as long as + * a model exists with the same name as the controller, Sails will respond with built-in CRUD + * logic in the form of a JSON API, including support for sort, pagination, and filtering. + * + * For more information on the blueprint API, check out: + * http://sailsjs.org/#!/documentation/reference/blueprint-api + * + * For more information on the settings in this file, see: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.blueprints.html + * + */ + +module.exports.blueprints = { + + /*************************************************************************** + * * + * Action routes speed up the backend development workflow by * + * eliminating the need to manually bind routes. When enabled, GET, POST, * + * PUT, and DELETE routes will be generated for every one of a controller's * + * actions. * + * * + * If an `index` action exists, additional naked routes will be created for * + * it. Finally, all `actions` blueprints support an optional path * + * parameter, `id`, for convenience. * + * * + * `actions` are enabled by default, and can be OK for production-- * + * however, if you'd like to continue to use controller/action autorouting * + * in a production deployment, you must take great care not to * + * inadvertently expose unsafe/unintentional controller logic to GET * + * requests. * + * * + ***************************************************************************/ + + // actions: true, + + /*************************************************************************** + * * + * RESTful routes (`sails.config.blueprints.rest`) * + * * + * REST blueprints are the automatically generated routes Sails uses to * + * expose a conventional REST API on top of a controller's `find`, * + * `create`, `update`, and `destroy` actions. * + * * + * For example, a BoatController with `rest` enabled generates the * + * following routes: * + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::: * + * GET /boat -> BoatController.find * + * GET /boat/:id -> BoatController.findOne * + * POST /boat -> BoatController.create * + * PUT /boat/:id -> BoatController.update * + * DELETE /boat/:id -> BoatController.destroy * + * * + * `rest` blueprint routes are enabled by default, and are suitable for use * + * in a production scenario, as long you take standard security precautions * + * (combine w/ policies, etc.) * + * * + ***************************************************************************/ + + // rest: true, + + /*************************************************************************** + * * + * Shortcut routes are simple helpers to provide access to a * + * controller's CRUD methods from your browser's URL bar. When enabled, * + * GET, POST, PUT, and DELETE routes will be generated for the * + * controller's`find`, `create`, `update`, and `destroy` actions. * + * * + * `shortcuts` are enabled by default, but should be disabled in * + * production. * + * * + ***************************************************************************/ + + // shortcuts: true, + + /*************************************************************************** + * * + * An optional mount path for all blueprint routes on a controller, * + * including `rest`, `actions`, and `shortcuts`. This allows you to take * + * advantage of blueprint routing, even if you need to namespace your API * + * methods. * + * * + * (NOTE: This only applies to blueprint autoroutes, not manual routes from * + * `sails.config.routes`) * + * * + ***************************************************************************/ + + // prefix: '', + + /*************************************************************************** + * * + * An optional mount path for all REST blueprint routes on a controller. * + * And it do not include `actions` and `shortcuts` routes. * + * This allows you to take advantage of REST blueprint routing, * + * even if you need to namespace your RESTful API methods * + * * + ***************************************************************************/ + + // restPrefix: '', + + /*************************************************************************** + * * + * Whether to pluralize controller names in blueprint routes. * + * * + * (NOTE: This only applies to blueprint autoroutes, not manual routes from * + * `sails.config.routes`) * + * * + * For example, REST blueprints for `FooController` with `pluralize` * + * enabled: * + * GET /foos/:id? * + * POST /foos * + * PUT /foos/:id? * + * DELETE /foos/:id? * + * * + ***************************************************************************/ + + // pluralize: false, + + /*************************************************************************** + * * + * Whether the blueprint controllers should populate model fetches with * + * data from other models which are linked by associations * + * * + * If you have a lot of data in one-to-many associations, leaving this on * + * may result in very heavy api calls * + * * + ***************************************************************************/ + + // populate: true, + + /**************************************************************************** + * * + * Whether to run Model.watch() in the find and findOne blueprint actions. * + * Can be overridden on a per-model basis. * + * * + ****************************************************************************/ + + // autoWatch: true, + + /**************************************************************************** + * * + * The default number of records to show in the response from a "find" * + * action. Doubles as the default size of populated arrays if populate is * + * true. * + * * + ****************************************************************************/ + + // defaultLimit: 30 + +}; diff --git a/lightcalc/config/bootstrap.js b/lightcalc/config/bootstrap.js new file mode 100644 index 0000000..96208a1 --- /dev/null +++ b/lightcalc/config/bootstrap.js @@ -0,0 +1,17 @@ +/** + * Bootstrap + * (sails.config.bootstrap) + * + * An asynchronous bootstrap function that runs before your Sails app gets lifted. + * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. + * + * For more information on bootstrapping your app, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.bootstrap.html + */ + +module.exports.bootstrap = function(cb) { + + // It's very important to trigger this callback method when you are finished + // with the bootstrap! (otherwise your server will never lift, since it's waiting on the bootstrap) + cb(); +}; diff --git a/lightcalc/config/connections.js b/lightcalc/config/connections.js new file mode 100644 index 0000000..bf9b49a --- /dev/null +++ b/lightcalc/config/connections.js @@ -0,0 +1,94 @@ +/** + * Connections + * (sails.config.connections) + * + * `Connections` are like "saved settings" for your adapters. What's the difference between + * a connection and an adapter, you might ask? An adapter (e.g. `sails-mysql`) is generic-- + * it needs some additional information to work (e.g. your database host, password, user, etc.) + * A `connection` is that additional information. + * + * Each model must have a `connection` property (a string) which is references the name of one + * of these connections. If it doesn't, the default `connection` configured in `config/models.js` + * will be applied. Of course, a connection can (and usually is) shared by multiple models. + * . + * Note: If you're using version control, you should put your passwords/api keys + * in `config/local.js`, environment variables, or use another strategy. + * (this is to prevent you inadvertently sensitive credentials up to your repository.) + * + * For more information on configuration, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.connections.html + */ + +module.exports.connections = { + + /*************************************************************************** + * * + * Local disk storage for DEVELOPMENT ONLY * + * * + * Installed by default. * + * * + ***************************************************************************/ + localDiskDb: { + adapter: 'sails-disk' + }, + + /*************************************************************************** + * * + * MySQL is the world's most popular relational database. * + * http://en.wikipedia.org/wiki/MySQL * + * * + * Run: npm install sails-mysql * + * * + ***************************************************************************/ + someMysqlServer: { + adapter: 'sails-mysql', + host: 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', + user: 'YOUR_MYSQL_USER', + password: 'YOUR_MYSQL_PASSWORD', + database: 'YOUR_MYSQL_DB' + }, + + /*************************************************************************** + * * + * MongoDB is the leading NoSQL database. * + * http://en.wikipedia.org/wiki/MongoDB * + * * + * Run: npm install sails-mongo * + * * + ***************************************************************************/ + someMongodbServer: { + adapter: 'sails-mongo', + // host: 'localhost', + // port: 27017, + // user: 'username', + // password: 'password', + // database: 'your_mongo_db_name_here' + url: process.env.MONGOLAB_URI || 'mongodb://heroku_lwjz5hqq:g0asvb54o1a54vm85r8sqh8lm6@ds043972.mongolab.com:43972/heroku_lwjz5hqq', + schema: 'true' + }, + + /*************************************************************************** + * * + * PostgreSQL is another officially supported relational database. * + * http://en.wikipedia.org/wiki/PostgreSQL * + * * + * Run: npm install sails-postgresql * + * * + * * + ***************************************************************************/ + somePostgresqlServer: { + adapter: 'sails-postgresql', + host: 'YOUR_POSTGRES_SERVER_HOSTNAME_OR_IP_ADDRESS', + user: 'YOUR_POSTGRES_USER', + password: 'YOUR_POSTGRES_PASSWORD', + database: 'YOUR_POSTGRES_DB' + } + + + /*************************************************************************** + * * + * More adapters: https://github.com/balderdashy/sails * + * * + ***************************************************************************/ + +}; diff --git a/lightcalc/config/cors.js b/lightcalc/config/cors.js new file mode 100644 index 0000000..9939d58 --- /dev/null +++ b/lightcalc/config/cors.js @@ -0,0 +1,78 @@ +/** + * Cross-Origin Resource Sharing (CORS) Settings + * (sails.config.cors) + * + * CORS is like a more modern version of JSONP-- it allows your server/API + * to successfully respond to requests from client-side JavaScript code + * running on some other domain (e.g. google.com) + * Unlike JSONP, it works with POST, PUT, and DELETE requests + * + * For more information on CORS, check out: + * http://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * + * Note that any of these settings (besides 'allRoutes') can be changed on a per-route basis + * by adding a "cors" object to the route configuration: + * + * '/get foo': { + * controller: 'foo', + * action: 'bar', + * cors: { + * origin: 'http://foobar.com,https://owlhoot.com' + * } + * } + * + * For more information on this configuration file, see: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.cors.html + * + */ + +module.exports.cors = { + + /*************************************************************************** + * * + * Allow CORS on all routes by default? If not, you must enable CORS on a * + * per-route basis by either adding a "cors" configuration object to the * + * route config, or setting "cors:true" in the route config to use the * + * default settings below. * + * * + ***************************************************************************/ + + // allRoutes: false, + + /*************************************************************************** + * * + * Which domains which are allowed CORS access? This can be a * + * comma-delimited list of hosts (beginning with http:// or https://) or * + * "*" to allow all domains CORS access. * + * * + ***************************************************************************/ + + // origin: '*', + + /*************************************************************************** + * * + * Allow cookies to be shared for CORS requests? * + * * + ***************************************************************************/ + + // credentials: true, + + /*************************************************************************** + * * + * Which methods should be allowed for CORS requests? This is only used in * + * response to preflight requests (see article linked above for more info) * + * * + ***************************************************************************/ + + // methods: 'GET, POST, PUT, DELETE, OPTIONS, HEAD', + + /*************************************************************************** + * * + * Which headers should be allowed for CORS requests? This is only used in * + * response to preflight requests. * + * * + ***************************************************************************/ + + // headers: 'content-type' + +}; diff --git a/lightcalc/config/csrf.js b/lightcalc/config/csrf.js new file mode 100644 index 0000000..5667e0b --- /dev/null +++ b/lightcalc/config/csrf.js @@ -0,0 +1,64 @@ +/** + * Cross-Site Request Forgery Protection Settings + * (sails.config.csrf) + * + * CSRF tokens are like a tracking chip. While a session tells the server that a user + * "is who they say they are", a csrf token tells the server "you are where you say you are". + * + * When enabled, all non-GET requests to the Sails server must be accompanied by + * a special token, identified as the '_csrf' parameter. + * + * This option protects your Sails app against cross-site request forgery (or CSRF) attacks. + * A would-be attacker needs not only a user's session cookie, but also this timestamped, + * secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. + * + * This allows us to have certainty that our users' requests haven't been hijacked, + * and that the requests they're making are intentional and legitimate. + * + * This token has a short-lived expiration timeline, and must be acquired by either: + * + * (a) For traditional view-driven web apps: + * Fetching it from one of your views, where it may be accessed as + * a local variable, e.g.: + *
+ * + *
+ * + * or (b) For AJAX/Socket-heavy and/or single-page apps: + * Sending a GET request to the `/csrfToken` route, where it will be returned + * as JSON, e.g.: + * { _csrf: 'ajg4JD(JGdajhLJALHDa' } + * + * + * Enabling this option requires managing the token in your front-end app. + * For traditional web apps, it's as easy as passing the data from a view into a form action. + * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. + * + * For more information on CSRF, check out: + * http://en.wikipedia.org/wiki/Cross-site_request_forgery + * + * For more information on this configuration file, including info on CSRF + CORS, see: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.csrf.html + * + */ + +/**************************************************************************** +* * +* Enabled CSRF protection for your site? * +* * +****************************************************************************/ + +// module.exports.csrf = false; + +/**************************************************************************** +* * +* You may also specify more fine-grained settings for CSRF, including the * +* domains which are allowed to request the CSRF token via AJAX. These * +* settings override the general CORS settings in your config/cors.js file. * +* * +****************************************************************************/ + +// module.exports.csrf = { +// grantTokenViaAjax: true, +// origin: '' +// } diff --git a/lightcalc/config/env/development.js b/lightcalc/config/env/development.js new file mode 100644 index 0000000..ced04f4 --- /dev/null +++ b/lightcalc/config/env/development.js @@ -0,0 +1,25 @@ +/** + * Development environment settings + * + * This file can include shared settings for a development team, + * such as API keys or remote database passwords. If you're using + * a version control solution for your Sails app, this file will + * be committed to your repository unless you add it to your .gitignore + * file. If your repository will be publicly viewable, don't add + * any private information to this file! + * + */ + +module.exports = { + + /*************************************************************************** + * Set the default database connection for models in the development * + * environment (see config/connections.js and config/models.js ) * + ***************************************************************************/ + + models: { + migrate: 'safe', + connection: 'localDiskDb' + } + +}; diff --git a/lightcalc/config/env/production.js b/lightcalc/config/env/production.js new file mode 100644 index 0000000..97bbd35 --- /dev/null +++ b/lightcalc/config/env/production.js @@ -0,0 +1,39 @@ +/** + * Production environment settings + * + * This file can include shared settings for a production environment, + * such as API keys or remote database passwords. If you're using + * a version control solution for your Sails app, this file will + * be committed to your repository unless you add it to your .gitignore + * file. If your repository will be publicly viewable, don't add + * any private information to this file! + * + */ + +module.exports = { + + /*************************************************************************** + * Set the default database connection for models in the production * + * environment (see config/connections.js and config/models.js ) * + ***************************************************************************/ + + models: { + migrate: 'safe', + connection: 'someMongodbServer' + }, + + /*************************************************************************** + * Set the port in the production environment to 80 * + ***************************************************************************/ + + port: process.env.PORT || 5000, + + /*************************************************************************** + * Set the log level in production environment to "silent" * + ***************************************************************************/ + + log: { + level: "silent" + } + +}; diff --git a/lightcalc/config/globals.js b/lightcalc/config/globals.js new file mode 100644 index 0000000..c1819c3 --- /dev/null +++ b/lightcalc/config/globals.js @@ -0,0 +1,63 @@ +/** + * Global Variable Configuration + * (sails.config.globals) + * + * Configure which global variables which will be exposed + * automatically by Sails. + * + * For more information on configuration, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.globals.html + */ +module.exports.globals = { + + /**************************************************************************** + * * + * Expose the lodash installed in Sails core as a global variable. If this * + * is disabled, like any other node module you can always run npm install * + * lodash --save, then var _ = require('lodash') at the top of any file. * + * * + ****************************************************************************/ + + // _: true, + + /**************************************************************************** + * * + * Expose the async installed in Sails core as a global variable. If this is * + * disabled, like any other node module you can always run npm install async * + * --save, then var async = require('async') at the top of any file. * + * * + ****************************************************************************/ + + // async: true, + + /**************************************************************************** + * * + * Expose the sails instance representing your app. If this is disabled, you * + * can still get access via req._sails. * + * * + ****************************************************************************/ + + // sails: true, + + /**************************************************************************** + * * + * Expose each of your app's services as global variables (using their * + * "globalId"). E.g. a service defined in api/models/NaturalLanguage.js * + * would have a globalId of NaturalLanguage by default. If this is disabled, * + * you can still access your services via sails.services.* * + * * + ****************************************************************************/ + + // services: true, + + /**************************************************************************** + * * + * Expose each of your app's models as global variables (using their * + * "globalId"). E.g. a model defined in api/models/User.js would have a * + * globalId of User by default. If this is disabled, you can still access * + * your models via sails.models.*. * + * * + ****************************************************************************/ + + // models: true +}; diff --git a/lightcalc/config/http.js b/lightcalc/config/http.js new file mode 100644 index 0000000..6725416 --- /dev/null +++ b/lightcalc/config/http.js @@ -0,0 +1,87 @@ +/** + * HTTP Server Settings + * (sails.config.http) + * + * Configuration for the underlying HTTP server in Sails. + * Only applies to HTTP requests (not WebSockets) + * + * For more information on configuration, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.http.html + */ + +module.exports.http = { + + /**************************************************************************** + * * + * Express middleware to use for every Sails request. To add custom * + * middleware to the mix, add a function to the middleware config object and * + * add its key to the "order" array. The $custom key is reserved for * + * backwards-compatibility with Sails v0.9.x apps that use the * + * `customMiddleware` config option. * + * * + ****************************************************************************/ + + // middleware: { + + /*************************************************************************** + * * + * The order in which middleware should be run for HTTP request. (the Sails * + * router is invoked by the "router" middleware below.) * + * * + ***************************************************************************/ + + // order: [ + // 'startRequestTimer', + // 'cookieParser', + // 'session', + // 'myRequestLogger', + // 'bodyParser', + // 'handleBodyParserError', + // 'compress', + // 'methodOverride', + // 'poweredBy', + // '$custom', + // 'router', + // 'www', + // 'favicon', + // '404', + // '500' + // ], + + /**************************************************************************** + * * + * Example custom middleware; logs each request to the console. * + * * + ****************************************************************************/ + + // myRequestLogger: function (req, res, next) { + // console.log("Requested :: ", req.method, req.url); + // return next(); + // } + + + /*************************************************************************** + * * + * The body parser that will handle incoming multipart HTTP requests. By * + * default as of v0.10, Sails uses * + * [skipper](http://github.com/balderdashy/skipper). See * + * http://www.senchalabs.org/connect/multipart.html for other options. * + * * + ***************************************************************************/ + + // bodyParser: require('skipper') + + // }, + + /*************************************************************************** + * * + * The number of seconds to cache flat files on disk being served by * + * Express static middleware (by default, these files are in `.tmp/public`) * + * * + * The HTTP static cache is only active in a 'production' environment, * + * since that's the only time Express will cache flat-files. * + * * + ***************************************************************************/ + + // cache: 31557600000 +}; diff --git a/lightcalc/config/i18n.js b/lightcalc/config/i18n.js new file mode 100644 index 0000000..facf707 --- /dev/null +++ b/lightcalc/config/i18n.js @@ -0,0 +1,57 @@ +/** + * Internationalization / Localization Settings + * (sails.config.i18n) + * + * If your app will touch people from all over the world, i18n (or internationalization) + * may be an important part of your international strategy. + * + * + * For more informationom i18n in Sails, check out: + * http://sailsjs.org/#!/documentation/concepts/Internationalization + * + * For a complete list of i18n options, see: + * https://github.com/mashpie/i18n-node#list-of-configuration-options + * + * + */ + +module.exports.i18n = { + + /*************************************************************************** + * * + * Which locales are supported? * + * * + ***************************************************************************/ + + // locales: ['en', 'es', 'fr', 'de'], + + /**************************************************************************** + * * + * What is the default locale for the site? Note that this setting will be * + * overridden for any request that sends an "Accept-Language" header (i.e. * + * most browsers), but it's still useful if you need to localize the * + * response for requests made by non-browser clients (e.g. cURL). * + * * + ****************************************************************************/ + + // defaultLocale: 'en', + + /**************************************************************************** + * * + * Automatically add new keys to locale (translation) files when they are * + * encountered during a request? * + * * + ****************************************************************************/ + + // updateFiles: false, + + /**************************************************************************** + * * + * Path (relative to app root) of directory to store locale (translation) * + * files in. * + * * + ****************************************************************************/ + + // localesDirectory: '/config/locales' + +}; diff --git a/lightcalc/config/locales/_README.md b/lightcalc/config/locales/_README.md new file mode 100644 index 0000000..5f89b15 --- /dev/null +++ b/lightcalc/config/locales/_README.md @@ -0,0 +1,28 @@ +# Internationalization / Localization Settings + +> Also see the official docs on internationalization/localization: +> http://links.sailsjs.org/docs/config/locales + +## Locales +All locale files live under `config/locales`. Here is where you can add translations +as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on request headers. + +Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): +```json +{ + "Hello!": "Hola!", + "Hello %s, how are you today?": "¿Hola %s, como estas?", +} +``` +## Usage +Locales can be accessed in controllers/policies through `res.i18n()`, or in views through the `__(key)` or `i18n(key)` functions. +Remember that the keys are case sensitive and require exact key matches, e.g. + +```ejs +

<%= __('Welcome to PencilPals!') %>

+

<%= i18n('Hello %s, how are you today?', 'Pencil Maven') %>

+

<%= i18n('That\'s right-- you can use either i18n() or __()') %>

+``` + +## Configuration +Localization/internationalization config can be found in `config/i18n.js`, from where you can set your supported locales. diff --git a/lightcalc/config/locales/de.json b/lightcalc/config/locales/de.json new file mode 100644 index 0000000..12f23d9 --- /dev/null +++ b/lightcalc/config/locales/de.json @@ -0,0 +1,4 @@ +{ + "Welcome": "Willkommen", + "A brand new app.": "Eine neue App." +} diff --git a/lightcalc/config/locales/en.json b/lightcalc/config/locales/en.json new file mode 100644 index 0000000..6eeeb33 --- /dev/null +++ b/lightcalc/config/locales/en.json @@ -0,0 +1,4 @@ +{ + "Welcome": "Welcome", + "A brand new app.": "A brand new app." +} diff --git a/lightcalc/config/locales/es.json b/lightcalc/config/locales/es.json new file mode 100644 index 0000000..91f8248 --- /dev/null +++ b/lightcalc/config/locales/es.json @@ -0,0 +1,4 @@ +{ + "Welcome": "Bienvenido", + "A brand new app.": "Una aplicación de la nueva marca." +} diff --git a/lightcalc/config/locales/fr.json b/lightcalc/config/locales/fr.json new file mode 100644 index 0000000..972935c --- /dev/null +++ b/lightcalc/config/locales/fr.json @@ -0,0 +1,4 @@ +{ + "Welcome": "Bienvenue", + "A brand new app.": "Une toute nouvelle application." +} diff --git a/lightcalc/config/log.js b/lightcalc/config/log.js new file mode 100644 index 0000000..e192529 --- /dev/null +++ b/lightcalc/config/log.js @@ -0,0 +1,29 @@ +/** + * Built-in Log Configuration + * (sails.config.log) + * + * Configure the log level for your app, as well as the transport + * (Underneath the covers, Sails uses Winston for logging, which + * allows for some pretty neat custom transports/adapters for log messages) + * + * For more information on the Sails logger, check out: + * http://sailsjs.org/#!/documentation/concepts/Logging + */ + +module.exports.log = { + + /*************************************************************************** + * * + * Valid `level` configs: i.e. the minimum log level to capture with * + * sails.log.*() * + * * + * The order of precedence for log levels from lowest to highest is: * + * silly, verbose, info, debug, warn, error * + * * + * You may also set the level to "silent" to suppress all logs. * + * * + ***************************************************************************/ + + // level: 'info' + +}; diff --git a/lightcalc/config/models.js b/lightcalc/config/models.js new file mode 100644 index 0000000..49ac9ee --- /dev/null +++ b/lightcalc/config/models.js @@ -0,0 +1,32 @@ +/** + * Default model configuration + * (sails.config.models) + * + * Unless you override them, the following properties will be included + * in each of your models. + * + * For more info on Sails models, see: + * http://sailsjs.org/#!/documentation/concepts/ORM + */ + +module.exports.models = { + + /*************************************************************************** + * * + * Your app's default connection. i.e. the name of one of your app's * + * connections (see `config/connections.js`) * + * * + ***************************************************************************/ + // connection: 'localDiskDb', + + /*************************************************************************** + * * + * How and whether Sails will attempt to automatically rebuild the * + * tables/collections/etc. in your schema. * + * * + * See http://sailsjs.org/#!/documentation/concepts/ORM/model-settings.html * + * * + ***************************************************************************/ + // migrate: 'alter' + +}; diff --git a/lightcalc/config/policies.js b/lightcalc/config/policies.js new file mode 100644 index 0000000..1785d4e --- /dev/null +++ b/lightcalc/config/policies.js @@ -0,0 +1,51 @@ +/** + * Policy Mappings + * (sails.config.policies) + * + * Policies are simple functions which run **before** your controllers. + * You can apply one or more policies to a given controller, or protect + * its actions individually. + * + * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed + * below by its filename, minus the extension, (e.g. "authenticated") + * + * For more information on how policies work, see: + * http://sailsjs.org/#!/documentation/concepts/Policies + * + * For more information on configuring policies, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.policies.html + */ + + +module.exports.policies = { + + /*************************************************************************** + * * + * Default policy for all controllers and actions (`true` allows public * + * access) * + * * + ***************************************************************************/ + + // '*': true, + + /*************************************************************************** + * * + * Here's an example of mapping some policies to run before a controller * + * and its actions * + * * + ***************************************************************************/ + // RabbitController: { + + // Apply the `false` policy as the default for all of RabbitController's actions + // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) + // '*': false, + + // For the action `nurture`, apply the 'isRabbitMother' policy + // (this overrides `false` above) + // nurture : 'isRabbitMother', + + // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies + // before letting any users feed our rabbits + // feed : ['isNiceToAnimals', 'hasRabbitFood'] + // } +}; diff --git a/lightcalc/config/routes.js b/lightcalc/config/routes.js new file mode 100644 index 0000000..a14c9cb --- /dev/null +++ b/lightcalc/config/routes.js @@ -0,0 +1,49 @@ +/** + * Route Mappings + * (sails.config.routes) + * + * Your routes map URLs to views and controllers. + * + * If Sails receives a URL that doesn't match any of the routes below, + * it will check for matching files (images, scripts, stylesheets, etc.) + * in your assets directory. e.g. `http://localhost:1337/images/foo.jpg` + * might match an image file: `/assets/images/foo.jpg` + * + * Finally, if those don't match either, the default 404 handler is triggered. + * See `api/responses/notFound.js` to adjust your app's 404 logic. + * + * Note: Sails doesn't ACTUALLY serve stuff from `assets`-- the default Gruntfile in Sails copies + * flat files from `assets` to `.tmp/public`. This allows you to do things like compile LESS or + * CoffeeScript for the front-end. + * + * For more information on configuring custom routes, check out: + * http://sailsjs.org/#!/documentation/concepts/Routes/RouteTargetSyntax.html + */ + +module.exports.routes = { + + /*************************************************************************** + * * + * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, * + * etc. depending on your default view engine) your home page. * + * * + * (Alternatively, remove this and add an `index.html` file in your * + * `assets` directory) * + * * + ***************************************************************************/ + + '/' : { + controller : 'home' + } + + /*************************************************************************** + * * + * Custom routes here... * + * * + * If a request to a URL doesn't match any of the custom routes above, it * + * is matched against Sails route blueprints. See `config/blueprints.js` * + * for configuration options and examples. * + * * + ***************************************************************************/ + +}; diff --git a/lightcalc/config/session.js b/lightcalc/config/session.js new file mode 100644 index 0000000..4ea1e63 --- /dev/null +++ b/lightcalc/config/session.js @@ -0,0 +1,91 @@ +/** + * Session Configuration + * (sails.config.session) + * + * Sails session integration leans heavily on the great work already done by + * Express, but also unifies Socket.io with the Connect session store. It uses + * Connect's cookie parser to normalize configuration differences between Express + * and Socket.io and hooks into Sails' middleware interpreter to allow you to access + * and auto-save to `req.session` with Socket.io the same way you would with Express. + * + * For more information on configuring the session, check out: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.session.html + */ + +module.exports.session = { + + /*************************************************************************** + * * + * Session secret is automatically generated when your new app is created * + * Replace at your own risk in production-- you will invalidate the cookies * + * of your users, forcing them to log in again. * + * * + ***************************************************************************/ + secret: 'e3995e183a0e6b370e0ce948721abd4f', + + + /*************************************************************************** + * * + * Set the session cookie expire time The maxAge is set by milliseconds, * + * the example below is for 24 hours * + * * + ***************************************************************************/ + + // cookie: { + // maxAge: 24 * 60 * 60 * 1000 + // }, + + /*************************************************************************** + * * + * In production, uncomment the following lines to set up a shared redis * + * session store that can be shared across multiple Sails.js servers * + ***************************************************************************/ + + // adapter: 'redis', + + /*************************************************************************** + * * + * The following values are optional, if no options are set a redis * + * instance running on localhost is expected. Read more about options at: * + * https://github.com/visionmedia/connect-redis * + * * + * * + ***************************************************************************/ + + // host: 'localhost', + // port: 6379, + // ttl: , + // db: 0, + // pass: , + // prefix: 'sess:', + + + /*************************************************************************** + * * + * Uncomment the following lines to use your Mongo adapter as a session * + * store * + * * + ***************************************************************************/ + + // adapter: 'mongo', + // host: 'localhost', + // port: 27017, + // db: 'sails', + // collection: 'sessions', + + /*************************************************************************** + * * + * Optional Values: * + * * + * # Note: url will override other connection settings url: * + * 'mongodb://user:pass@host:port/database/collection', * + * * + ***************************************************************************/ + + // username: '', + // password: '', + // auto_reconnect: false, + // ssl: false, + // stringify: true + +}; diff --git a/lightcalc/config/sockets.js b/lightcalc/config/sockets.js new file mode 100644 index 0000000..f23b925 --- /dev/null +++ b/lightcalc/config/sockets.js @@ -0,0 +1,141 @@ +/** + * WebSocket Server Settings + * (sails.config.sockets) + * + * These settings provide transparent access to the options for Sails' + * encapsulated WebSocket server, as well as some additional Sails-specific + * configuration layered on top. + * + * For more information on sockets configuration, including advanced config options, see: + * http://sailsjs.org/#!/documentation/reference/sails.config/sails.config.sockets.html + */ + +module.exports.sockets = { + + + /*************************************************************************** + * * + * Node.js (and consequently Sails.js) apps scale horizontally. It's a * + * powerful, efficient approach, but it involves a tiny bit of planning. At * + * scale, you'll want to be able to copy your app onto multiple Sails.js * + * servers and throw them behind a load balancer. * + * * + * One of the big challenges of scaling an application is that these sorts * + * of clustered deployments cannot share memory, since they are on * + * physically different machines. On top of that, there is no guarantee * + * that a user will "stick" with the same server between requests (whether * + * HTTP or sockets), since the load balancer will route each request to the * + * Sails server with the most available resources. However that means that * + * all room/pubsub/socket processing and shared memory has to be offloaded * + * to a shared, remote messaging queue (usually Redis) * + * * + * Luckily, Socket.io (and consequently Sails.js) apps support Redis for * + * sockets by default. To enable a remote redis pubsub server, uncomment * + * the config below. * + * * + * Worth mentioning is that, if `adapter` config is `redis`, but host/port * + * is left unset, Sails will try to connect to redis running on localhost * + * via port 6379 * + * * + ***************************************************************************/ + // adapter: 'memory', + + // + // -OR- + // + + // adapter: 'redis', + // host: '127.0.0.1', + // port: 6379, + // db: 'sails', + // pass: '', + + + + /*************************************************************************** + * * + * Whether to expose a 'get /__getcookie' route with CORS support that sets * + * a cookie (this is used by the sails.io.js socket client to get access to * + * a 3rd party cookie and to enable sessions). * + * * + * Warning: Currently in this scenario, CORS settings apply to interpreted * + * requests sent via a socket.io connection that used this cookie to * + * connect, even for non-browser clients! (e.g. iOS apps, toasters, node.js * + * unit tests) * + * * + ***************************************************************************/ + + // grant3rdPartyCookie: true, + + + + /*************************************************************************** + * * + * `beforeConnect` * + * * + * This custom beforeConnect function will be run each time BEFORE a new * + * socket is allowed to connect, when the initial socket.io handshake is * + * performed with the server. * + * * + * By default, when a socket tries to connect, Sails allows it, every time. * + * (much in the same way any HTTP request is allowed to reach your routes. * + * If no valid cookie was sent, a temporary session will be created for the * + * connecting socket. * + * * + * If the cookie sent as part of the connection request doesn't match any * + * known user session, a new user session is created for it. * + * * + * In most cases, the user would already have a cookie since they loaded * + * the socket.io client and the initial HTML page you're building. * + * * + * However, in the case of cross-domain requests, it is possible to receive * + * a connection upgrade request WITHOUT A COOKIE (for certain transports) * + * In this case, there is no way to keep track of the requesting user * + * between requests, since there is no identifying information to link * + * him/her with a session. The sails.io.js client solves this by connecting * + * to a CORS/jsonp endpoint first to get a 3rd party cookie(fortunately this* + * works, even in Safari), then opening the connection. * + * * + * You can also pass along a ?cookie query parameter to the upgrade url, * + * which Sails will use in the absence of a proper cookie e.g. (when * + * connecting from the client): * + * io.sails.connect('http://localhost:1337?cookie=smokeybear') * + * * + * Finally note that the user's cookie is NOT (and will never be) accessible* + * from client-side javascript. Using HTTP-only cookies is crucial for your * + * app's security. * + * * + ***************************************************************************/ + // beforeConnect: function(handshake, cb) { + // // `true` allows the connection + // return cb(null, true); + // + // // (`false` would reject the connection) + // }, + + + /*************************************************************************** + * * + * `afterDisconnect` * + * * + * This custom afterDisconnect function will be run each time a socket * + * disconnects * + * * + ***************************************************************************/ + // afterDisconnect: function(session, socket, cb) { + // // By default: do nothing. + // return cb(); + // }, + + /*************************************************************************** + * * + * `transports` * + * * + * A array of allowed transport methods which the clients will try to use. * + * On server environments that don't support sticky sessions, the "polling" * + * transport should be disabled. * + * * + ***************************************************************************/ + // transports: ["polling", "websocket"] + +}; diff --git a/lightcalc/config/views.js b/lightcalc/config/views.js new file mode 100644 index 0000000..5b6496f --- /dev/null +++ b/lightcalc/config/views.js @@ -0,0 +1,95 @@ +/** + * View Engine Configuration + * (sails.config.views) + * + * Server-sent views are a classic and effective way to get your app up + * and running. Views are normally served from controllers. Below, you can + * configure your templating language/framework of choice and configure + * Sails' layout support. + * + * For more information on views and layouts, check out: + * http://sailsjs.org/#!/documentation/concepts/Views + */ + +module.exports.views = { + + /**************************************************************************** + * * + * View engine (aka template language) to use for your app's *server-side* * + * views * + * * + * Sails+Express supports all view engines which implement TJ Holowaychuk's * + * `consolidate.js`, including, but not limited to: * + * * + * ejs, jade, handlebars, mustache underscore, hogan, haml, haml-coffee, * + * dust atpl, eco, ect, jazz, jqtpl, JUST, liquor, QEJS, swig, templayed, * + * toffee, walrus, & whiskers * + * * + * For more options, check out the docs: * + * https://github.com/balderdashy/sails-wiki/blob/0.9/config.views.md#engine * + * * + ****************************************************************************/ + + engine: 'ejs', + + + /**************************************************************************** + * * + * Layouts are simply top-level HTML templates you can use as wrappers for * + * your server-side views. If you're using ejs or jade, you can take * + * advantage of Sails' built-in `layout` support. * + * * + * When using a layout, when one of your views is served, it is injected * + * into the `body` partial defined in the layout. This lets you reuse header * + * and footer logic between views. * + * * + * NOTE: Layout support is only implemented for the `ejs` view engine! * + * For most other engines, it is not necessary, since they implement * + * partials/layouts themselves. In those cases, this config will be * + * silently ignored. * + * * + * The `layout` setting may be set to one of the following: * + * * + * If `false`, layouts will be disabled. Otherwise, if a string is * + * specified, it will be interpreted as the relative path to your layout * + * file from `views/` folder. (the file extension, ".ejs", should be * + * omitted) * + * * + ****************************************************************************/ + + /**************************************************************************** + * * + * Using Multiple Layouts * + * * + * If you're using the default `ejs` or `handlebars` Sails supports the use * + * of multiple `layout` files. To take advantage of this, before rendering a * + * view, override the `layout` local in your controller by setting * + * `res.locals.layout`. (this is handy if you parts of your app's UI look * + * completely different from each other) * + * * + * e.g. your default might be * + * layout: 'layouts/public' * + * * + * But you might override that in some of your controllers with: * + * layout: 'layouts/internal' * + * * + ****************************************************************************/ + + layout: 'layout', + + /**************************************************************************** + * * + * Partials are simply top-level snippets you can leverage to reuse template * + * for your server-side views. If you're using handlebars, you can take * + * advantage of Sails' built-in `partials` support. * + * * + * If `false` or empty partials will be located in the same folder as views. * + * Otherwise, if a string is specified, it will be interpreted as the * + * relative path to your partial files from `views/` folder. * + * * + ****************************************************************************/ + + partials: false + + +}; \ No newline at end of file diff --git a/lightcalc/karma.conf.js b/lightcalc/karma.conf.js new file mode 100644 index 0000000..26440d8 --- /dev/null +++ b/lightcalc/karma.conf.js @@ -0,0 +1,97 @@ +// Karma configuration +// Generated on Wed Jun 17 2015 00:51:08 GMT-0300 (BRT) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'chai-as-promised', 'chai'], + + + // list of files / patterns to load in the browser + files: [ + 'assets/bower_components/jquery/dist/jquery.js', + 'assets/bower_components/angular/angular.js', + 'assets/bower_components/angular-mocks/angular-mocks.js', + 'assets/bower_components/angular-route/angular-route.js', + 'assets/bower_components/angular-resource/angular-resource.js', + 'assets/bower_components/angular-messages/angular-messages.js', + 'assets/bower_components/angular-show-errors/src/showErrors.js', + 'assets/js/app.js', + 'assets/js/routes.js', + 'assets/js/controllers/*.js', + 'assets/js/directives/*.js', + 'assets/js/services/*.js', + 'assets/js/filters/*.js', + 'assets/templates/**/*.html', + 'assets/js/tests/**/*.test.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + plugins : [ + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-mocha', + 'karma-chai', + 'karma-mocha', + 'karma-chai-as-promised', + 'karma-ng-html2js-preprocessor' + ], + + preprocessors: { + "assets/templates/**/*.html": ["ng-html2js"] + }, + + ngHtml2JsPreprocessor: { + // the name of the Angular module to create + moduleName: "MyTemplates" + }, + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/lightcalc/package.json b/lightcalc/package.json new file mode 100644 index 0000000..01902d0 --- /dev/null +++ b/lightcalc/package.json @@ -0,0 +1,55 @@ +{ + "name": "lightcalc", + "private": true, + "version": "0.0.0", + "description": "a Sails application", + "keywords": [], + "engines": { + "node": "0.12.4", + "npm": "2.11.1" + }, + "dependencies": { + "ejs": "~0.8.4", + "forever": "^0.14.1", + "grunt": "0.4.2", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-coffee": "~0.10.1", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-copy": "~0.5.0", + "grunt-contrib-cssmin": "~0.9.0", + "grunt-contrib-jst": "~0.6.0", + "grunt-contrib-less": "0.11.1", + "grunt-contrib-uglify": "~0.4.0", + "grunt-contrib-watch": "~0.5.3", + "grunt-sails-linker": "~0.9.5", + "grunt-sync": "~0.0.4", + "include-all": "~0.1.3", + "rc": "~0.5.0", + "sails": "~0.11.0", + "sails-disk": "~0.10.0", + "sails-mongo": "^0.11.2" + }, + "scripts": { + "debug": "node debug app.js", + "start": "node app.js", + "postinstall": "npm install bower -g && bower install" + }, + "main": "app.js", + "repository": { + "type": "git", + "url": "git://github.com/hugo/lightcalc.git" + }, + "author": "hugo", + "license": "", + "devDependencies": { + "chai-as-promised": "^5.1.0", + "karma": "^0.12.36", + "karma-chai-as-promised": "^0.1.2", + "karma-chrome-launcher": "^0.1.12", + "karma-firefox-launcher": "^0.1.6", + "karma-mocha": "^0.1.10", + "karma-ng-html2js-preprocessor": "^0.1.2", + "mocha": "^2.2.5", + "supertest": "^1.0.1" + } +} diff --git a/lightcalc/tasks/README.md b/lightcalc/tasks/README.md new file mode 100644 index 0000000..78d2f51 --- /dev/null +++ b/lightcalc/tasks/README.md @@ -0,0 +1,54 @@ +# About the `tasks` folder + +The `tasks` directory is a suite of Grunt tasks and their configurations, bundled for your convenience. The Grunt integration is mainly useful for bundling front-end assets, (like stylesheets, scripts, & markup templates) but it can also be used to run all kinds of development tasks, from browserify compilation to database migrations. + +If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, read on! + + +### How does this work? + +The asset pipeline bundled in Sails is a set of Grunt tasks configured with conventional defaults designed to make your project more consistent and productive. + +The entire front-end asset workflow in Sails is completely customizable-- while it provides some suggestions out of the box, Sails makes no pretense that it can anticipate all of the needs you'll encounter building the browser-based/front-end portion of your application. Who's to say you're even building an app for a browser? + + + +### What tasks does Sails run automatically? + +Sails runs some of these tasks (the ones in the `tasks/register` folder) automatically when you run certain commands. + +###### `sails lift` + +Runs the `default` task (`tasks/register/default.js`). + +###### `sails lift --prod` + +Runs the `prod` task (`tasks/register/prod.js`). + +###### `sails www` + +Runs the `build` task (`tasks/register/build.js`). + +###### `sails www --prod` (production) + +Runs the `buildProd` task (`tasks/register/buildProd.js`). + + +### Can I customize this for SASS, Angular, client-side Jade templates, etc? + +You can modify, omit, or replace any of these Grunt tasks to fit your requirements. You can also add your own Grunt tasks- just add a `someTask.js` file in the `grunt/config` directory to configure the new task, then register it with the appropriate parent task(s) (see files in `grunt/register/*.js`). + + +### Do I have to use Grunt? + +Nope! To disable Grunt integration in Sails, just delete your Gruntfile or disable the Grunt hook. + + +### What if I'm not building a web frontend? + +That's ok! A core tenant of Sails is client-agnosticism-- it's especially designed for building APIs used by all sorts of clients; native Android/iOS/Cordova, serverside SDKs, etc. + +You can completely disable Grunt by following the instructions above. + +If you still want to use Grunt for other purposes, but don't want any of the default web front-end stuff, just delete your project's `assets` folder and remove the front-end oriented tasks from the `grunt/register` and `grunt/config` folders. You can also run `sails new myCoolApi --no-frontend` to omit the `assets` folder and front-end-oriented Grunt tasks for future projects. You can also replace your `sails-generate-frontend` module with alternative community generators, or create your own. This allows `sails new` to create the boilerplate for native iOS apps, Android apps, Cordova apps, SteroidsJS apps, etc. + diff --git a/lightcalc/tasks/config/clean.js b/lightcalc/tasks/config/clean.js new file mode 100644 index 0000000..7682afe --- /dev/null +++ b/lightcalc/tasks/config/clean.js @@ -0,0 +1,21 @@ +/** + * Clean files and folders. + * + * --------------------------------------------------------------- + * + * This grunt task is configured to clean out the contents in the .tmp/public of your + * sails project. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-clean + */ +module.exports = function(grunt) { + + grunt.config.set('clean', { + dev: ['.tmp/public/**', '!.tmp/public/bower_components/**'], + build: ['www'], + bower: ['.tmp/public/bower_components/**'] + }); + + grunt.loadNpmTasks('grunt-contrib-clean'); +}; diff --git a/lightcalc/tasks/config/coffee.js b/lightcalc/tasks/config/coffee.js new file mode 100644 index 0000000..7314a3f --- /dev/null +++ b/lightcalc/tasks/config/coffee.js @@ -0,0 +1,38 @@ +/** + * Compile CoffeeScript files to JavaScript. + * + * --------------------------------------------------------------- + * + * Compiles coffeeScript files from `assest/js` into Javascript and places them into + * `.tmp/public/js` directory. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-coffee + */ +module.exports = function(grunt) { + + grunt.config.set('coffee', { + dev: { + options: { + bare: true, + sourceMap: true, + sourceRoot: './' + }, + files: [{ + expand: true, + cwd: 'assets/js/', + src: ['**/*.coffee'], + dest: '.tmp/public/js/', + ext: '.js' + }, { + expand: true, + cwd: 'assets/js/', + src: ['**/*.coffee'], + dest: '.tmp/public/js/', + ext: '.js' + }] + } + }); + + grunt.loadNpmTasks('grunt-contrib-coffee'); +}; diff --git a/lightcalc/tasks/config/concat.js b/lightcalc/tasks/config/concat.js new file mode 100644 index 0000000..4e18379 --- /dev/null +++ b/lightcalc/tasks/config/concat.js @@ -0,0 +1,27 @@ +/** + * Concatenate files. + * + * --------------------------------------------------------------- + * + * Concatenates files javascript and css from a defined array. Creates concatenated files in + * .tmp/public/contact directory + * [concat](https://github.com/gruntjs/grunt-contrib-concat) + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-concat + */ +module.exports = function(grunt) { + + grunt.config.set('concat', { + js: { + src: require('../pipeline').jsFilesToInject, + dest: '.tmp/public/concat/production.js' + }, + css: { + src: require('../pipeline').cssFilesToInject, + dest: '.tmp/public/concat/production.css' + } + }); + + grunt.loadNpmTasks('grunt-contrib-concat'); +}; diff --git a/lightcalc/tasks/config/copy.js b/lightcalc/tasks/config/copy.js new file mode 100644 index 0000000..76a2079 --- /dev/null +++ b/lightcalc/tasks/config/copy.js @@ -0,0 +1,38 @@ +/** + * Copy files and folders. + * + * --------------------------------------------------------------- + * + * # dev task config + * Copies all directories and files, exept coffescript and less fiels, from the sails + * assets folder into the .tmp/public directory. + * + * # build task config + * Copies all directories nd files from the .tmp/public directory into a www directory. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-copy + */ +module.exports = function(grunt) { + + grunt.config.set('copy', { + dev: { + files: [{ + expand: true, + cwd: './assets', + src: ['**/*.!(coffee|less)'], + dest: '.tmp/public' + }] + }, + build: { + files: [{ + expand: true, + cwd: '.tmp/public', + src: ['**/*'], + dest: 'www' + }] + } + }); + + grunt.loadNpmTasks('grunt-contrib-copy'); +}; diff --git a/lightcalc/tasks/config/cssmin.js b/lightcalc/tasks/config/cssmin.js new file mode 100644 index 0000000..24bedd6 --- /dev/null +++ b/lightcalc/tasks/config/cssmin.js @@ -0,0 +1,21 @@ +/** + * Compress CSS files. + * + * --------------------------------------------------------------- + * + * Minifies css files and places them into .tmp/public/min directory. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-cssmin + */ +module.exports = function(grunt) { + + grunt.config.set('cssmin', { + dist: { + src: ['.tmp/public/concat/production.css'], + dest: '.tmp/public/min/production.min.css' + } + }); + + grunt.loadNpmTasks('grunt-contrib-cssmin'); +}; diff --git a/lightcalc/tasks/config/jst.js b/lightcalc/tasks/config/jst.js new file mode 100644 index 0000000..0740b79 --- /dev/null +++ b/lightcalc/tasks/config/jst.js @@ -0,0 +1,41 @@ +/** + * Precompiles Underscore templates to a `.jst` file. + * + * --------------------------------------------------------------- + * + * (i.e. basically it takes HTML files and turns them into tiny little + * javascript functions that you pass data to and return HTML. This can + * speed up template rendering on the client, and reduce bandwidth usage.) + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-jst + * + */ + +module.exports = function(grunt) { + + grunt.config.set('jst', { + dev: { + + // To use other sorts of templates, specify a regexp like the example below: + // options: { + // templateSettings: { + // interpolate: /\{\{(.+?)\}\}/g + // } + // }, + + // Note that the interpolate setting above is simply an example of overwriting lodash's + // default interpolation. If you want to parse templates with the default _.template behavior + // (i.e. using
), there's no need to overwrite `templateSettings.interpolate`. + + + files: { + // e.g. + // 'relative/path/from/gruntfile/to/compiled/template/destination' : ['relative/path/to/sourcefiles/**/*.html'] + '.tmp/public/jst.js': require('../pipeline').templateFilesToInject + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-jst'); +}; diff --git a/lightcalc/tasks/config/less.js b/lightcalc/tasks/config/less.js new file mode 100644 index 0000000..476326e --- /dev/null +++ b/lightcalc/tasks/config/less.js @@ -0,0 +1,28 @@ +/** + * Compiles LESS files into CSS. + * + * --------------------------------------------------------------- + * + * Only the `assets/styles/importer.less` is compiled. + * This allows you to control the ordering yourself, i.e. import your + * dependencies, mixins, variables, resets, etc. before other stylesheets) + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-less + */ +module.exports = function(grunt) { + + grunt.config.set('less', { + dev: { + files: [{ + expand: true, + cwd: 'assets/styles/', + src: ['*.less'], + dest: '.tmp/public/styles/', + ext: '.css' + }] + } + }); + + grunt.loadNpmTasks('grunt-contrib-less'); +}; diff --git a/lightcalc/tasks/config/sails-linker.js b/lightcalc/tasks/config/sails-linker.js new file mode 100644 index 0000000..a1a65d6 --- /dev/null +++ b/lightcalc/tasks/config/sails-linker.js @@ -0,0 +1,267 @@ +/** + * Autoinsert script tags (or other filebased tags) in an html file. + * + * --------------------------------------------------------------- + * + * Automatically inject ', + appRoot: '.tmp/public' + }, + files: { + '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, + 'views/**/*.html': require('../pipeline').jsFilesToInject, + 'views/**/*.ejs': require('../pipeline').jsFilesToInject + } + }, + + devJsRelative: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public', + relative: true + }, + files: { + '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, + 'views/**/*.html': require('../pipeline').jsFilesToInject, + 'views/**/*.ejs': require('../pipeline').jsFilesToInject + } + }, + + prodJs: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public' + }, + files: { + '.tmp/public/**/*.html': ['.tmp/public/min/production.min.js'], + 'views/**/*.html': ['.tmp/public/min/production.min.js'], + 'views/**/*.ejs': ['.tmp/public/min/production.min.js'] + } + }, + + prodJsRelative: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public', + relative: true + }, + files: { + '.tmp/public/**/*.html': ['.tmp/public/min/production.min.js'], + 'views/**/*.html': ['.tmp/public/min/production.min.js'], + 'views/**/*.ejs': ['.tmp/public/min/production.min.js'] + } + }, + + devStyles: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public' + }, + + files: { + '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, + 'views/**/*.html': require('../pipeline').cssFilesToInject, + 'views/**/*.ejs': require('../pipeline').cssFilesToInject + } + }, + + devStylesRelative: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public', + relative: true + }, + + files: { + '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, + 'views/**/*.html': require('../pipeline').cssFilesToInject, + 'views/**/*.ejs': require('../pipeline').cssFilesToInject + } + }, + + prodStyles: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public' + }, + files: { + '.tmp/public/index.html': ['.tmp/public/min/production.min.css'], + 'views/**/*.html': ['.tmp/public/min/production.min.css'], + 'views/**/*.ejs': ['.tmp/public/min/production.min.css'] + } + }, + + prodStylesRelative: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public', + relative: true + }, + files: { + '.tmp/public/index.html': ['.tmp/public/min/production.min.css'], + 'views/**/*.html': ['.tmp/public/min/production.min.css'], + 'views/**/*.ejs': ['.tmp/public/min/production.min.css'] + } + }, + + // Bring in JST template object + devTpl: { + options: { + startTag: '', + endTag: '', + fileTmpl: '', + appRoot: '.tmp/public' + }, + files: { + '.tmp/public/index.html': ['.tmp/public/jst.js'], + 'views/**/*.html': ['.tmp/public/jst.js'], + 'views/**/*.ejs': ['.tmp/public/jst.js'] + } + }, + + devJsJade: { + options: { + startTag: '// SCRIPTS', + endTag: '// SCRIPTS END', + fileTmpl: 'script(src="%s")', + appRoot: '.tmp/public' + }, + files: { + 'views/**/*.jade': require('../pipeline').jsFilesToInject + } + }, + + devJsRelativeJade: { + options: { + startTag: '// SCRIPTS', + endTag: '// SCRIPTS END', + fileTmpl: 'script(src="%s")', + appRoot: '.tmp/public', + relative: true + }, + files: { + 'views/**/*.jade': require('../pipeline').jsFilesToInject + } + }, + + prodJsJade: { + options: { + startTag: '// SCRIPTS', + endTag: '// SCRIPTS END', + fileTmpl: 'script(src="%s")', + appRoot: '.tmp/public' + }, + files: { + 'views/**/*.jade': ['.tmp/public/min/production.min.js'] + } + }, + + prodJsRelativeJade: { + options: { + startTag: '// SCRIPTS', + endTag: '// SCRIPTS END', + fileTmpl: 'script(src="%s")', + appRoot: '.tmp/public', + relative: true + }, + files: { + 'views/**/*.jade': ['.tmp/public/min/production.min.js'] + } + }, + + devStylesJade: { + options: { + startTag: '// STYLES', + endTag: '// STYLES END', + fileTmpl: 'link(rel="stylesheet", href="%s")', + appRoot: '.tmp/public' + }, + + files: { + 'views/**/*.jade': require('../pipeline').cssFilesToInject + } + }, + + devStylesRelativeJade: { + options: { + startTag: '// STYLES', + endTag: '// STYLES END', + fileTmpl: 'link(rel="stylesheet", href="%s")', + appRoot: '.tmp/public', + relative: true + }, + + files: { + 'views/**/*.jade': require('../pipeline').cssFilesToInject + } + }, + + prodStylesJade: { + options: { + startTag: '// STYLES', + endTag: '// STYLES END', + fileTmpl: 'link(rel="stylesheet", href="%s")', + appRoot: '.tmp/public' + }, + files: { + 'views/**/*.jade': ['.tmp/public/min/production.min.css'] + } + }, + + prodStylesRelativeJade: { + options: { + startTag: '// STYLES', + endTag: '// STYLES END', + fileTmpl: 'link(rel="stylesheet", href="%s")', + appRoot: '.tmp/public', + relative: true + }, + files: { + 'views/**/*.jade': ['.tmp/public/min/production.min.css'] + } + }, + + // Bring in JST template object + devTplJade: { + options: { + startTag: '// TEMPLATES', + endTag: '// TEMPLATES END', + fileTmpl: 'script(type="text/javascript", src="%s")', + appRoot: '.tmp/public' + }, + files: { + 'views/**/*.jade': ['.tmp/public/jst.js'] + } + } + }); + + grunt.loadNpmTasks('grunt-sails-linker'); +}; diff --git a/lightcalc/tasks/config/sync.js b/lightcalc/tasks/config/sync.js new file mode 100644 index 0000000..8acb31f --- /dev/null +++ b/lightcalc/tasks/config/sync.js @@ -0,0 +1,27 @@ +/** + * A grunt task to keep directories in sync. It is very similar to grunt-contrib-copy + * but tries to copy only those files that has actually changed. + * + * --------------------------------------------------------------- + * + * Synchronize files from the `assets` folder to `.tmp/public`, + * smashing anything that's already there. + * + * For usage docs see: + * https://github.com/tomusdrw/grunt-sync + * + */ +module.exports = function(grunt) { + + grunt.config.set('sync', { + dev: { + files: [{ + cwd: './assets', + src: ['**/*.!(coffee)'], + dest: '.tmp/public' + }] + } + }); + + grunt.loadNpmTasks('grunt-sync'); +}; diff --git a/lightcalc/tasks/config/uglify.js b/lightcalc/tasks/config/uglify.js new file mode 100644 index 0000000..d06bd9a --- /dev/null +++ b/lightcalc/tasks/config/uglify.js @@ -0,0 +1,22 @@ +/** + * Minify files with UglifyJS. + * + * --------------------------------------------------------------- + * + * Minifies client-side javascript `assets`. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-uglify + * + */ +module.exports = function(grunt) { + + grunt.config.set('uglify', { + dist: { + src: ['.tmp/public/concat/production.js'], + dest: '.tmp/public/min/production.min.js' + } + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); +}; diff --git a/lightcalc/tasks/config/watch.js b/lightcalc/tasks/config/watch.js new file mode 100644 index 0000000..2effc2c --- /dev/null +++ b/lightcalc/tasks/config/watch.js @@ -0,0 +1,34 @@ +/** + * Run predefined tasks whenever watched file patterns are added, changed or deleted. + * + * --------------------------------------------------------------- + * + * Watch for changes on + * - files in the `assets` folder + * - the `tasks/pipeline.js` file + * and re-run the appropriate tasks. + * + * For usage docs see: + * https://github.com/gruntjs/grunt-contrib-watch + * + */ +module.exports = function(grunt) { + + grunt.config.set('watch', { + api: { + + // API files to watch: + files: ['api/**/*', '!**/node_modules/**'] + }, + assets: { + + // Assets to watch: + files: ['assets/**/*', '!assets/bower_components/**', 'tasks/pipeline.js', '!**/node_modules/**'], + + // When assets are changed: + tasks: ['syncAssets' , 'linkAssets'] + } + }); + + grunt.loadNpmTasks('grunt-contrib-watch'); +}; diff --git a/lightcalc/tasks/pipeline.js b/lightcalc/tasks/pipeline.js new file mode 100644 index 0000000..5dd5045 --- /dev/null +++ b/lightcalc/tasks/pipeline.js @@ -0,0 +1,77 @@ +/** + * grunt/pipeline.js + * + * The order in which your css, javascript, and template files should be + * compiled and linked from your views and static HTML files. + * + * (Note that you can take advantage of Grunt-style wildcard/glob/splat expressions + * for matching multiple files.) + */ + + + +// CSS files to inject in order +// +// (if you're using LESS with the built-in default config, you'll want +// to change `assets/styles/importer.less` instead.) +var cssFilesToInject = [ + 'bower_components/bootstrap/dist/css/bootstrap.css', + 'styles/**/*.css' +]; + + +// Client-side javascript files to inject in order +// (uses Grunt-style wildcard/glob/splat expressions) +var jsFilesToInject = [ + + // Load sails.io before everything else + 'js/dependencies/sails.io.js', + + // Dependencies like jQuery, or Angular are brought in here + 'js/dependencies/**/*.js', + + 'bower_components/jquery/dist/jquery.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-route/angular-route.js', + 'bower_components/angular-resource/angular-resource.js', + 'bower_components/angular-messages/angular-messages.js', + 'bower_components/angular-show-errors/src/showErrors.js', + + // All of the rest of your client-side js files + // will be injected here in no particular order. + 'js/app.js', + 'js/routes.js', + 'js/controllers/*.js', + 'js/directives/*.js', + 'js/services/*.js', + 'js/filters/*.js' +]; + + +// Client-side HTML templates are injected using the sources below +// The ordering of these templates shouldn't matter. +// (uses Grunt-style wildcard/glob/splat expressions) +// +// By default, Sails uses JST templates and precompiles them into +// functions for you. If you want to use jade, handlebars, dust, etc., +// with the linker, no problem-- you'll just want to make sure the precompiled +// templates get spit out to the same file. Be sure and check out `tasks/README.md` +// for information on customizing and installing new tasks. +var templateFilesToInject = [ + 'templates/**/*.html' +]; + + + +// Prefix relative paths to source files so they point to the proper locations +// (i.e. where the other Grunt tasks spit them out, or in some cases, where +// they reside in the first place) +module.exports.cssFilesToInject = cssFilesToInject.map(function(path) { + return '.tmp/public/' + path; +}); +module.exports.jsFilesToInject = jsFilesToInject.map(function(path) { + return '.tmp/public/' + path; +}); +module.exports.templateFilesToInject = templateFilesToInject.map(function(path) { + return 'assets/' + path; +}); diff --git a/lightcalc/tasks/register/build.js b/lightcalc/tasks/register/build.js new file mode 100644 index 0000000..94a0e6d --- /dev/null +++ b/lightcalc/tasks/register/build.js @@ -0,0 +1,8 @@ +module.exports = function (grunt) { + grunt.registerTask('build', [ + 'compileAssets', + 'linkAssetsBuild', + 'clean:build', + 'copy:build' + ]); +}; diff --git a/lightcalc/tasks/register/buildProd.js b/lightcalc/tasks/register/buildProd.js new file mode 100644 index 0000000..eb2171f --- /dev/null +++ b/lightcalc/tasks/register/buildProd.js @@ -0,0 +1,12 @@ +module.exports = function (grunt) { + grunt.registerTask('buildProd', [ + 'compileAssets', + 'concat', + 'uglify', + 'cssmin', + 'linkAssetsBuildProd', + 'clean:bower', + 'clean:build', + 'copy:build' + ]); +}; diff --git a/lightcalc/tasks/register/compileAssets.js b/lightcalc/tasks/register/compileAssets.js new file mode 100644 index 0000000..b63e2b8 --- /dev/null +++ b/lightcalc/tasks/register/compileAssets.js @@ -0,0 +1,9 @@ +module.exports = function (grunt) { + grunt.registerTask('compileAssets', [ + 'clean:dev', + 'jst:dev', + 'less:dev', + 'copy:dev', + 'coffee:dev' + ]); +}; diff --git a/lightcalc/tasks/register/default.js b/lightcalc/tasks/register/default.js new file mode 100644 index 0000000..c7de81e --- /dev/null +++ b/lightcalc/tasks/register/default.js @@ -0,0 +1,3 @@ +module.exports = function (grunt) { + grunt.registerTask('default', ['compileAssets', 'linkAssets', 'watch']); +}; diff --git a/lightcalc/tasks/register/linkAssets.js b/lightcalc/tasks/register/linkAssets.js new file mode 100644 index 0000000..17225cf --- /dev/null +++ b/lightcalc/tasks/register/linkAssets.js @@ -0,0 +1,10 @@ +module.exports = function (grunt) { + grunt.registerTask('linkAssets', [ + 'sails-linker:devJs', + 'sails-linker:devStyles', + 'sails-linker:devTpl', + 'sails-linker:devJsJade', + 'sails-linker:devStylesJade', + 'sails-linker:devTplJade' + ]); +}; diff --git a/lightcalc/tasks/register/linkAssetsBuild.js b/lightcalc/tasks/register/linkAssetsBuild.js new file mode 100644 index 0000000..ad68e5d --- /dev/null +++ b/lightcalc/tasks/register/linkAssetsBuild.js @@ -0,0 +1,10 @@ +module.exports = function (grunt) { + grunt.registerTask('linkAssetsBuild', [ + 'sails-linker:devJsRelative', + 'sails-linker:devStylesRelative', + 'sails-linker:devTpl', + 'sails-linker:devJsRelativeJade', + 'sails-linker:devStylesRelativeJade', + 'sails-linker:devTplJade' + ]); +}; diff --git a/lightcalc/tasks/register/linkAssetsBuildProd.js b/lightcalc/tasks/register/linkAssetsBuildProd.js new file mode 100644 index 0000000..9682ac6 --- /dev/null +++ b/lightcalc/tasks/register/linkAssetsBuildProd.js @@ -0,0 +1,10 @@ +module.exports = function (grunt) { + grunt.registerTask('linkAssetsBuildProd', [ + 'sails-linker:prodJsRelative', + 'sails-linker:prodStylesRelative', + 'sails-linker:devTpl', + 'sails-linker:prodJsRelativeJade', + 'sails-linker:prodStylesRelativeJade', + 'sails-linker:devTplJade' + ]); +}; diff --git a/lightcalc/tasks/register/prod.js b/lightcalc/tasks/register/prod.js new file mode 100644 index 0000000..db9daed --- /dev/null +++ b/lightcalc/tasks/register/prod.js @@ -0,0 +1,14 @@ +module.exports = function (grunt) { + grunt.registerTask('prod', [ + 'compileAssets', + 'concat', + 'uglify', + 'cssmin', + 'sails-linker:prodJs', + 'sails-linker:prodStyles', + 'sails-linker:devTpl', + 'sails-linker:prodJsJade', + 'sails-linker:prodStylesJade', + 'sails-linker:devTplJade' + ]); +}; diff --git a/lightcalc/tasks/register/syncAssets.js b/lightcalc/tasks/register/syncAssets.js new file mode 100644 index 0000000..99bb12f --- /dev/null +++ b/lightcalc/tasks/register/syncAssets.js @@ -0,0 +1,8 @@ +module.exports = function (grunt) { + grunt.registerTask('syncAssets', [ + 'jst:dev', + 'less:dev', + 'sync:dev', + 'coffee:dev' + ]); +}; diff --git a/lightcalc/test/bootstrap.test.js b/lightcalc/test/bootstrap.test.js new file mode 100644 index 0000000..0ff496a --- /dev/null +++ b/lightcalc/test/bootstrap.test.js @@ -0,0 +1,46 @@ +var Sails = require('sails'), + sails; + +global.DOMAIN = 'http://localhost'; +global.PORT = 1420; +global.HOST = DOMAIN + ':' + PORT; + +before(function(done) { + Sails.lift({ + log: { + level: 'warn' + }, + connections: { + testDiskDb: { + adapter: 'sails-disk', + filePath : '.tmp/sails.test.dat' + } + }, + models: { + migrate: 'safe', + connection: 'testDiskDb' + }, + port: process.env.PORT || 1338, + environment: 'test', + csrf: false, + hooks: { + grunt: false, + socket: false, + pubsub: false + }, + session: { + adapter: 'memory' + }, + + }, function(err, server) { + sails = server; + if (err) return done(err); + // here you can load fixtures, etc. + done(err, sails); + }); +}); + +after(function(done) { + // here you can clear fixtures, etc. + sails.lower(done); +}); \ No newline at end of file diff --git a/lightcalc/test/readme b/lightcalc/test/readme new file mode 100644 index 0000000..a6db02c --- /dev/null +++ b/lightcalc/test/readme @@ -0,0 +1,48 @@ +Project of software test + + + +By Hugo Bessa -- hugo@bessa.me +By jef -- jfnf@cin.ufpe.br + + + + +- * - * - * - * - * - * - * - * - +Requeriments for run those tests of backend: + +#1 - The Node.js - comes with npm. It's an online repository to projetcs publication of open-sources codes to Node.js. + +Install: http://nodejs.org/dist/v0.12.4/node-v0.12.4.pkg + + +#2 - Mocha - is a JavaScript test framework for node.js and the browser. It handles test suites and test cases. + +Install: + +$ npm install -g mocha + +$ npm install supertest + +$ npm install assert + + $ npm install sails + + +- * - * - * - * - * - * - * - * - +Github's project: + +clonar esse respositório git clone https://github.com/hugobessa/lightcalc.git +- * - * - * - * - * - * - * - * - +Fire on the code + +go to the local folder of lightCalc and run the mocha framework, like - + +$ mocha test/bootstrap.test.js test/unit/**/*.test.js + + + + + + + diff --git a/lightcalc/test/unit/controllers/CalculatorsController.test.js b/lightcalc/test/unit/controllers/CalculatorsController.test.js new file mode 100644 index 0000000..377a802 --- /dev/null +++ b/lightcalc/test/unit/controllers/CalculatorsController.test.js @@ -0,0 +1,93 @@ +var request = require('supertest'); +var assert = require('assert'); + +describe('CalculatorsController', function() { + + describe('#list()', function() { + beforeEach(function(done){ + Calculator.destroy({}).exec(function(){ + done(); + }); + }); + + it('should return an empty array', function (done) { + request(sails.hooks.http.app) + .get('/calculator') + .end(function(err, res){ + assert.equal(res.status, 200); + assert.equal(res.body.length, 0); + done() + }); + }); + }); + + describe('#create()', function() { + beforeEach(function(done){ + Calculator.create({name: 'FantasmaOpera'}).exec(function(){ + done(); + }); + }); + + it('should create a calculator', function (done) { + var calculator = {name: 'hugobessa'}; + + request(sails.hooks.http.app) + .post('/calculator') + .send(calculator) + .end(function(err, res){ + assert(res.status, 201); + Calculator.find({name: 'hugobessa'}).exec(function(err, found){ + assert.equal(found[0].name, 'hugobessa', done); + done(); + }) + }); + }); + + it('should return error trying to save a calculator with a short name', function (done) { + var calculator = {name: 'hugobes'}; + + request(sails.hooks.http.app) + .post('/calculator') + .send(calculator) + .end(function(err, res){ + assert.equal(res.status, 400); + done(); + }); + }); + + it('should return error trying to save a calculator with a long name', function (done) { + var calculator = {name: 'hugobessahugobessahugobessahugobessahugobessahugobessa'}; + + request(sails.hooks.http.app) + .post('/calculator') + .send(calculator) + .end(function(err, res){ + assert.equal(res.status, 400); + done(); + }); + }); + + it('should return error trying to save a calculator with a not alphanumeric name', function (done) { + var calculator = {name: 'hugãoteste'}; + + request(sails.hooks.http.app) + .post('/calculator') + .send(calculator) + .end(function(err, res){ + assert.equal(res.status, 400); + done(); + }); + }); + + it('should return error trying to save duplicate calculator name', function (done) { + var calculator = {name: 'FantasmaOpera'}; + + request(sails.hooks.http.app) + .post('/calculator') + .send(calculator) + .expect(400, done); + }); + }); +}); + + diff --git a/lightcalc/test/unit/controllers/OperationsController.test.js b/lightcalc/test/unit/controllers/OperationsController.test.js new file mode 100644 index 0000000..064b4db --- /dev/null +++ b/lightcalc/test/unit/controllers/OperationsController.test.js @@ -0,0 +1,92 @@ +var request = require('supertest'); +var assert = require('assert'); + +describe('OperationsController', function() { + + describe('#list()', function() { + + describe('there\'s no operation registered', function(){ + beforeEach(function(done){ + Operation.destroy({}).exec(function(){ + done(); + }); + }) + it('should return an empty array', function (done) { + request(sails.hooks.http.app) + .get('/operation') + .send() + .end(function(err, res){ + assert.equal(res.status, 200); + assert.equal(res.body.length, 0); + done() + }); + + }); + }); + + describe('there\'s one operation registered', function(){ + var calculator, operation; + beforeEach(function(done){ + Calculator.destroy({}).exec(function(err){ + Calculator.create({name: 'testecalc'}).exec(function(err, created){ + calculator = created; + Operation.create({text: '2 + 2', calculator: calculator}).exec(function(err, created){ + operation = created + done(); + }) + }); + }); + }); + + it('should return one operation', function (done) { + request(sails.hooks.http.app) + .get('/operation') + .send() + .end(function(err, res){ + assert.equal(res.status, 200); + assert.equal(res.body[0].id, operation.id); + done() + }); + }); + }) + }); + + + describe('#create()', function() { + var calculator; + beforeEach(function(done){ + Calculator.destroy({}).exec(function(err){ + Calculator.create({name: 'testecalc'}).exec(function(err, created){ + calculator = created; + done(); + }); + }); + }); + it('should create an operation in testecalc calculator', function (done) { + request(sails.hooks.http.app) + .post('/operation/') + .send({text:'2 + 3', calculator: calculator.id}) + .end(function(err, res){ + assert.equal(res.status, 201); + + Operation.find({calculator: calculator.id}).exec(function(err, found){ + assert.equal(found[0].text, '2 + 3'); + done(); + }) + + }); + }); + + it('should fail creating an operation in an inexistent calculator', function (done) { + request(sails.hooks.http.app) + .post('/operation/') + .send({text:'2 + 3', calculator: 'asdasdsadas'}) + .end(function(err, res){ + assert.equal(res.status, 400); + done() + }); + }); + + }); + +}); \ No newline at end of file diff --git a/lightcalc/views/403.ejs b/lightcalc/views/403.ejs new file mode 100644 index 0000000..c32ba9e --- /dev/null +++ b/lightcalc/views/403.ejs @@ -0,0 +1,76 @@ + + + + + Forbidden + + + + + +
+
+ +
+ +
+

+ Forbidden +

+

+ <% if (typeof error !== 'undefined') { %> + <%= error %> + <% } else { %> + You don't have permission to see the page you're trying to reach. + <% } %> +

+

+ Why might this be happening? +

+
+ + +
+ + + diff --git a/lightcalc/views/404.ejs b/lightcalc/views/404.ejs new file mode 100644 index 0000000..1329819 --- /dev/null +++ b/lightcalc/views/404.ejs @@ -0,0 +1,76 @@ + + + + + Page Not Found + + + + + +
+
+ +
+ +
+

+ Something's fishy here. +

+

+ <% if (typeof error!== 'undefined') { %> + <%= error %> + <% } else { %> + The page you were trying to reach doesn't exist. + <% } %> +

+

+ Why might this be happening? +

+
+ + +
+ + + diff --git a/lightcalc/views/500.ejs b/lightcalc/views/500.ejs new file mode 100644 index 0000000..34e8af9 --- /dev/null +++ b/lightcalc/views/500.ejs @@ -0,0 +1,81 @@ + + + + + Server Error + + + + +
+
+
+ + +
+
+
+

+ Internal Server Error +

+

+ Something isn't right here. +

+ <% if (typeof error !== 'undefined') { %> +

+        	<%- error %>
+        
+ <% } else { %> +

+ A team of highly trained sea bass is working on this as we speak.
+ If the problem persists, please contact the system administrator and inform them of the time that the error occured, and anything you might have done that may have caused the error. +

+ <% } %> + +
+ + +
+ + diff --git a/lightcalc/views/home/index.ejs b/lightcalc/views/home/index.ejs new file mode 100644 index 0000000..c785652 --- /dev/null +++ b/lightcalc/views/home/index.ejs @@ -0,0 +1,3 @@ +
+ +
diff --git a/lightcalc/views/layout.ejs b/lightcalc/views/layout.ejs new file mode 100644 index 0000000..71277e7 --- /dev/null +++ b/lightcalc/views/layout.ejs @@ -0,0 +1,109 @@ + + + + Light Calc + + + + + + + + + + + + + + + <%- body %> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 33bb61e0314232fe8459d8c4ce53b8c251af3bdb Mon Sep 17 00:00:00 2001 From: Hugo Bessa Date: Wed, 17 Jun 2015 20:35:55 -0300 Subject: [PATCH 4/4] =?UTF-8?q?Atualizando=20depend=C3=AAncias=20do=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lightcalc/karma.conf.js | 4 +--- lightcalc/package.json | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lightcalc/karma.conf.js b/lightcalc/karma.conf.js index 26440d8..caa76fb 100644 --- a/lightcalc/karma.conf.js +++ b/lightcalc/karma.conf.js @@ -10,7 +10,7 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'chai-as-promised', 'chai'], + frameworks: ['mocha', 'chai'], // list of files / patterns to load in the browser @@ -72,8 +72,6 @@ module.exports = function(config) { 'karma-firefox-launcher', 'karma-mocha', 'karma-chai', - 'karma-mocha', - 'karma-chai-as-promised', 'karma-ng-html2js-preprocessor' ], diff --git a/lightcalc/package.json b/lightcalc/package.json index 01902d0..3a92f6e 100644 --- a/lightcalc/package.json +++ b/lightcalc/package.json @@ -42,11 +42,10 @@ "author": "hugo", "license": "", "devDependencies": { - "chai-as-promised": "^5.1.0", + "chai": "^3.0.0", "karma": "^0.12.36", - "karma-chai-as-promised": "^0.1.2", + "karma-chai": "^0.1.0", "karma-chrome-launcher": "^0.1.12", - "karma-firefox-launcher": "^0.1.6", "karma-mocha": "^0.1.10", "karma-ng-html2js-preprocessor": "^0.1.2", "mocha": "^2.2.5",