diff --git a/.dockerignore b/.dockerignore index 0bbf9fa8..f56202ae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,20 @@ .git +.github .idea -.vagrant -coverage -design -e2e_coverage -Vagrantfile +data +docker +node_modules +selenium +.all-contributorsrc +.csslintrc +.editorconfig +.env.example +.gitignore +.jshintrc +.slugignore +*.md +conf.json +docker-compose.yml +Dockerfile Procfile +protractor.conf.js diff --git a/.editorconfig b/.editorconfig index ed90a068..ad92ba39 100755 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf -indent_style = tab +indent_style = space insert_final_newline = true [{Dockerfile,Procfile}] diff --git a/.gitignore b/.gitignore index c1152952..76008bce 100755 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ Oscar_Credentials.* npm-debug.log node_modules/ public/lib/ -public/dist +public/dist/ app/tests/coverage/ .bower-*/ .idea/ diff --git a/.jshintrc b/.jshintrc index 94d52d77..73f3a588 100755 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,7 @@ "globals": { // Globals variables. "jasmine": true, "angular": true, + "devel": false, "_": true, "saveAs": true, "ApplicationConfiguration": true, diff --git a/Dockerfile b/Dockerfile index eb92b05c..0dce360e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,76 +2,51 @@ FROM node:10-alpine MAINTAINER OhMyForm # Install some needed packages -RUN apk add --no-cache \ - git \ - && rm -rf /tmp/* +RUN apk add --no-cache git \ + && rm -rf /tmp/* \ + && npm install --quiet -g grunt bower pm2 \ + && npm cache clean --force \ + && mkdir -p /opt/app/public/lib -## TODO: Crush these consecutive RUN's into a single run if possible. -# Install NPM Global Libraries -RUN npm install --quiet -g grunt bower pm2 && npm cache clean --force +# to expose the public folder to other containers +# VOLUME /opt/app WORKDIR /opt/app -RUN mkdir -p /opt/app/public/lib - -## TODO: Optimize layers here as copy layers can be easily reduced if saner COPY usage is achieved. -# Add bower.json -COPY bower.json /opt/app/bower.json -COPY .bowerrc /opt/app/.bowerrc - -COPY ./process.yml /opt/app/process.yml -COPY ./app /opt/app/app -COPY ./public /opt/app/public -COPY ./config /opt/app/config -COPY ./gruntfile.js /opt/app/gruntfile.js -COPY ./server.js /opt/app/server.js -COPY ./scripts/create_admin.js /opt/app/scripts/create_admin.js ## TODO: Find a method that's better than this for passing ENV's if possible. # Set default ENV -ENV NODE_ENV=development -ENV SECRET_KEY=ChangeMeChangeMe -#ENV MONGODB_URI=mongodb://mongo/ohmyform -#ENV REDIS_URL=redis://redis:6379 -ENV PORT=5000 -ENV BASE_URL=localhost -ENV SOCKET_PORT=20523 -ENV SIGNUP_DISABLED=FALSE -ENV SUBDOMAINS_DISABLED=FALSE -ENV ENABLE_CLUSTER_MODE=FALSE -ENV MAILER_EMAIL_ID=ohmyform@localhost -ENV MAILER_PASSWORD= -ENV MAILER_FROM=ohmyform@localhost -ENV MAILER_SERVICE_PROVIDER= -ENV MAILER_SMTP_HOST= -ENV MAILER_SMTP_PORT= -ENV MAILER_SMTP_SECURE= +ENV NODE_ENV=development \ + SECRET_KEY=ChangeMeChangeMe \ + PORT=5000 \ + BASE_URL=localhost \ + SOCKET_PORT=20523 \ + SIGNUP_DISABLED=FALSE \ + SUBDOMAINS_DISABLED=FALSE \ + ENABLE_CLUSTER_MODE=FALSE \ + MAILER_EMAIL_ID=ohmyform@localhost \ + MAILER_PASSWORD="" \ + MAILER_FROM=ohmyform@localhost \ + MAILER_SERVICE_PROVIDER="" \ + MAILER_SMTP_HOST="" \ + MAILER_SMTP_PORT="" \ + MAILER_SMTP_SECURE="" \ + CREATE_ADMIN=FALSE \ + ADMIN_EMAIL=admin@ohmyform.com \ + ADMIN_USERNAME=root \ + ADMIN_PASSWORD=root \ + APP_NAME=OhMyForm \ + APP_KEYWORDS="" \ + APP_DESC="" \ + COVERALLS_REPO_TOKEN="" \ + GOOGLE_ANALYTICS_ID="" \ + RAVEN_DSN="" -ENV CREATE_ADMIN=FALSE -ENV ADMIN_EMAIL=admin@ohmyform.com -ENV ADMIN_USERNAME=root -ENV ADMIN_PASSWORD=root +# keep .dockerignore up to date +COPY . . -ENV APP_NAME=OhMyForm -ENV APP_KEYWORDS= -ENV APP_DESC= +RUN npm install --only=production \ + && bower install --allow-root -f \ + && grunt build -# optional ENV settings -ENV COVERALLS_REPO_TOKEN= -ENV GOOGLE_ANALYTICS_ID= -ENV RAVEN_DSN= - -## TODO: Determine if it's necessary to have this COPY be it's own separate operation. -# Copies the local package.json file to the container -# and utilities docker container cache to not needing to rebuild -# and install node_modules/ everytime we build the docker, but only -# when the local package.json file changes. -# Add npm package.json -COPY ./package.json /opt/app/package.json -RUN npm install --only=production --quiet -RUN bower install --allow-root -RUN grunt build -## TODO: Determine if it would be possible to do a multi stage container where the prebuilt app is copied with nothing else from the build step. - -## TODO: Make this configure things on startup in a sane way or don't if the operator passes any configuration files perhaps via a start.sh. # Run OhMyForm server CMD ["node", "server.js"] diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index b1806199..00000000 --- a/Vagrantfile +++ /dev/null @@ -1,25 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -## TODO: Reconsider this as we don't have this configuration anymore. - -# All Vagrant configuration is done below. The "2" in Vagrant.configure -# configures the configuration version (we support older styles for -# backwards compatibility). Please don't change it unless you know what -# you're doing. -Vagrant.configure("2") do |config| - - # Every Vagrant development environment requires a box. You can search for - # boxes at https://atlas.hashicorp.com/search. - config.vm.box = "ubuntu/trusty64" - config.vm.network :forwarded_port, guest: 3000, host: 4567 - - config.vm.provision "docker" do |d| - d.run "mongo", - args: "-p 27017:27017 -d --name some-mongo" - d.run "redis", - args: "-p 6379:6379 -d --name some-redis" - d.run "tellform/development", - args: "-p 3000:3000 --link some-redis:redis-db --link some-mongo:db" - end -end diff --git a/app.json b/app.json deleted file mode 100644 index 36ae03d7..00000000 --- a/app.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "TellForm", - "description": "Beautiful, opensource web forms", - "repository": "https://github.com/tellform/tellform", - "logo": "https://tellform.com/public/img/tellform_logo_black.png", - "keywords": ["node", "express", "static", "mean"], - "addons": ["mongolab", "sendgrid", "heroku-redis"], - "env": { - "SUBDOMAINS_DISABLED": { - "description": "Disable support for running subdomains. (This should be true if you are not using your own custom domain.", - "value": "TRUE" - }, - "ENABLE_CLUSTER_MODE": { - "description": "ENABLE support for running in cluster mode on pm2", - "value": "FALSE" - }, - "NODE_ENV": { - "description": "Choose whether to run app in development or production mode", - "value": "production" - }, - "BASE_URL": { - "description": "The url of your heroku app." - }, - "SOCKET_URL": { - "description": "Where you websockets will connect to (i.e. your heroku app url)" - }, - "MAILER_SERVICE_PROVIDER": { - "description": "Which mail service/API you will be using (i.e. SparkPost, Mandrill, etc)", - "value": "SendGrid" - } - }, - "buildpacks": [ - { - "url": "https://github.com/heroku/heroku-buildpack-nodejs#v111" - } - ] -} diff --git a/app/controllers/forms.server.controller.js b/app/controllers/forms.server.controller.js index 2face111..3eadee81 100644 --- a/app/controllers/forms.server.controller.js +++ b/app/controllers/forms.server.controller.js @@ -9,7 +9,14 @@ var mongoose = require('mongoose'), FormSubmission = mongoose.model('FormSubmission'), config = require('../../config/config'), diff = require('deep-diff'), - _ = require('lodash'); + _ = require('lodash'), + nodemailer = require('nodemailer'), + emailNotifications = require('../libs/send-email-notifications'), + constants = require('../libs/constants'), + helpers = require('./helpers.server.controller'), + async = require('async'); + +var smtpTransport = nodemailer.createTransport(config.mailer.options); /** * Delete a forms submissions @@ -28,17 +35,7 @@ exports.deleteSubmissions = function(req, res) { return; } - form.analytics.visitors = []; - form.save(function(formSaveErr){ - if(formSaveErr){ - res.status(400).send({ - message: errorHandler.getErrorMessage(formSaveErr) - }); - return; - } - res.status(200).send('Form submissions successfully deleted'); - - }); + res.status(200).send('Form submissions successfully deleted'); }); }; @@ -69,7 +66,54 @@ exports.createSubmission = function(req, res) { message: errorHandler.getErrorMessage(err) }); } - res.status(200).send('Form submission successfully saved'); + var form = req.body; + var formFieldDict = emailNotifications.createFieldDict(form.form_fields); + + async.waterfall([ + function(callback) { + if (form.selfNotifications && form.selfNotifications.enabled) { + if(form.selfNotifications.fromField){ + form.selfNotifications.fromEmails = formFieldDict[form.selfNotifications.fromField]; + } else { + form.selfNotifications.fromEmails = config.mailer.options.from; + } + + emailNotifications.send(form.selfNotifications, formFieldDict, smtpTransport, function(err){ + if(err){ + return callback({ + message: 'Failure sending submission self-notification email' + }); + } + + callback(); + }); + } else { + callback(); + } + }, + function(callback) { + if (form.respondentNotifications && form.respondentNotifications.enabled && form.respondentNotifications.toField) { + + form.respondentNotifications.toEmails = formFieldDict[form.respondentNotifications.toField]; + emailNotifications.send(form.respondentNotifications, formFieldDict, smtpTransport, function(err){ + if(err){ + return callback({ + message: 'Failure sending submission respondent-notification email' + }); + } + + callback(); + }); + } else { + callback(); + } + } + ], function (err) { + if(err){ + return res.status(400).send(err); + } + res.status(200).send('Form submission successfully saved'); + }); }); }; @@ -82,7 +126,7 @@ exports.listSubmissions = function(req, res) { FormSubmission.find({ form: _form._id }).sort('created').lean().exec(function(err, _submissions) { if (err) { console.error(err); - res.status(500).send({ + return res.status(500).send({ message: errorHandler.getErrorMessage(err) }); } @@ -90,29 +134,183 @@ exports.listSubmissions = function(req, res) { }); }; +/** + * Get Visitor Analytics Data for a given Form + */ +exports.getVisitorData = function(req, res) { + var results = []; + + Form.aggregate([ + { + $match: { + _id: mongoose.Types.ObjectId(req.form.id), + admin: mongoose.Types.ObjectId(req.user.id) + } + }, + { + $facet: { + 'deviceStatistics': [ + { + $unwind: '$analytics.visitors' + }, + { + $project: { + _id: 0, + deviceType: '$analytics.visitors.deviceType', + SubmittedTimeElapsed: { + $cond: [ + { + $eq: ['$analytics.visitors.isSubmitted', true] + }, + '$analytics.visitors.timeElapsed', + 0 + ] + }, + SubmittedResponses: { + $cond: [ + { + $eq: ['$analytics.visitors.isSubmitted', true] + }, + 1, + 0 + ] + } + } + }, + { + $group: { + _id: '$deviceType', + total_time: { $sum: '$SubmittedTimeElapsed' }, + responses: { $sum: '$SubmittedResponses' }, + visits: { $sum: 1 } + } + }, + { + $project: { + total_time: '$total_time', + responses: '$responses', + visits: '$visits', + average_time: { + $cond: [ + { $eq: [ '$responses', 0 ] }, + 0, + { $divide: ['$total_time', '$responses'] } + ] + }, + conversion_rate: { + $multiply: [ + 100, + { + $cond: [ + { $eq: [ '$visits', 0 ] }, + 0, + { $divide: ['$responses', '$visits'] } + ] + } + ] + } + } + } + ], + 'globalStatistics': [ + { + $unwind: '$analytics.visitors' + }, + { + $project: { + _id: 0, + deviceType: '$analytics.visitors.deviceType', + SubmittedTimeElapsed: { + $cond: [ + { + $eq: ['$analytics.visitors.isSubmitted', true] + }, + '$analytics.visitors.timeElapsed', + 0 + ] + }, + SubmittedResponses: { + $cond: [ + { + $eq: ['$analytics.visitors.isSubmitted', true] + }, + 1, + 0 + ] + } + } + }, + { + $group: { + _id: null, + total_time: { $sum: '$SubmittedTimeElapsed' }, + responses: { $sum: '$SubmittedResponses' }, + visits: { $sum: 1 } + } + }, + { + $project: { + _id: 0, + total_time: '$total_time', + responses: '$responses', + visits: '$visits', + average_time: { + $cond: [ + { $eq: [ '$responses', 0 ] }, + 0, + { $divide: ['$total_time', '$responses'] } + ] + }, + conversion_rate: { + $multiply: [ + 100, + { + $cond: [ + { $eq: [ '$visits', 0 ] }, + 0, + { $divide: ['$responses', '$visits'] } + ] + } + ] + } + } + } + ], + } + } + ]) + .cursor() + .exec() + .on('end', function() { + res.json(results); + }) + .on('data', function(entry){ + results.push(entry); + }); +}; + /** * Create a new form */ exports.create = function(req, res) { - if(!req.body.form){ return res.status(400).send({ message: 'Invalid Input' }); } - var form = new Form(req.body.form); + var form = new Form(req.body.form); form.admin = req.user._id; - form.save(function(err) { - debugger; + form.save(function(err, createdForm) { if (err) { return res.status(500).send({ message: errorHandler.getErrorMessage(err) }); } - return res.json(form); + createdForm = helpers.removeSensitiveModelData('private_form', createdForm.toJSON()); + return res.json(createdForm); }); }; @@ -123,16 +321,14 @@ exports.read = function(req, res) { if(!req.user || (req.form.admin.id !== req.user.id) ){ readForRender(req, res); } else { - var newForm = req.form.toJSON(); - - if (req.userId) { - if(req.form.admin._id+'' === req.userId+''){ - return res.json(newForm); - } + if(!req.form){ return res.status(404).send({ message: 'Form Does Not Exist' }); } + + var newForm = helpers.removeSensitiveModelData('private_form', req.form.toJSON()); + return res.json(newForm); } }; @@ -148,9 +344,7 @@ var readForRender = exports.readForRender = function(req, res) { }); } - delete newForm.lastModified; - delete newForm.__v; - delete newForm.created; + newForm = helpers.removeSensitiveModelData('public_form', newForm); if(newForm.startPage && !newForm.startPage.showStart){ delete newForm.startPage; @@ -166,15 +360,12 @@ exports.update = function(req, res) { var form = req.form; var updatedForm = req.body.form; - if(form.form_fields === undefined){ - form.form_fields = []; - } - if(form.analytics === undefined){ + if(!form.analytics && req.body.form.analytics){ form.analytics = { visitors: [], gaCode: '' - } + }; } if (req.body.changes) { @@ -184,19 +375,23 @@ exports.update = function(req, res) { diff.applyChange(form._doc, true, change); }); } else { + if(!updatedForm){ + res.status(400).send({ + message: 'Updated Form is empty' + }); + } + delete updatedForm.lastModified; + delete updatedForm.created; + delete updatedForm.id; + delete updatedForm._id; delete updatedForm.__v; - delete updatedForm.created; + //Unless we have 'admin' privileges, updating the form's admin is disabled if(updatedForm && req.user.roles.indexOf('admin') === -1) { delete updatedForm.admin; } - if(form.analytics === null){ - form.analytics.visitors = []; - form.analytics.gaCode = ''; - } - //Do this so we can create duplicate fields var checkForValidId = new RegExp('^[0-9a-fA-F]{24}$'); for(var i=0; i < req.body.form.form_fields.length; i++){ @@ -214,6 +409,7 @@ exports.update = function(req, res) { message: errorHandler.getErrorMessage(err) }); } else { + savedForm = helpers.removeSensitiveModelData('private_form', savedForm.toJSON()); res.json(savedForm); } }); @@ -245,24 +441,53 @@ exports.list = function(req, res) { Form.find(searchObj) .sort('-created') - .select('title language admin submissions isLive') - .populate('admin.username', 'admin._id') + .select('title language isLive') .lean() .exec(function(err, forms) { if (err) { - res.status(400).send({ + return res.status(400).send({ message: errorHandler.getErrorMessage(err) }); - } else { - for(var i=0; i static', @@ -72,8 +91,9 @@ module.exports = { userRoleTypes: ['user', 'admin', 'superuser'], regex: { + username: /^[a-zA-Z0-9\-]+$/, url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/, hexCode: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, - email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + email: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, } }; \ No newline at end of file diff --git a/app/libs/send-email-notifications.js b/app/libs/send-email-notifications.js new file mode 100644 index 00000000..158882e7 --- /dev/null +++ b/app/libs/send-email-notifications.js @@ -0,0 +1,48 @@ +'use strict'; +const jsdom = require('jsdom'); +var JSDOM = jsdom.JSDOM; + +module.exports = { + send: function(emailSettings, emailTemplateVars, smtpTransport, cb){ + var parsedTemplate = this.parseTemplate(emailSettings.htmlTemplate, emailTemplateVars, false); + var parsedSubject = this.parseTemplate(emailSettings.subject, emailTemplateVars, true); + + var mailOptions = { + replyTo: emailSettings.fromEmails, + from: 'noreply@tellform.com', + cc: emailSettings.toEmails, + subject: parsedSubject, + html: parsedTemplate + }; + + smtpTransport.sendMail(mailOptions, function(err){ + cb(err); + }); + }, + + parseTemplate: function(emailTemplate, emailTemplateVars, onlyText){ + var dom = new JSDOM(''+emailTemplate); + + Object.keys(emailTemplateVars).forEach(function (key) { + var elem = dom.window.document.querySelector('span.placeholder-tag[data-id=\'' + key + '\']'); + if(elem !== null){ + elem.outerHTML = emailTemplateVars[key]; + } + }); + + if(onlyText){ + return dom.window.document.documentElement.textContent; + } + return dom.serialize(); + }, + + createFieldDict: function(form_fields){ + var formFieldDict = {}; + form_fields.forEach(function(field){ + if(field.hasOwnProperty('fieldValue') && field.hasOwnProperty('_id')){ + formFieldDict[field._id] = String(field.fieldValue); + } + }); + return formFieldDict; + } +}; \ No newline at end of file diff --git a/app/libs/timestamp.server.plugin.js b/app/libs/timestamp.server.plugin.js index 084e2c1b..bba1649d 100644 --- a/app/libs/timestamp.server.plugin.js +++ b/app/libs/timestamp.server.plugin.js @@ -2,38 +2,36 @@ // Plugin module.exports = function timestamp (schema, options) { - options || (options = {}) + options = options || (options === {}); // Options - var fields = {} - , createdPath = options.createdPath || 'created' - , modifiedPath = options.modifiedPath || 'modified' - , useVirtual = (options.useVirtual !== undefined) - ? options.useVirtual - : true + var fields = {}, + createdPath = options.createdPath || 'created', + modifiedPath = options.modifiedPath || 'modified', + useVirtual = (options.useVirtual !== undefined) ? options.useVirtual : true; // Add paths to schema if not present if (!schema.paths[createdPath]) { - fields[modifiedPath] = { type: Date } + fields[modifiedPath] = { type: Date }; } if (useVirtual) { // Use the ObjectID for extracting the created time schema.virtual(createdPath).get(function () { - return new Date(this._id.generationTime * 1000) - }) + return new Date(this._id.generationTime * 1000); + }); } else { if (!schema.paths[createdPath]) { fields[createdPath] = { - type: Date - , default: Date.now - } + type: Date, + default: Date.now + }; } } - schema.add(fields) + schema.add(fields); // Update the modified timestamp on save schema.pre('save', function (next) { - this[modifiedPath] = new Date - next() - }) -} \ No newline at end of file + this[modifiedPath] = new Date(); + next(); + }); +}; \ No newline at end of file diff --git a/app/models/form.server.model.js b/app/models/form.server.model.js index f9139f26..3b7e9513 100644 --- a/app/models/form.server.model.js +++ b/app/models/form.server.model.js @@ -8,19 +8,11 @@ var mongoose = require('mongoose'), _ = require('lodash'), timeStampPlugin = require('../libs/timestamp.server.plugin'), async = require('async'), - Random = require('random-js'), - mt = Random.engines.mt19937(); - - -mt.autoSeed(); + constants = require('../libs/constants'); //Mongoose Models var FieldSchema = require('./form_field.server.model.js'); -var FormSubmissionSchema = require('./form_submission.server.model.js'), - FormSubmission = mongoose.model('FormSubmission', FormSubmissionSchema); - - var ButtonSchema = new Schema({ url: { type: String, @@ -47,8 +39,8 @@ var VisitorDataSchema = new Schema({ referrer: { type: String }, - lastActiveField: { - type: Schema.Types.ObjectId + filledOutFields: { + type: [Schema.Types.ObjectId] }, timeElapsed: { type: Number @@ -57,11 +49,12 @@ var VisitorDataSchema = new Schema({ type: Boolean }, language: { - type: String + type: String, + enum: constants.languageTypes, + default: 'en', }, ipAddr: { - type: String, - default: '' + type: String }, deviceType: { type: String, @@ -100,13 +93,10 @@ var FormSchema = new Schema({ }, visitors: [VisitorDataSchema] }, - - form_fields: [FieldSchema], - submissions: [{ - type: Schema.Types.ObjectId, - ref: 'FormSubmission' - }], - + form_fields: { + type: [FieldSchema], + default: [] + }, admin: { type: Schema.Types.ObjectId, ref: 'User', @@ -149,17 +139,59 @@ var FormSchema = new Schema({ buttons:[ButtonSchema] }, - hideFooter: { - type: Boolean, - default: false + selfNotifications: { + fromField: { + type: String + }, + toEmails: { + type: String + }, + subject: { + type: String + }, + htmlTemplate: { + type: String + }, + enabled: { + type: Boolean, + default: false + } }, + + respondentNotifications: { + toField: { + type: String + }, + fromEmails: { + type: String, + match: [/.+\@.+\..+/, 'Please fill a valid email address'] + }, + subject: { + type: String, + default: 'Tellform: Thank you for filling out this TellForm' + }, + htmlTemplate: { + type: String, + default: 'Hello,

We’ve received your submission.

Thank you & have a nice day!', + }, + enabled: { + type: Boolean, + default: false + } + }, + + showFooter: { + type: Boolean, + default: true + }, + isLive: { type: Boolean, default: true }, design: { - colors:{ + colors: { backgroundColor: { type: String, match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/], @@ -190,98 +222,6 @@ var FormSchema = new Schema({ } }, formSchemaOptions); -/* -** In-Form Analytics Virtual Attributes - */ -FormSchema.virtual('analytics.views').get(function () { - if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){ - return this.analytics.visitors.length; - } else { - return 0; - } -}); - -FormSchema.virtual('analytics.submissions').get(function () { - return this.submissions.length; -}); - -FormSchema.virtual('analytics.conversionRate').get(function () { - if(this.analytics && this.analytics.visitors && this.analytics.visitors.length > 0){ - return this.submissions.length/this.analytics.visitors.length*100; - } else { - return 0; - } -}); - -FormSchema.virtual('analytics.fields').get(function () { - var fieldDropoffs = []; - var visitors = this.analytics.visitors; - var that = this; - - if(!this.form_fields || this.form_fields.length === 0) { - return null; - } - - for(var i=0; i i){ - return sum + 1; - } - return sum; - }, 0); - } else { - continueViews = _.reduce(visitors, function(sum, visitorObj){ - if(visitorObj.lastActiveField+'' === field._id+'' && visitorObj.isSubmitted){ - return sum + 1; - } - return sum; - }, 0); - - } - - var totalViews = dropoffViews+continueViews; - var continueRate = 0; - var dropoffRate = 0; - - if(totalViews > 0){ - continueRate = (continueViews/totalViews*100).toFixed(0); - dropoffRate = (dropoffViews/totalViews*100).toFixed(0); - } - - fieldDropoffs[i] = { - dropoffViews: dropoffViews, - responses: continueViews, - totalViews: totalViews, - continueRate: continueRate, - dropoffRate: dropoffRate, - field: field - }; - - } - } - - return fieldDropoffs; -}); - FormSchema.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', @@ -289,159 +229,16 @@ FormSchema.plugin(timeStampPlugin, { }); FormSchema.pre('save', function (next) { - switch(this.language){ - case 'spanish': - this.language = 'es'; - break; - case 'french': - this.language = 'fr'; - break; - case 'italian': - this.language = 'it'; - break; - case 'german': - this.language = 'de'; - break; - default: - break; + if(this.form_fields && this.form_fields.length){ + this.form_fields = this.form_fields.filter(function(field){ + return !field.deletePreserved; + }); } next(); }); -function getDeletedIndexes(needle, haystack){ - var deletedIndexes = []; - - if(haystack.length > 0){ - for(var i = 0; i < needle.length; i++){ - if(haystack.indexOf(needle[i]) === -1){ - deletedIndexes.push(i); - } - } - } - return deletedIndexes; -} - -function formFieldsAllHaveIds(form_fields){ - for(var i=0; i 0 ){ - - var modifiedSubmissions = []; - - async.forEachOfSeries(deletedIds, - function (deletedIdIndex, key, cb_id) { - - var deleted_id = old_ids[deletedIdIndex]; - //Find FormSubmissions that contain field with _id equal to 'deleted_id' - FormSubmission. - find({ form: that, form_fields: {$elemMatch: {globalId: deleted_id} } }). - exec(function(err, submissions){ - if(err) { - return cb_id(err); - } - - //Preserve fields that have at least one submission - if (submissions.length) { - //Add submissions - modifiedSubmissions.push.apply(modifiedSubmissions, submissions); - } - - return cb_id(null); - }); - }, - function (err) { - if(err){ - console.error(err.message); - return cb(err); - } - - //Iterate through all submissions with modified form_fields - async.forEachOfSeries(modifiedSubmissions, function (submission, key, callback) { - - var submission_form_fields = submission.toObject().form_fields; - var currentform_form_fields = that.toObject().form_fields; - - //Iterate through ids of deleted fields - for (var i = 0; i < deletedIds.length; i++) { - var index = _.findIndex(submission_form_fields, function (field) { - var tmp_id = field.globalId + ''; - return tmp_id === old_ids[deletedIds[i]]; - }); - - var deletedField = submission_form_fields[index]; - - //Hide field if it exists - if (deletedField) { - - //Delete old form_field - submission_form_fields.splice(index, 1); - - deletedField.deletePreserved = true; - - //Move deleted form_field to start - submission_form_fields.unshift(deletedField); - currentform_form_fields.unshift(deletedField); - } - } - submission.form_fields = submission_form_fields; - that.form_fields = currentform_form_fields; - - return callback(null); - }, function (err) { - return cb(err); - }); - }); - } else { - return cb(null); - } - } else { - return cb(null); - } - } - ], - function(err){ - if(err){ - return next(err); - } - next(); - }); -}); - FormSchema.index({created: 1}); mongoose.model('Form', FormSchema); +module.exports = mongoose.model('Form'); diff --git a/app/models/form_field.server.model.js b/app/models/form_field.server.model.js index 31f96e71..9338a9d7 100644 --- a/app/models/form_field.server.model.js +++ b/app/models/form_field.server.model.js @@ -9,7 +9,8 @@ var mongoose = require('mongoose'), _ = require('lodash'), Schema = mongoose.Schema, LogicJumpSchema = require('./logic_jump.server.model'), - tokgen = require('../libs/tokenGenerator'); + tokgen = require('../libs/tokenGenerator'), + constants = require('../libs/constants'); var FieldOptionSchema = new Schema({ option_id: { @@ -34,21 +35,7 @@ var RatingFieldSchema = new Schema({ }, shape: { type: String, - enum: [ - 'Heart', - 'Star', - 'thumbs-up', - 'thumbs-down', - 'Circle', - 'Square', - 'Check Circle', - 'Smile Outlined', - 'Hourglass', - 'bell', - 'Paper Plane', - 'Comment', - 'Trash' - ] + enum: constants.ratingShapeTypes }, validShapes: { type: [String] @@ -62,9 +49,6 @@ function BaseFieldSchema(){ Schema.apply(this, arguments); this.add({ - globalId: { - type: String, - }, isSubmission: { type: Boolean, default: false @@ -85,6 +69,7 @@ function BaseFieldSchema(){ ratingOptions: RatingFieldSchema, fieldOptions: [FieldOptionSchema], + required: { type: Boolean, default: true @@ -103,31 +88,12 @@ function BaseFieldSchema(){ }, fieldType: { type: String, - enum: [ - 'textfield', - 'date', - 'email', - 'link', - 'legal', - 'url', - 'textarea', - 'statement', - 'welcome', - 'thankyou', - 'file', - 'dropdown', - 'scale', - 'rating', - 'radio', - 'checkbox', - 'hidden', - 'yes_no', - 'natural', - 'stripe', - 'number' - ] + enum: constants.fieldTypes }, - fieldValue: Schema.Types.Mixed + fieldValue: { + type: Schema.Types.Mixed, + default: '' + } }); this.plugin(timeStampPlugin, { @@ -140,7 +106,7 @@ function BaseFieldSchema(){ this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues; if(this.fieldType === 'rating' && this.ratingOptions.validShapes.length === 0){ - this.ratingOptions.validShapes = mongoose.model('RatingOptions').schema.path('shape').enumValues; + this.ratingOptions.validShapes = constants.ratingShapeTypes; } next(); @@ -162,19 +128,14 @@ FormFieldSchema.pre('validate', function(next) { return(next(error)); } - }else{ + } else { //Setting default values for ratingOptions - if(!this.ratingOptions.steps){ + if(!this.ratingOptions.steps) { this.ratingOptions.steps = 10; } if(!this.ratingOptions.shape){ this.ratingOptions.shape = 'Star'; } - - //Checking that the fieldValue is between 0 and ratingOptions.steps - if(this.fieldValue+0 > this.ratingOptions.steps || this.fieldValue+0 < 0){ - this.fieldValue = 1; - } } @@ -183,27 +144,18 @@ FormFieldSchema.pre('validate', function(next) { if(this.fieldOptions && this.fieldOptions.length > 0){ error.errors.ratingOptions = new mongoose.Error.ValidatorError({path:'fieldOptions', message: 'fieldOptions are only allowed for type dropdown, checkbox or radio fields.', type: 'notvalid', value: this.ratingOptions}); console.error(error); - return(next(error)); + return next(error); } } return next(); }); -//LogicJump Save -FormFieldSchema.pre('save', function(next) { - if(!this.globalId){ - this.globalId = tokgen(); - } - next(); -}); - //Submission fieldValue correction FormFieldSchema.pre('save', function(next) { if(this.fieldType === 'dropdown' && this.isSubmission){ this.fieldValue = this.fieldValue.option_value; } - return next(); }); diff --git a/app/models/form_submission.server.model.js b/app/models/form_submission.server.model.js index a3505f3a..a7f93bd5 100644 --- a/app/models/form_submission.server.model.js +++ b/app/models/form_submission.server.model.js @@ -6,7 +6,9 @@ var mongoose = require('mongoose'), Schema = mongoose.Schema, timeStampPlugin = require('../libs/timestamp.server.plugin'), - FieldSchema = require('./form_field.server.model.js'); + FieldSchema = require('./form_field.server.model'), + helpers = require('../controllers/helpers.server.controller'), + constants = require('../libs/constants'); /** * Form Submission Schema @@ -55,18 +57,7 @@ FormSubmissionSchema.pre('save', function (next) { this.form_fields[i].fieldValue = this.form_fields[i].fieldValue.option_value; } - delete form_fields[i].validFieldTypes; - delete form_fields[i].disabled; - delete form_fields[i].required; - delete form_fields[i].isSubmission; - delete form_fields[i].title; - delete form_fields[i].fieldOptions; - delete form_fields[i].ratingOptions; - delete form_fields[i].logicJump; - delete form_fields[i].description; - delete form_fields[i].created; - delete form_fields[i].lastModified; - delete form_fields[i].deletePreserved; + helpers.removeKeysFromDict(this.form_fields[i], constants.extraneousFormFieldProps); } next(); }); @@ -77,19 +68,7 @@ FormSubmissionSchema.path('form_fields', { form_fields[i].isSubmission = true; form_fields[i]._id = new mongoose.mongo.ObjectID(); - delete form_fields[i].validFieldTypes; - delete form_fields[i].disabled; - delete form_fields[i].required; - delete form_fields[i].isSubmission; - delete form_fields[i].title; - delete form_fields[i].fieldOptions; - delete form_fields[i].ratingOptions; - delete form_fields[i].logicJump; - delete form_fields[i].description; - delete form_fields[i].created; - delete form_fields[i].lastModified; - delete form_fields[i].deletePreserved; - + helpers.removeKeysFromDict(form_fields[i], constants.extraneousFormFieldProps); } return form_fields; } @@ -101,4 +80,6 @@ FormSubmissionSchema.plugin(timeStampPlugin, { useVirtual: false }); -module.exports = FormSubmissionSchema; +mongoose.model('FormSubmission', FormSubmissionSchema); + +module.exports = mongoose.model('FormSubmission'); \ No newline at end of file diff --git a/app/models/plugins/languagePlugin.js b/app/models/plugins/languagePlugin.js deleted file mode 100644 index a7b9b6e6..00000000 --- a/app/models/plugins/languagePlugin.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const constants = require('../../libs/constants'), - config = require('../../../config/config'); - -module.exports = exports = function lastModifiedPlugin (schema, options) { - schema.add({ - language: { - type: String, - enum: constants.languageTypes, - default: config.defaultLanguage, - required: options.required || 'Must be a valid language' - } - }); - - schema.pre('save', function (next) { - var currWord = this.language; - - //English is the default backup language - this.language = 'en'; - if(constants.wordToLangCode.has(currWord)){ - this.language = constants.wordToLangCode[currWord]; - } - next(); - }); -}; \ No newline at end of file diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index 9c284aee..d5484c72 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -9,29 +9,8 @@ var mongoose = require('mongoose'), config = require('../../config/config'), timeStampPlugin = require('../libs/timestamp.server.plugin'), path = require('path'), - querystring = require('querystring'); - -/** - * A Validation function for local strategy properties - */ -var validateLocalStrategyProperty = function(property) { - var propHasLength; - if (property) { - propHasLength = !!property.length; - } else { - propHasLength = false; - } - - return ((this.provider !== 'local' && !this.updated) || propHasLength); -}; - -/** - * A Validation function for username - */ -var validateUsername = function(username) { - return (username.match(/^[a-zA-Z0-9.-_]+$/) !== null); -}; - + querystring = require('querystring'), + constants = require('../libs/constants'); /** * User Schema @@ -52,14 +31,14 @@ var UserSchema = new Schema({ trim: true, lowercase: true, unique: 'Account already exists with this email', - match: [/.+\@.+\..+/, 'Please fill a valid email address'], + match: [constants.regex.email, 'Please fill a valid email address'], required: [true, 'Email is required'] }, username: { type: String, unique: true, lowercase: true, - match: [/^[a-zA-Z0-9\-]+$/, 'Username can only contain alphanumeric characters and \'-\''], + match: [constants.regex.username, 'Username can only contain alphanumeric characters and \'-\''], required: [true, 'Username is required'] }, passwordHash: { @@ -73,18 +52,16 @@ var UserSchema = new Schema({ type: String, default: 'local' }, - providerData: {}, - additionalProvidersData: {}, roles: { type: [{ type: String, - enum: ['user', 'admin', 'superuser'] + enum: constants.userRoleTypes }], default: ['user'] }, language: { type: String, - enum: ['en', 'fr', 'es', 'it', 'de'], + enum: constants.languageTypes, default: 'en', }, lastModified: { @@ -111,10 +88,6 @@ var UserSchema = new Schema({ } }); -UserSchema.virtual('displayName').get(function () { - return this.firstName + ' ' + this.lastName; -}); - UserSchema.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', @@ -135,7 +108,7 @@ UserSchema.virtual('password').get(function () { /** * Create instance method for hashing a password */ -UserSchema.methods.hashPassword = function(password) { +UserSchema.statics.hashPassword = UserSchema.methods.hashPassword = function(password) { var encoding = 'base64'; var iterations = 10000; var keylen = 128; @@ -192,4 +165,6 @@ UserSchema.methods.isAdmin = function() { return false; }; -module.exports = mongoose.model('User', UserSchema); +mongoose.model('User', UserSchema); + +module.exports = mongoose.model('User'); \ No newline at end of file diff --git a/app/routes/forms.server.routes.js b/app/routes/forms.server.routes.js index 6798e525..601744a7 100644 --- a/app/routes/forms.server.routes.js +++ b/app/routes/forms.server.routes.js @@ -31,8 +31,8 @@ module.exports = function(app) { } app.route('/forms/:formIdFast([a-zA-Z0-9]+)') - .post(forms.createSubmission) - + .post(forms.createSubmission); + app.route('/forms') .get(auth.isAuthenticatedOrApiKey, forms.list) .post(auth.isAuthenticatedOrApiKey, forms.create); @@ -47,6 +47,9 @@ module.exports = function(app) { .get(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.listSubmissions) .delete(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.deleteSubmissions); + app.route('/forms/:formId([a-zA-Z0-9]+)/visitors') + .get(auth.isAuthenticatedOrApiKey, forms.hasAuthorization, forms.getVisitorData); + // Slower formId middleware app.param('formId', forms.formByID); diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js index 967176ce..89fbcd0e 100755 --- a/app/routes/users.server.routes.js +++ b/app/routes/users.server.routes.js @@ -12,6 +12,7 @@ module.exports = function(app) { var users = require('../../app/controllers/users.server.controller'); // Setting up the users profile api + app.route('/users/password').post(users.requiresLogin, users.changePassword); app.route('/users/me').get(auth.isAuthenticatedOrApiKey, users.getUser); app.route('/users').put(auth.isAuthenticatedOrApiKey, users.update); @@ -19,8 +20,7 @@ module.exports = function(app) { app.route('/auth/verify/:token').get(users.validateVerificationToken); app.route('/auth/verify').post(users.resendVerificationEmail); - // Setting up the users password api - app.route('/users/password').post(users.requiresLogin, users.changePassword); + // Setting up the password reset api app.route('/auth/forgot').post(users.forgot); app.route('/auth/reset/:token').get(users.validateResetToken); app.route('/auth/reset/:token').post(users.reset); @@ -33,7 +33,4 @@ module.exports = function(app) { app.route('/auth/signout').get(users.signout); app.route('/auth/genkey').get(users.requiresLogin, users.generateAPIKey); - - // Finish by binding the user middleware - app.param('userId', users.userByID); }; diff --git a/app/sockets/analytics_service.js b/app/sockets/analytics_service.js index 993a0468..4f85dc4a 100644 --- a/app/sockets/analytics_service.js +++ b/app/sockets/analytics_service.js @@ -13,38 +13,30 @@ module.exports = function (io, socket) { var visitorsData = {}; var saveVisitorData = function (data, socket, cb){ - Form.findById(data.formId, function(err, form) { - if (err) { - console.error(err); - throw new Error(errorHandler.getErrorMessage(err)); - } + Form.findByIdAndUpdate( + data.formId, + { + $push: { + 'analytics.visitors': { + socketId: data.socketId, + referrer: data.referrer, + timeElapsed: data.timeElapsed, + isSubmitted: data.isSubmitted, + language: data.language, + ipAddr: '', + deviceType: data.deviceType + } + } + }, + function(err, form) { + if (err) { + console.error(err); + throw new Error(errorHandler.getErrorMessage(err)); + } - var newVisitor = { - socketId: data.socketId, - referrer: data.referrer, - lastActiveField: data.lastActiveField, - timeElapsed: data.timeElapsed, - isSubmitted: data.isSubmitted, - language: data.language, - ipAddr: '', - deviceType: data.deviceType - }; - - form.analytics.visitors.push(newVisitor); - - - form.form_fields = form.form_fields.map(v => Object.assign({}, v, { fieldValue: null })); - - form.save(function (formSaveErr) { - if (err) { - console.error(err); - throw new Error(errorHandler.getErrorMessage(formSaveErr)); - } - - if(cb){ - return cb(); - } - }); + if(cb){ + return cb(); + } }); }; @@ -55,7 +47,6 @@ module.exports = function (io, socket) { visitorsData[current_socket.id].socketId = current_socket.id; visitorsData[current_socket.id].isSaved = false; - if (data.isSubmitted && !data.isSaved) { visitorsData[current_socket.id].isSaved = true; saveVisitorData(data, function() { diff --git a/app/tests/form.server.model.test.js b/app/tests/form.server.model.test.js index 573e631b..c2c16a65 100644 --- a/app/tests/form.server.model.test.js +++ b/app/tests/form.server.model.test.js @@ -7,8 +7,8 @@ require('../../server.js'); */ var should = require('should'), mongoose = require('mongoose'), - User = mongoose.model('User'), - Form = mongoose.model('Form'); + User = require('../models/user.server.model.js'), + Form = require('../models/form.server.model.js'); /** * Globals @@ -40,8 +40,8 @@ describe('Form Model Unit Tests:', function() { language: 'en', form_fields: [ {'fieldType':'textfield', title:'First Name', 'fieldValue': ''}, - {'fieldType':'checkbox', title:'nascar', 'fieldValue': ''}, - {'fieldType':'checkbox', title:'hockey', 'fieldValue': ''} + {'fieldType':'legal', title:'nascar', 'fieldValue': ''}, + {'fieldType':'legal', title:'hockey', 'fieldValue': ''} ] }); done(); diff --git a/app/tests/form.server.routes.test.js b/app/tests/form.server.routes.test.js index 93c5e6cb..b967a364 100644 --- a/app/tests/form.server.routes.test.js +++ b/app/tests/form.server.routes.test.js @@ -6,11 +6,26 @@ var should = require('should'), request = require('supertest'), Session = require('supertest-session'), mongoose = require('mongoose'), - User = mongoose.model('User'), - Form = mongoose.model('Form'), + User = require('../models/user.server.model.js'), + Form = require('../models/form.server.model.js'), + FormSubmission = require('../models/form_submission.server.model.js'), Field = mongoose.model('Field'), - FormSubmission = mongoose.model('FormSubmission'), - async = require('async'); + async = require('async'), + _ = require('lodash'); + +function omitDeep(collection, excludeKeys) { + + function omitFn(value) { + + if (value && typeof value === 'object') { + excludeKeys.forEach((key) => { + delete value[key]; + }); + } + } + + return _.cloneDeepWith(collection, omitFn); +} /** * Globals @@ -24,6 +39,18 @@ var credentials = { password: 'password' }; +var sampleVisitorData = [{ + socketId: 'ntneooe8989eotnoeeo', + referrer: 'http://google.com', + timeElapsed: 89898989, + isSubmitted: true, + language: 'en', + ipAddr: '192.168.1.1', + deviceType: 'desktop', + userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', + filledOutFields: [] +}]; + /** * Form routes tests */ @@ -50,8 +77,8 @@ describe('Form Routes Unit tests', function() { admin: user.id, form_fields: [ new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''}) + new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''}) ], isLive: true }; @@ -69,7 +96,6 @@ describe('Form Routes Unit tests', function() { .send({form: myForm}) .expect(401) .end(function(FormSaveErr, FormSaveRes) { - console.log(FormSaveRes.text); // Call the assertion callback done(FormSaveErr); }); @@ -92,7 +118,7 @@ describe('Form Routes Unit tests', function() { FormObj.save(function(err, form) { if(err) return done(err); - userSession.get('/subdomain/' + credentials.username + '/forms/' + form._id + '/render') + userSession.get('/forms/' + form._id + '/render') .expect(200) .end(function(err, res) { if(err) return done(err) @@ -115,7 +141,7 @@ describe('Form Routes Unit tests', function() { FormObj.save(function(err, form) { if(err) return done(err); - userSession.get('/subdomain/' + credentials.username + '/forms/' + form._id + '/render') + userSession.get('/forms/' + form._id + '/render') .expect(401, {message: 'Form is Not Public'}) .end(function(err, res) { done(err); @@ -167,7 +193,7 @@ describe('Form Routes Unit tests', function() { it(' > should not be able to create a Form if body is empty', function(done) { loginSession.post('/forms') .send({form: null}) - .expect(400, {"message":"Invalid Input"}) + .expect(400, {'message':'Invalid Input'}) .end(function(FormSaveErr, FormSaveRes) { // Call the assertion callback done(FormSaveErr); @@ -316,8 +342,8 @@ describe('Form Routes Unit tests', function() { admin: user.id, form_fields: [ new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''}) + new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''}) ], isLive: true }; @@ -328,8 +354,8 @@ describe('Form Routes Unit tests', function() { admin: user.id, form_fields: [ new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'formula one', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'football', 'fieldValue': ''}) + new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''}) ], isLive: true }; @@ -365,6 +391,123 @@ describe('Form Routes Unit tests', function() { }); }); + it(' > should preserve visitor data when updating a Form', function(done) { + // Create new Form model instance + + var formObject = { + title: 'First Form', + language: 'en', + admin: user.id, + form_fields: [ + new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''}) + ], + isLive: true, + analytics: { + gaCode: '', + visitors: sampleVisitorData + } + }; + + var formUpdateObject = { + title: 'Second Form', + language: 'en', + admin: user.id, + form_fields: [ + new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''}) + ], + isLive: true + }; + + var CurrentForm = new Form(formObject); + + // Save the Form + CurrentForm.save(function(err, form) { + if(err) return done(err); + + loginSession.put('/forms/' + form.id) + .send({ form: formUpdateObject }) + .expect(200) + .end(function(err, res) { + + should.not.exist(err); + + Form.findById(form.id, function (FormFindErr, UpdatedForm){ + should.not.exist(FormFindErr); + should.exist(UpdatedForm); + + var updatedFormObj = UpdatedForm.toJSON(); + var oldFormObj = CurrentForm.toJSON(); + + updatedFormObj.analytics.should.deepEqual(oldFormObj.analytics); + + done(FormFindErr); + }); + }); + }); + }); + + it(' > shouldn\'t allow a user to change the id when updating a form', function(done) { + // Create new Form model instance + + var formObject = { + title: 'First Form', + language: 'en', + admin: user.id, + form_fields: [ + new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''}) + ], + isLive: true + }; + + var formUpdateObject = { + id: mongoose.Types.ObjectId(), + title: 'First Form', + language: 'en', + admin: user.id, + form_fields: [ + new Field({'fieldType':'textfield', 'title':'Last Name', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'formula one', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'football', 'fieldValue': ''}) + ], + isLive: true + }; + + var CurrentForm = new Form(formObject); + + // Save the Form + CurrentForm.save(function(err, InitialForm) { + if(err) return done(err); + + loginSession.put('/forms/' + InitialForm.id) + .send({ form: formUpdateObject }) + .expect(200) + .end(function(err, OldForm) { + should.not.exist(err); + + Form.findById(InitialForm.id, function (FormFindErr, UpdatedForm){ + should.not.exist(FormFindErr); + should.exist(UpdatedForm); + + var updatedFormObj = UpdatedForm.toJSON(); + var oldFormObj = InitialForm.toJSON(); + + updatedFormObj = omitDeep('lastModified'); + oldFormObj = omitDeep('lastModified'); + + updatedFormObj.should.deepEqual(oldFormObj); + + done(FormFindErr); + }); + }); + }); + }); + afterEach('should be able to signout user', function(done){ authenticatedSession.get('/auth/signout') .expect(200) diff --git a/app/tests/form_submission.model.test.js b/app/tests/form_submission.model.test.js index 09442c81..d08851a0 100644 --- a/app/tests/form_submission.model.test.js +++ b/app/tests/form_submission.model.test.js @@ -11,13 +11,12 @@ var should = require('should'), _ = require('lodash'), async = require('async'), config = require('../../config/config'), - FormSubmission = mongoose.model('FormSubmission'); + FormSubmission = require('../models/form_submission.server.model.js'); var exampleDemo = { address: '880-9650 Velit. St.', city: '', dateOfBirth: '10', - displayName: 'Test User', email: 'polydaic@gmail.com', firstName: 'Test User', hin: '', @@ -82,9 +81,8 @@ describe('FormSubmission Model Unit Tests:', function() { user = new User({ firstName: 'Full', lastName: 'Name', - displayName: 'Full Name', - email: 'test1@test.com'+Date.now(), - username: 'test1'+Date.now(), + email: 'test1@test.com', + username: 'test1', password: 'password', provider: 'local' }); @@ -168,7 +166,7 @@ describe('FormSubmission Model Unit Tests:', function() { }); it('should be able to find FormSubmission by $elemMatch on form_fields id', function(done){ - FormSubmission.findOne({ form: myForm._id, form_fields: {$elemMatch: {globalId: myForm.form_fields[0].globalId} } }) + FormSubmission.findOne({ form: myForm.id, form_fields: {$elemMatch: {_id: myForm.form_fields[0]._id} } }) .exec(function(err, submission){ should.not.exist(err); should.exist(submission); @@ -178,76 +176,6 @@ describe('FormSubmission Model Unit Tests:', function() { }); }); - describe('Test FormField and Submission Logic', function() { - - beforeEach(function(done){ - - //Create Submission - mySubmission = new FormSubmission({ - form_fields: _.merge(sampleSubmission, myForm.form_fields), - admin: user, - form: myForm, - timeElapsed: 17.55 - }); - - mySubmission.save(function(err){ - should.not.exist(err); - done(); - }); - - }); - - it('should preserve deleted form_fields that have submissions without any problems', function(done) { - - var fieldPropertiesToOmit = ['deletePreserved', 'globalId', 'lastModified', 'created', '_id', 'submissionId', 'isSubmission', 'validFieldTypes', 'title']; - var old_fields = myForm.toObject().form_fields; - var new_form_fields = _.clone(myForm.toObject().form_fields); - new_form_fields.splice(0, 1); - - myForm.form_fields = new_form_fields; - - myForm.save(function(err, _form) { - - should.not.exist(err); - should.exist(_form.form_fields); - - var actual_fields = _.deepOmit(_form.toObject().form_fields, fieldPropertiesToOmit); - old_fields = _.deepOmit(old_fields, fieldPropertiesToOmit); - - should.deepEqual(actual_fields, old_fields, 'old form_fields not equal to newly saved form_fields'); - done(); - }); - }); - - it('should delete \'preserved\' form_fields whose submissions have been removed without any problems', function(done) { - - var old_fields = myForm.toObject().form_fields; - old_fields.splice(0,1); - var new_form_fields = _.clone(myForm.toObject().form_fields); - new_form_fields.splice(0, 1); - - myForm.form_fields = new_form_fields; - - myForm.save(function(err, _form){ - should.not.exist(err); - should.exist(_form.form_fields); - should.exist(old_fields); - - var actual_fields = _.deepOmit(_form.toObject().form_fields, ['lastModified', 'created', '_id']); - old_fields = _.deepOmit(old_fields, ['lastModified', 'created', '_id']); - - should.deepEqual(JSON.stringify(actual_fields), JSON.stringify(old_fields)); //'old form_fields not equal to newly saved form_fields'); - done(); - }); - }); - - afterEach(function(done){ - mySubmission.remove(function(){ - done(); - }); - }); - }); - afterEach(function(done) { Form.remove().exec(function() { User.remove().exec(function() { diff --git a/app/tests/form_submission.routes.test.js b/app/tests/form_submission.routes.test.js index 13d5e780..17c77ace 100644 --- a/app/tests/form_submission.routes.test.js +++ b/app/tests/form_submission.routes.test.js @@ -21,15 +21,14 @@ var credentials, user; * Form routes tests */ describe('Form Submission Routes Unit tests', function() { - var FormObj, _Submission, submissionSession, _SubmissionBody - + var FormObj, _Submission, submissionSession, _SubmissionBody; beforeEach(function(done) { // Create user credentials credentials = { - email: 'test@test.com', - username: 'test', + email: 'test423@test.com', + username: 'test534', password: 'password' }; @@ -45,16 +44,34 @@ describe('Form Submission Routes Unit tests', function() { // Save a user to the test db and create new Form user.save(function(err) { - if(err) return done(err); + if(err) { + return done(err); + } + FormObj = new Form({ title: 'Form Title', language: 'en', admin: user._id, form_fields: [ new Field({'fieldType':'textfield', 'title':'First Name', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'nascar', 'fieldValue': ''}), - new Field({'fieldType':'checkbox', 'title':'hockey', 'fieldValue': ''}) - ] + new Field({'fieldType':'legal', 'title':'nascar', 'fieldValue': ''}), + new Field({'fieldType':'legal', 'title':'hockey', 'fieldValue': ''}) + ], + selfNotifications: { + fromField: mongoose.Types.ObjectId(), + toEmails: 'john@smith.com', + subject: 'Hello there', + htmlTemplate: '

A form was submitted

', + enabled: true + }, + + respondentNotifications: { + toField: mongoose.Types.ObjectId(), + fromEmails: 'john@smith.com', + subject: 'Tellform: Thank you for filling out this TellForm', + htmlTemplate:'Hello,

We’ve received your submission.

Thank you & have a nice day!', + enabled: true + } }); FormObj.save(function(formSaveErr, form) { @@ -64,8 +81,8 @@ describe('Form Submission Routes Unit tests', function() { form: form._id, form_fields: [ {'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David', _id: '', isSubmission: false, deletePreserved: false}, - {'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true}, - {'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false} + {'fieldType':'legal', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true}, + {'fieldType':'legal', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false} ], percentageComplete: 100, timeElapsed: 11.55, @@ -84,8 +101,8 @@ describe('Form Submission Routes Unit tests', function() { _id: form._id, form_fields: [ {'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David', _id: '', isSubmission: false, deletePreserved: false}, - {'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true}, - {'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false} + {'fieldType':'legal', 'title':'nascar', 'fieldValue': true, _id: '', isSubmission: false, deletePreserved: true}, + {'fieldType':'legal', 'title':'hockey', 'fieldValue': false, _id: '', isSubmission: false, deletePreserved: false} ], percentageComplete: 100, timeElapsed: 11.55, @@ -237,6 +254,4 @@ describe('Form Submission Routes Unit tests', function() { }); }); }); - - }); diff --git a/app/tests/libs/send-email-notifications.test.js b/app/tests/libs/send-email-notifications.test.js new file mode 100644 index 00000000..772328ee --- /dev/null +++ b/app/tests/libs/send-email-notifications.test.js @@ -0,0 +1,82 @@ +'use strict'; + +/** + * Module dependencies. + */ +const should = require('should'), + emailNotifications = require('../../libs/send-email-notifications'), + mockTransport = require('nodemailer').createTransport({ + jsonTransport: true + }), + config = require('../../../config/config'); + +/** + * Globals + */ +const validFormFields = [ + {fieldType:'textfield', title:'First Name', fieldValue: 'John Smith', deletePreserved: false, _id:'56340745f59a6fc9e22028e9'}, + {fieldType:'link', title:'Your Website', fieldValue: 'https://johnsmith.me', deletePreserved: false, _id:'5c9e22028e907634f45f59a6'}, + {fieldType:'number', title:'Your Age', fieldValue: 45, deletePreserved: false, _id:'56e90745f5934fc9e22028a6'} +]; + +const validFieldDict = { + '56340745f59a6fc9e22028e9': 'John Smith', + '5c9e22028e907634f45f59a6': 'https://johnsmith.me', + '56e90745f5934fc9e22028a6': '45' +}; + +const invalidFormFields = [ + {fieldType:'textfield', title:'First Name', fieldValue: 'John Smith', deletePreserved: false}, + {fieldType:'link', title:'Your Website', deletePreserved: false, _id:'5c9e22028e907634f45f59a6'}, + {fieldType:'number', title:'Your Age'} +]; + +const htmlTemplate = '

First Name'+ + '
Your Website'+ + '
Your Age

'; + +const renderedTemplate = '

John Smith
https://johnsmith.me
45

'; + +/** + * Unit tests + */ +describe('Send Email Notification Unit Tests', function() { + + describe('Method createFieldDict', function() { + it('should be return a fieldDict from valid form fields', function() { + var actualFieldDict = emailNotifications.createFieldDict(validFormFields); + actualFieldDict.should.deepEqual(validFieldDict); + }); + + it('should return empty object if form fields are invalid or empty ', function() { + var actualFieldDict = emailNotifications.createFieldDict(invalidFormFields); + actualFieldDict.should.be.empty(); + }); + }); + + describe('Method parseTemplate', function(){ + it('should properly render a template given a valid field dict', function() { + var actualRenderedTemplate = emailNotifications.parseTemplate(htmlTemplate, validFieldDict, false).replace((/ |\r\n|\n|\r|\t/gm),''); + actualRenderedTemplate.should.equal(renderedTemplate.replace((/ |\r\n|\n|\r|\t/gm),'')); + }); + }); + + describe('Method send', function() { + this.timeout(10000); + const emailSettings = { + fromEmails: 'somewhere@somewhere.com', + toEmails: 'there@there.com', + subject: 'Hello First Name!', + htmlTemplate: htmlTemplate + }; + + const emailTemplateVars = validFieldDict; + + it('should properly replace a template var in a valid template', function(done) { + emailNotifications.send(emailSettings, emailTemplateVars, mockTransport, function(err){ + should.not.exist(err); + done(); + }); + }); + }); +}); diff --git a/app/tests/libs/timestamp.server.plugin.test.js b/app/tests/libs/timestamp.server.plugin.test.js index 2901fe72..d943ec27 100644 --- a/app/tests/libs/timestamp.server.plugin.test.js +++ b/app/tests/libs/timestamp.server.plugin.test.js @@ -1,70 +1,72 @@ +'use strict'; + // Dependencies -var util = require('util') - , assert = require('assert') - , mongoose = require('mongoose') - , timestamp = require('../../libs/timestamp.server.plugin') - , Schema = mongoose.Schema - , ObjectId = Schema.ObjectId +var util = require('util'), + assert = require('assert'), + mongoose = require('mongoose'), + timestamp = require('../../libs/timestamp.server.plugin'), + Schema = mongoose.Schema, + ObjectId = Schema.ObjectId; // Run tests describe('Timestamp', function () { describe('#default()', function () { - var FooSchema = new Schema() - FooSchema.plugin(timestamp) - var FooModel = mongoose.model('timeFoo', FooSchema) - , bar = new FooModel() + var FooSchema = new Schema(); + FooSchema.plugin(timestamp); + var FooModel = mongoose.model('timeFoo', FooSchema), + bar = new FooModel(); before(function () { FooModel.remove(function (err) { - assert.strictEqual(err, null) - }) - }) + assert.strictEqual(err, null); + }); + }); it('should have custom properties', function (done) { - assert.strictEqual(typeof FooSchema.virtuals.created, 'object') - assert.strictEqual(typeof FooSchema.paths.modified, 'object') - done() - }) + assert.strictEqual(typeof FooSchema.virtuals.created, 'object'); + assert.strictEqual(typeof FooSchema.paths.modified, 'object'); + done(); + }); it('should create the default attributes', function (done) { bar.save(function (err, doc) { - assert.strictEqual(err, null) - assert.strictEqual(util.isDate(doc.created), true) - assert.strictEqual(util.isDate(doc.modified), true) - done() - }) - }) - }) + assert.strictEqual(err, null); + assert.strictEqual(util.isDate(doc.created), true); + assert.strictEqual(util.isDate(doc.modified), true); + done(); + }); + }); + }); describe('#custom()', function () { - var FooSchema = new Schema() + var FooSchema = new Schema(); FooSchema.plugin(timestamp, { - createdPath: 'oh' - , modifiedPath: 'hai' - , useVirtual: false - }) - var BarModel = mongoose.model('timeBar', FooSchema) - , bar = new BarModel() + createdPath: 'oh', + modifiedPath: 'hai', + useVirtual: false + }); + var BarModel = mongoose.model('timeBar', FooSchema), + bar = new BarModel(); before(function () { BarModel.remove(function (err) { - assert.strictEqual(err, null) - }) - }) + assert.strictEqual(err, null); + }); + }); it('should have custom properties', function (done) { - assert.strictEqual(typeof FooSchema.paths.oh, 'object') - assert.strictEqual(typeof FooSchema.paths.hai, 'object') - done() - }) + assert.strictEqual(typeof FooSchema.paths.oh, 'object'); + assert.strictEqual(typeof FooSchema.paths.hai, 'object'); + done(); + }); it('should create custom attributes', function (done) { bar.save(function (err, doc) { - assert.strictEqual(err, null) - assert.strictEqual(util.isDate(doc.oh), true) - assert.strictEqual(util.isDate(doc.hai), true) - done() - }) - }) - }) -}) \ No newline at end of file + assert.strictEqual(err, null); + assert.strictEqual(util.isDate(doc.oh), true); + assert.strictEqual(util.isDate(doc.hai), true); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/app/tests/user.server.model.test.js b/app/tests/user.server.model.test.js index 5a7cb9ad..b15fb3b1 100755 --- a/app/tests/user.server.model.test.js +++ b/app/tests/user.server.model.test.js @@ -5,8 +5,8 @@ */ var should = require('should'), mongoose = require('mongoose'), - User = mongoose.model('User'); - + User = require('../models/user.server.model.js'); + /** * Globals */ diff --git a/app/tests/user.server.routes.test.js b/app/tests/user.server.routes.test.js index 89918285..7ed5e006 100644 --- a/app/tests/user.server.routes.test.js +++ b/app/tests/user.server.routes.test.js @@ -4,26 +4,25 @@ var should = require('should'), app = require('../../server'), Session = require('supertest-session'), mongoose = require('mongoose'), - User = mongoose.model('User'), + User = require('../models/user.server.model.js'), config = require('../../config/config'), - tmpUser = mongoose.model(config.tempUserCollection); + tmpUser = mongoose.model(config.tempUserCollection), + async = require('async'); /** * Globals */ -var credentials, _User, activateToken, userSession; +var credentials, _User, userSession; /** * Form routes tests */ describe('User CRUD tests', function() { - this.timeout(30000); - - beforeEach(function() { + before(function() { // Create user credentials credentials = { - email: 'test732@test.com', - username: 'test732', + email: 'test099@test.com', + username: 'test099', password: 'password3223' }; @@ -31,77 +30,424 @@ describe('User CRUD tests', function() { _User = { email: credentials.email, username: credentials.username, - password: credentials.password + password: credentials.password, + firstName: 'John', + lastName: 'Smith' }; //Initialize Session userSession = Session(app); }); - it(' > Create, Verify and Activate a User > ', function() { - - it('should be able to create a temporary (non-activated) User', function(done) { - userSession.post('/auth/signup') - .send(_User) - .expect(200) - .end(function(FormSaveErr) { - // Handle error - should.not.exist(FormSaveErr); - - tmpUser.findOne({username: _User.username}, function (err, user) { - should.not.exist(err); + describe(' > Create, Verify and Activate a User > ', function() { + this.timeout(10000); + it('should be able to create and activate a User', function(done) { + async.waterfall([ + function(callback) { + userSession.post('/auth/signup') + .send(_User) + .expect(200) + .end(function(err) { + callback(err); + }); + }, + function(callback) { + tmpUser.findOne({username: _User.username}) + .lean() + .exec(function (err, user) { should.exist(user); _User.username.should.equal(user.username); _User.firstName.should.equal(user.firstName); _User.lastName.should.equal(user.lastName); - activateToken = user.GENERATED_VERIFYING_URL; - - userSession.get('/auth/verify/'+activateToken) - .expect(200) - .end(function(VerifyErr, VerifyRes) { - // Handle error - if (VerifyErr) { - return done(VerifyErr); - } - - (VerifyRes.text).should.equal('User successfully verified'); - - userSession.post('/auth/signin') - .send(credentials) - .expect('Content-Type', /json/) - .expect(200) - .end(function(signinErr, signinRes) { - // Handle signin error - if (signinErr) { - return done(signinErr); - } - - var user = signinRes.body; - (user.username).should.equal(credentials.username); - - userSession.get('/auth/signout') - .expect(200) - .end(function(signoutErr, signoutRes) { - - // Handle signout error - if (signoutErr) { - return done(signoutErr); - } - - (signoutRes.text).should.equal('You have successfully logged out.'); - - done(); - }); - }); - }); + callback(err, user.GENERATED_VERIFYING_URL); }); - }); + }, + function(activateToken, callback) { + userSession.get('/auth/verify/' + activateToken) + .expect(200) + .end(function(err, res) { + (res.text).should.equal('User successfully verified'); + callback(err); + }); + }, + function(callback) { + userSession.post('/auth/signin') + .send(credentials) + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + (res.body.username).should.equal(credentials.username); + callback(err); + }); + }, + function(callback) { + userSession.get('/auth/signout') + .expect(200) + .end(function(err, res) { + (res.text).should.equal('You have successfully logged out.'); + callback(err); + }); + }, + function(callback) { + User.findOne({ username: _User.username }) + .lean() + .exec(function(err, user){ + should.exist(user); + callback(err); + }); + } + ], function (err) { + done(err); + }); }); + after(function(done){ + User.remove().exec(done); + }); }); - afterEach(function(done) { + describe(' > Reset Password > ', function(){ + this.timeout(10000); + beforeEach(function(done){ + var UserObj = new User(_User); + UserObj.save(function(err){ + done(err); + }); + }); + + it('should be able to reset password of a created User with a valid passwordResetToken', function(done) { + var changedPassword = 'password1234'; + var resetPasswordToken; + + async.waterfall([ + function(callback) { + userSession.post('/auth/forgot') + .send({ username: _User.username }) + .expect(200) + .end(function(err) { + callback(err); + }); + }, + function(callback) { + User.findOne({ username: _User.username }) + .lean() + .exec(function(err, user){ + if(err){ + callback(err); + } + callback(null, user.resetPasswordToken); + }); + }, + function(resetPasswordToken, callback) { + userSession.get('/auth/reset/' + resetPasswordToken) + .expect(302) + .end(function(err) { + callback(err, resetPasswordToken); + }); + }, + function(resetPasswordToken, callback) { + userSession.post('/auth/reset/' + resetPasswordToken) + .send({ + newPassword: changedPassword, + verifyPassword: changedPassword + }) + .expect(200) + .end(function(err, res) { + callback(err, resetPasswordToken); + }); + }, + function(resetPasswordToken, callback) { + User.findOne({ username: _User.username }) + .exec(function(err, user){ + should.exist(user); + user.authenticate(changedPassword).should.be.true(); + should.not.exist(user.resetPasswordToken); + + callback(err); + }); + } + ], function (err, result) { + credentials.password = changedPassword; + done(err); + }); + }); + + it('should be not able to reset password of a created User with a invalid passwordResetToken', function(done) { + var changedPassword = 'password4321'; + var resetPasswordToken = 'thisIsNotAValidToken'; + + async.waterfall([ + function(callback) { + userSession.post('/auth/forgot') + .send({ username: credentials.username }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.get('/auth/reset/' + resetPasswordToken) + .expect(400) + .end(function(err) { + callback(err); + }); + }, + function(callback) { + userSession.post('/auth/reset/' + resetPasswordToken) + .send({ + newPassword: changedPassword, + verifyPassword: changedPassword + }) + .expect(400) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + User.findOne({ username: _User.username }) + .exec(function(err, user){ + should.exist(user); + user.authenticate(changedPassword).should.be.false(); + callback(err); + }); + } + ], function (err, result) { + done(err); + }); + }); + + afterEach(function(done){ + User.remove({ username: credentials.username }).exec(done); + }); + }); + + describe(' > User Profile Changes > ', function(){ + var profileSession = new Session(app); + + this.timeout(10000); + beforeEach(function(done){ + var UserObj = new User(_User); + UserObj.save(function(err, user){ + done(err); + }); + }); + + it('should be able to change password when logged in', function(done) { + var changedPassword = 'aVeryBadPassword'; + + async.waterfall([ + function(callback) { + userSession.post('/auth/signin') + .send({ + username: _User.username, + password: _User.password + }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.post('/users/password') + .send({ + currentPassword: _User.password, + newPassword: changedPassword, + verifyPassword: changedPassword + }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + User.findOne({ username: _User.username }) + .exec(function(err, user){ + user.authenticate(changedPassword).should.be.true(); + callback(err); + }); + } + ], function (err) { + done(err); + }); + }); + + it('should be able to update user when logged in', function(done) { + var newUser = {}; + newUser.firstName = 'goodnight'; + newUser.lastName = 'everyone'; + + newUser.email = 'grcg@gcrc.com'; + newUser.username = 'grcg'; + + async.waterfall([ + function(callback) { + userSession.post('/auth/signin') + .send({ + username: _User.username, + password: _User.password + }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.put('/users') + .send(newUser) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + User.findOne({ username: newUser.username }) + .exec(function(err, user){ + user.firstName.should.equal(newUser.firstName); + user.lastName.should.equal(newUser.lastName); + user.email.should.equal(newUser.email); + user.username.should.equal(newUser.username); + callback(err); + }); + } + ], function (err) { + done(err); + }); + }); + + it('should be able to fetch user when logged in', function(done) { + async.waterfall([ + function(callback) { + userSession.post('/auth/signin') + .send({ + username: _User.username, + password: _User.password + }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.get('/users/me') + .expect(200) + .end(function(err, res) { + var user = res.body; + user.firstName.should.equal(_User.firstName); + user.lastName.should.equal(_User.lastName); + user.email.should.equal(_User.email); + user.username.should.equal(_User.username); + callback(err); + }); + } + ], function (err) { + done(err); + }); + }); + + afterEach(function(done){ + userSession.get('/auth/signout') + .end(function(err, res) { + User.remove().exec(done); + }); + }); + }); + + describe(' > User API > ', function(){ + var apiKey; + + this.timeout(10000); + before(function(done){ + var UserObj = new User(_User); + UserObj.save(function(err, user){ + done(err); + }); + }); + + it('should be able to request API Key', function(done) { + async.waterfall([ + function(callback) { + userSession.post('/auth/signin') + .send({ + username: _User.username, + password: _User.password + }) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.get('/auth/genkey') + .expect(200) + .end(function(err, res) { + apiKey = res.body.apiKey; + callback(err); + }); + }, + function(callback) { + userSession.get('/auth/signout') + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + userSession.get('/users/me?apikey=' + apiKey) + .expect(200) + .end(function(err, res) { + var user = res.body; + + user.firstName.should.equal(_User.firstName); + user.lastName.should.equal(_User.lastName); + user.email.should.equal(_User.email); + user.username.should.equal(_User.username); + callback(err); + }); + }, + ], function (err) { + done(err); + }); + }); + + it('should be able to update user with API key', function(done) { + var newUser = {}; + newUser.firstName = 'goodnight'; + newUser.lastName = 'everyone'; + + newUser.email = 'grcg@gcrc.com'; + newUser.username = 'grcg'; + + async.waterfall([ + function(callback) { + userSession.put('/users?apikey=' + apiKey) + .send(newUser) + .expect(200) + .end(function(err, res) { + callback(err); + }); + }, + function(callback) { + User.findOne({ username: newUser.username }) + .exec(function(err, user){ + user.firstName.should.equal(newUser.firstName); + user.lastName.should.equal(newUser.lastName); + user.email.should.equal(newUser.email); + user.username.should.equal(newUser.username); + callback(err); + }); + } + ], function (err) { + done(err); + }); + }); + + after(function(done){ + User.remove().exec(done); + }); + }); + + after(function(done) { User.remove().exec(function () { tmpUser.remove().exec(function(){ userSession.destroy(); diff --git a/app/views/form.server.view.pug b/app/views/form.server.view.pug index 61bfffe8..d66ce5fe 100644 --- a/app/views/form.server.view.pug +++ b/app/views/form.server.view.pug @@ -2,24 +2,24 @@ doctype html html(lang='en', xmlns='http://www.w3.org/1999/xhtml') head title=title - // General META + // General META meta(charset='utf-8') meta(http-equiv='Content-type', content='text/html;charset=UTF-8') meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') meta(name='viewport', content='width=device-width,initial-scale=1,maximum-scale=1') meta(name='apple-mobile-web-app-capable', content='yes') meta(name='apple-mobile-web-app-status-bar-style', content='black') - // Semantic META + // Semantic META meta(name='keywords', content='keywords') meta(name='description', content='description') - // Facebook META + // Facebook META meta(property='og:site_name', content=title) meta(property='og:title', content=title) meta(property='og:description', content='description') meta(property='og:url', content='url') meta(property='og:image', content='/img/brand/logo.png') meta(property='og:type', content='website') - // Twitter META + // Twitter META meta(name='twitter:title', content=title) meta(name='twitter:description', content='description') meta(name='twitter:url', content='url') @@ -32,20 +32,20 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml') width: 100%; height: 100%; z-index: 9999; - background: url('/static/dist/page-loader.gif') 50% 35% no-repeat rgb(249,249,249); + background: url('/static/modules/core/img/loaders/page-loader.gif') 50% 35% no-repeat rgb(249,249,249); background-size: 50px 50px; } - // Fav Icon + // Fav Icon link(href='/static/modules/core/img/brand/favicon.ico', rel='shortcut icon', type='image/x-icon') body(ng-cloak='') .loader section.content section(ui-view='') - //Embedding The User Object signupDisabled, socketPort and socketUrl Boolean + //Embedding The User Object signupDisabled, socketPort and socketUrl Boolean script(type='text/javascript'). var signupDisabled = !{signupDisabled}; - var socketPort = false; + var socketPort = false; var socketUrl = false; var subdomainsDisabled = !{subdomainsDisabled}; @@ -81,12 +81,11 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml') //Socket.io Client Dependency script(src='/static/lib/socket.io-client/dist/socket.io.min.js') - script(src='/static/lib/jquery-ui/jquery-ui.js', type='text/javascript') - //Minified Bower Dependencies - script(src='/static/lib/angular/angular.min.js') - script(src='/static/dist/form-vendor.min.js') - script(src='/static/lib/angular-ui-date/src/date.js', type='text/javascript') + //Bower JS dependencies + each bowerJSFile in bowerFormJSFiles + script(type='text/javascript', src=bowerJSFile) + // end Bower JS dependencies //Application JavaScript Files each jsFile in formJSFiles @@ -94,14 +93,14 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml') // end Application Javascript dependencies if process.env.NODE_ENV === 'development' - //Livereload script rendered + //Livereload script rendered script(async='', type='text/javascript', src='http://#{request.hostname}:35729/livereload.js') - script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install(); - + //script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install(); + if google_analytics_id script window.ga=function(){ga.q.push(arguments)};ga.q=[];ga.l=+new Date;ga('create','{{google_analytics_id}}','auto');ga('send','pageview') - + script(src='https://www.google-analytics.com/analytics.js', async='') script(type="text/javascript"). diff --git a/app/views/index.server.view.pug b/app/views/index.server.view.pug index 10c29935..0bb5ca1f 100644 --- a/app/views/index.server.view.pug +++ b/app/views/index.server.view.pug @@ -3,7 +3,9 @@ extends layout.server.view.pug block content section.content(ui-view='', ng-cloak='') - script(src='/static/lib/file-saver.js/FileSaver.js', type='text/javascript') + link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.snow.min.css') + link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.bubble.min.css') + link(rel='stylesheet', href='/static/lib/jquery-ui/themes/flick/jquery-ui.min.css') //Embedding The User Object script(type='text/javascript'). @@ -12,7 +14,7 @@ block content //Embedding The signupDisabled Boolean script(type='text/javascript'). var signupDisabled = !{signupDisabled}; - var socketPort = false; + var socketPort = false; var socketUrl = false; var subdomainsDisabled = !{subdomainsDisabled}; var locale = "en"; @@ -42,6 +44,10 @@ block content script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/angular-strap/2.3.8/angular-strap.min.js') + script(src='https://cdnjs.cloudflare.com/ajax/libs/quill/1.3.4/quill.min.js') + script(src='https://cdnjs.cloudflare.com/ajax/libs/ng-quill/3.5.2/ng-quill.js') + script(src='https://unpkg.com/quill-placeholder-module@0.2.0/dist/placeholder-module.js') + //Application JavaScript Files each jsFile in jsFiles script(type='text/javascript', src=jsFile) @@ -49,7 +55,7 @@ block content if process.env.NODE_ENV === 'development' script(type='text/javascript', src='http://#{request.hostname}:35729/livereload.js') - + script(src='https://cdn.ravenjs.com/2.3.0/angular/raven.min.js') - - script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install(); \ No newline at end of file + + script Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install(); diff --git a/app/views/layout.server.view.pug b/app/views/layout.server.view.pug index 44bf1b9d..8f870023 100644 --- a/app/views/layout.server.view.pug +++ b/app/views/layout.server.view.pug @@ -31,8 +31,8 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml') // Fav Icon link(href='/static/modules/core/img/brand/favicon.ico', rel='shortcut icon', type='image/x-icon') - link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css') - link(rel='stylesheet', href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css', integrity='sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u', crossorigin='anonymous') + link(rel='stylesheet', href='/static/lib/font-awesome/css/font-awesome.min.css') + link(rel='stylesheet', href='/static/lib/bootstrap/dist/css/bootstrap.min.css') link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,900') //Bower CSS dependencies @@ -40,7 +40,6 @@ html(lang='en', xmlns='http://www.w3.org/1999/xhtml') link(rel='stylesheet', href=bowerCssFile) link(rel='stylesheet', href='/static/lib/angular-input-stars/angular-input-stars.css') link(rel='stylesheet', href='/static/lib/jquery-ui/themes/flick/jquery-ui.css') - link(rel='stylesheet', href='/static/modules/core/css/github-fork-ribbon.css') // end Bower CSS dependencies //Application CSS Files diff --git a/bower.json b/bower.json index d351e104..90c1f70f 100755 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "tellform", "description": "Opensource alternative to TypeForm", - "version": "2.0.0", + "version": "2.2.0", "homepage": "https://github.com/tellform/tellform", "authors": [ "David Baldwynn (http://baldwynn.me)" @@ -11,30 +11,25 @@ "appPath": "public/modules", "dependencies": { "bootstrap": "^3.3.7", - "angular-resource": "~1.4.7", + "angular-resource": "~1.7.8", "angular-cache-buster": "~0.4.3", - "angular-mocks": "~1.4.7", "angular-bootstrap": "~0.14.3", "angular-ui-utils": "~3.0.0", - "angular-ui-router": "~0.2.11", "ng-file-upload": "^12.0.4", "angular-raven": "~0.5.11", "angular-ui-date": "~0.0.11", "lodash": "~3.10.0", "angular-ui-sortable": "~0.13.4", - "angular-permission": "~1.1.1", "file-saver.js": "~1.20150507.2", "angular-bootstrap-colorpicker": "~3.0.19", - "angular-ui-router-tabs": "~1.7.0", "angular-scroll": "^1.0.0", - "angular-sanitize": "1.4.14", + "angular-sanitize": "^1.7.8", "v-button": "^1.1.1", - "angular-input-stars": "https://github.com/tellform/angular-input-stars.git#master", + "angular-input-stars": "^1.8.0", "raven-js": "^3.0.4", "tableExport.jquery.plugin": "^1.5.1", "js-yaml": "^3.6.1", - "angular-ui-select": "https://github.com/tellform/ui-select.git#compiled", - "angular-translate": "~2.11.0", + "angular-translate": "^2.18.1", "ng-translate": "*", "deep-diff": "^0.3.4", "jsep": "0.3.1", @@ -42,17 +37,14 @@ "mobile-detect": "^1.3.3", "socket.io-client": "^1.7.2", "css-toggle-switch": "^4.0.2", - "angular-strap": "^2.3.12" - }, - "resolutions": { - "angular-bootstrap": "^0.14.0", - "angular": "1.4.14", - "angular-ui-select": "compiled", - "jspdf": "~1.0.178", - "angular-sanitize": "1.4.14", - "angular-ui-sortable": "^0.17.1", - "angular-ui-date": "~0.0.11", - "angular-input-stars-directive": "master" + "angular-strap": "^2.3.12", + "angular-ui-select": "^0.19.8", + "angular-bootstrap-switch": "^0.5.2", + "jquery": "^3.2.1", + "ng-quill": "^4.5.0", + "angular-ui-router": "^1.0.11", + "angular-permission": "^5.3.2", + "angular-mocks": "^1.7.8" }, "overrides": { "BOWER-PACKAGE": { diff --git a/config/config.js b/config/config.js index a39c29fb..3dfca31d 100755 --- a/config/config.js +++ b/config/config.js @@ -62,6 +62,12 @@ module.exports.removeRootDir = function(files, removeRoot, addRoot) { /** * Get the app's bower dependencies */ +module.exports.getBowerFormJSAssets = function() { + if(process.env.NODE_ENV === 'production'){ + return ['/static/lib/angular/angular.min.js', '/static/dist/vendor.min.js', '/static/lib/angular-ui-date/src/date.js']; + } + return this.removeRootDir(minBowerFiles('**/**.js'), 'public/', 'static/'); +}; module.exports.getBowerJSAssets = function() { return this.removeRootDir(minBowerFiles('**/**.js'), 'public/', 'static/'); }; diff --git a/config/env/all.js b/config/env/all.js index 35fe1db4..9dd2fb16 100755 --- a/config/env/all.js +++ b/config/env/all.js @@ -8,20 +8,15 @@ module.exports = { keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs' }, db: { - uri: process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://'+ (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean', - options: { - user: '', - pass: '' - } + uri: process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://'+ (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean' }, - - - admin:{ + admin: { email: process.env.ADMIN_EMAIL || 'admin@admin.com', username: process.env.ADMIN_USERNAME || 'root', password: process.env.ADMIN_PASSWORD || 'root', + roles: ['user', 'admin'] }, - + redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379', port: process.env.PORT || 3000, @@ -103,18 +98,18 @@ module.exports = { 'public/config.js', 'public/application.js', 'public/dist/populate_template_cache.js', + 'public/dist/form_populate_template_cache.js', 'public/modules/*/*.js', 'public/modules/*/*/*.js', 'public/modules/*/*/*/*.js', 'public/modules/*/*/*/*/*.js', + '!public/modules/*/tests/**/*.js', 'public/form_modules/forms/*.js', 'public/form_modules/forms/directives/*.js', 'public/form_modules/forms/base/config/*.js', 'public/form_modules/forms/base/config/*/*.js', 'public/form_modules/forms/base/**/*.js', 'public/form_modules/forms/base/*/*.js', - '!public/modules/*/tests/**/*.js', - '!public/modules/*/tests/*.js' ], form_js: [ 'public/form-config.js', @@ -123,8 +118,7 @@ module.exports = { 'public/form_modules/forms/*.js', 'public/form_modules/forms/*/*.js', 'public/form_modules/forms/*/*/*.js', - 'public/form_modules/forms/*/*/*/*.js', - 'public/form_modules/forms/**.js', + 'public/form_modules/forms/**/*.js', '!public/form_modules/**/tests/**/*.js' ], views: [ diff --git a/config/env/development.js b/config/env/development.js index a62673f6..bd19d48d 100755 --- a/config/env/development.js +++ b/config/env/development.js @@ -4,11 +4,7 @@ module.exports = { baseUrl: process.env.BASE_URL || 'http://localhost:5000', port: process.env.PORT || 5000, db: { - uri: process.env.MONGODB_URI || 'mongodb://'+( process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') +'/mean', - options: { - user: '', - pass: '' - } + uri: process.env.MONGODB_URI || 'mongodb://'+( process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') +'/mean' }, log: { // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' diff --git a/config/env/production.js b/config/env/production.js index 0f8a72a5..98a93f27 100755 --- a/config/env/production.js +++ b/config/env/production.js @@ -3,7 +3,7 @@ module.exports = { baseUrl: process.env.BASE_URL || process.env.HEROKU_APP_NAME + '.herokuapp.com' || 'tellform.com', db: { - uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean', + uri: process.env.MONGODB_URI || process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR || '127.0.0.1') + '/mean' }, port: process.env.PORT || 5000, socketUrl: process.env.SOCKET_URL || 'ws.tellform.com', @@ -30,6 +30,6 @@ module.exports = { assets: { css: ['public/dist/application.min.css'], js: ['public/dist/application.min.js', 'public/dist/populate_template_cache.js'], - form_js: ['public/dist/form-application.min.js', 'public/dist/form_populate_template_cache.js', 'public/dist/form-vendor.min.js'] + form_js: ['public/dist/form-application.min.js', 'public/dist/form_populate_template_cache.js'] } }; diff --git a/config/env/secure.js b/config/env/secure.js deleted file mode 100755 index bc99aeb5..00000000 --- a/config/env/secure.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -module.exports = { - baseUrl: 'https://forms.polydaic.com', - port: 8443, - db: { - uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://127.0.0.1/mean', - options: { - user: '', - pass: '' - } - }, - log: { - // Can specify one of 'combined', 'common', 'dev', 'short', 'tiny' - format: 'combined', - // Stream defaults to process.stdout - // Uncomment to enable logging to a log on the file system - options: { - stream: 'access.log' - } - }, - - sessionCookie: { - path: '/', - httpOnly: false, - // If secure is set to true then it will cause the cookie to be set - // only when SSL-enabled (HTTPS) is used, and otherwise it won't - // set a cookie. 'true' is recommended yet it requires the above - // mentioned pre-requisite. - secure: true, - // Only set the maxAge to null if the cookie shouldn't be expired - // at all. The cookie will expunge when the browser is closed. - maxAge: 7200, - // To set the cookie in a specific domain uncomment the following - // setting: - domain: process.env.BASE_URL || 'localhost:3000' - }, - assets: { - css: 'public/dist/application.min.css', - js: 'public/dist/application.min.js' - }, - mailer: { - from: process.env.MAILER_FROM || '', - options: process.env.MAILER_SMTP_HOST ? { //Uses custom SMTP if MAILER_SMTP_HOST is set - host: process.env.MAILER_SMTP_HOST || '', - port: process.env.MAILER_SMTP_PORT || 587, - secure: (process.env.MAILER_SMTP_SECURE === 'TRUE'), - auth: { - user: process.env.MAILER_EMAIL_ID || '', - pass: process.env.MAILER_PASSWORD || '' - } - } : { - service: process.env.MAILER_SERVICE_PROVIDER || '', - auth: { - user: process.env.MAILER_EMAIL_ID || '', - pass: process.env.MAILER_PASSWORD || '' - } - } - } -}; diff --git a/config/env/test.js b/config/env/test.js index d3a02f24..651685f1 100755 --- a/config/env/test.js +++ b/config/env/test.js @@ -3,11 +3,7 @@ module.exports = { baseUrl: '127.0.0.1:3001', db: { - uri: 'mongodb://localhost/mean-test', - options: { - user: '', - pass: '' - } + uri: 'mongodb://localhost/mean-test' }, port: 3001, log: { @@ -19,6 +15,7 @@ module.exports = { //stream: 'access.log' } }, + subdomainsDisabled: true, app: { title: 'TellForm Test' }, diff --git a/config/express.js b/config/express.js index d8441b79..26f71bf7 100755 --- a/config/express.js +++ b/config/express.js @@ -39,8 +39,9 @@ var configureSocketIO = function (app, db) { var supportedLanguages = ['en', 'de', 'fr', 'it', 'es']; function containsAnySupportedLanguages(preferredLanguages){ - for (var i = 0; i < preferredLanguages.length; i++) { - var currIndex = supportedLanguages.indexOf(preferredLanguages[i]); + var i, currIndex; + for (i = 0; i < preferredLanguages.length; i++) { + currIndex = supportedLanguages.indexOf(preferredLanguages[i]); if (currIndex > -1) { return supportedLanguages[currIndex]; } @@ -75,8 +76,9 @@ module.exports = function(db) { if(config.socketUrl){ app.locals.socketUrl = config.socketUrl; - } + } + app.locals.bowerFormJSFiles = config.getBowerFormJSAssets(); app.locals.bowerJSFiles = config.getBowerJSAssets(); app.locals.bowerCssFiles = config.getBowerCSSAssets(); app.locals.bowerOtherFiles = config.getBowerOtherAssets(); @@ -91,7 +93,7 @@ module.exports = function(db) { var User = mongoose.model('User'); var subdomainPath = '/subdomain/'; var subdomains = req.subdomains; - + if (subdomains.slice(0, 4).join('.') + '' === '1.0.0.127') { subdomains = subdomains.slice(4); } @@ -100,7 +102,7 @@ module.exports = function(db) { if (!subdomains.length) { return next(); } - + urlPath = url.parse(req.url).path.split('/'); if (urlPath.indexOf('static') > -1) { urlPath.splice(1, 1); @@ -244,7 +246,6 @@ module.exports = function(db) { // Setting the app router and static folder app.use('/static', express.static(path.resolve('./public'))); - app.use('/uploads', express.static(path.resolve('./uploads'))); // CookieParser should be above session app.use(cookieParser()); @@ -255,7 +256,7 @@ module.exports = function(db) { resave: true, secret: config.sessionSecret, store: new MongoStore({ - mongooseConnection: db.connection, + mongooseConnection: mongoose.connection, collection: config.sessionCollection }), cookie: config.sessionCookie, @@ -270,6 +271,7 @@ module.exports = function(db) { //Visitor Language Detection app.use(function(req, res, next) { var acceptLanguage = req.headers['accept-language']; + var languages, supportedLanguage; if(acceptLanguage){ @@ -279,13 +281,12 @@ module.exports = function(db) { if(!req.user && supportedLanguage !== null){ var currLanguage = res.cookie('userLang'); - if(currLanguage && currLanguage !== supportedLanguage || !currLanguage){ res.clearCookie('userLang'); res.cookie('userLang', supportedLanguage, { maxAge: 90000, httpOnly: true }); + } else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies.userLang !== req.user.language) ){ + res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true }); } - } else if(req.user && (!req.cookies.hasOwnProperty('userLang') || req.cookies['userLang'] !== req.user.language) ){ - res.cookie('userLang', req.user.language, { maxAge: 90000, httpOnly: true }); } next(); }); @@ -348,22 +349,6 @@ module.exports = function(db) { }); }); - if (process.env.NODE_ENV === 'secure') { - // Load SSL key and certificate - var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8'); - var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8'); - - // Create HTTPS Server - var httpsServer = https.createServer({ - key: privateKey, - cert: certificate - }, app); - - // Return HTTPS server instance - return httpsServer; - } - - app = configureSocketIO(app, db); // Return Express server instance diff --git a/config/locales/de.json b/config/locales/de.json index f3095601..0be3be9e 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -4,18 +4,18 @@ "404_BODY": "%s ist kein gültiger Pfad.", "500_BODY": "Ein unerwarteter Fehler scheint aufgetreten zu sein, warum nicht versuchen, Ihre Seite zu aktualisieren oder Sie können uns kontaktieren, wenn das Problem weiterhin besteht.", "EMAIL_GREETING": "Hallo da!", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Willkommen bei TellForm! Hier ist ein spezieller Link um deinen neuen Account zu aktivieren:", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Willkommen bei OhMyForm! Hier ist ein spezieller Link um deinen neuen Account zu aktivieren:", "VERIFICATION_EMAIL_LINK_TEXT": "Mein Konto aktivieren", "VERIFICATION_EMAIL_PARAGRAPH_2": "Vielen Dank für die Nutzung unserer Dienste! Wenn Sie Fragen oder Anregungen haben, senden Sie uns bitte eine E-Mail an", - "VERIFICATION_EMAIL_SUBJECT": "Aktiviere dein neues TellForm-Konto!", + "VERIFICATION_EMAIL_SUBJECT": "Aktiviere dein neues OhMyForm-Konto!", "VERIFICATION_EMAIL_TEXT": "Bitte bestätigen Sie Ihren Account, indem Sie auf den folgenden Link klicken oder ihn in Ihren Browser kopieren und einfügen: $ {URL}", - "EMAIL_SIGNATURE": "- Das TellForm-Team", + "EMAIL_SIGNATURE": "- Das OhMyForm-Team", "WELCOME_EMAIL_PARAGRAPH_1": "Wir möchten Sie als unser neustes Mitglied begrüßen!", - "WELCOME_EMAIL_PARAGRAPH_2": "Wir wünschen Ihnen viel Spaß mit TellForm! Wenn Sie Probleme haben, senden Sie uns bitte eine E-Mail an", + "WELCOME_EMAIL_PARAGRAPH_2": "Wir wünschen Ihnen viel Spaß mit OhMyForm! Wenn Sie Probleme haben, senden Sie uns bitte eine E-Mail an", "WELCOME_EMAIL_SUBJECT": "Willkommen bei %s!", "WELCOME_EMAIL_TEXT": "Ihr Konto wurde erfolgreich verifiziert.", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "Dies ist eine Höflichkeitsnachricht, um zu bestätigen, dass Ihr Passwort gerade geändert wurde.", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_1": "Hier ist ein spezieller Link, mit dem Sie Ihr Passwort zurücksetzen können: Bitte beachten Sie, dass es innerhalb einer Stunde zu Ihrem Schutz abläuft:", "RESET_PASSWORD_REQUEST_EMAIL_LINK_TEXT": "Passwort zurücksetzen", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_2": "Falls Sie dies nicht gewünscht haben, ignorieren Sie bitte diese E-Mail und Ihr Passwort bleibt unverändert." -} \ No newline at end of file +} diff --git a/config/locales/en.json b/config/locales/en.json index 668622a3..20c478be 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -4,14 +4,14 @@ "404_BODY": "%s is not a valid path", "500_BODY": "An unexpected error seems to have occured. Why not try refreshing your page? Or you can contact us if the problem persists.", "EMAIL_GREETING": "Hello there!", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Welcome to TellForm! Here is a special link to activate your new account:", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Welcome to OhMyForm! Here is a special link to activate your new account:", "VERIFICATION_EMAIL_LINK_TEXT": "Activate my account", "VERIFICATION_EMAIL_PARAGRAPH_2": "Thanks so much for using our services! If you have any questions, or suggestions, please feel free to email us here at", - "VERIFICATION_EMAIL_SUBJECT": "Activate your new TellForm account!", + "VERIFICATION_EMAIL_SUBJECT": "Activate your new OhMyForm account!", "VERIFICATION_EMAIL_TEXT": "Please verify your account by clicking the following link, or by copying and pasting it into your browser: ${URL}", - "EMAIL_SIGNATURE": "- The TellForm team", + "EMAIL_SIGNATURE": "- The OhMyForm team", "WELCOME_EMAIL_PARAGRAPH_1": "We would like to welcome you as our newest member!", - "WELCOME_EMAIL_PARAGRAPH_2": "We hope you enjoy using TellForm! If you have any trouble please feel free to email us here at", + "WELCOME_EMAIL_PARAGRAPH_2": "We hope you enjoy using OhMyForm! If you have any trouble please feel free to email us here at", "WELCOME_EMAIL_SUBJECT": "Welcome to %s!", "WELCOME_EMAIL_TEXT": "Your account has been successfully verified.", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "This is a courtesy message to confirm that your password was just changed.", @@ -19,4 +19,4 @@ "RESET_PASSWORD_REQUEST_EMAIL_LINK_TEXT": "Reset Your Password", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_2": "If you did not request this, please ignore this email and your password will remain unchanged.", "RESET_PASSWORD_CONFIRMATION_EMAIL_BODY_1": "RESET_PASSWORD_CONFIRMATION_EMAIL_BODY_1" -} \ No newline at end of file +} diff --git a/config/locales/es.json b/config/locales/es.json index 4a6ebce9..51231f7b 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -4,18 +4,18 @@ "404_BODY": "%s no es una ruta válida", "500_BODY": "Parece que se produjo un error inesperado. ¿Por qué no intenta actualizar su página? O puede contactarnos si el problema persiste", "EMAIL_GREETING": "¡Hola!", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Bienvenido a TellForm. Aquí hay un enlace especial para activar su nueva cuenta:", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Bienvenido a OhMyForm. Aquí hay un enlace especial para activar su nueva cuenta:", "VERIFICATION_EMAIL_LINK_TEXT": "Activar mi cuenta", "VERIFICATION_EMAIL_PARAGRAPH_2": "¡Muchas gracias por utilizar nuestros servicios! Si tiene alguna pregunta o sugerencia, no dude en enviarnos un correo electrónico aquí", - "VERIFICATION_EMAIL_SUBJECT": "¡Active su nueva cuenta TellForm!", + "VERIFICATION_EMAIL_SUBJECT": "¡Active su nueva cuenta OhMyForm!", "VERIFICATION_EMAIL_TEXT": "Verifique su cuenta haciendo clic en el siguiente enlace, o copiándolo y pegándolo en su navegador: $ {URL}", - "EMAIL_SIGNATURE": "- El equipo de TellForm", + "EMAIL_SIGNATURE": "- El equipo de OhMyForm", "WELCOME_EMAIL_PARAGRAPH_1": "¡Nos gustaría darle la bienvenida como nuestro miembro más nuevo!", - "WELCOME_EMAIL_PARAGRAPH_2": "Esperamos que disfrute utilizando TellForm. Si tiene algún problema, no dude en enviarnos un correo electrónico aquí", + "WELCOME_EMAIL_PARAGRAPH_2": "Esperamos que disfrute utilizando OhMyForm. Si tiene algún problema, no dude en enviarnos un correo electrónico aquí", "WELCOME_EMAIL_SUBJECT": "¡Bienvenido a %s!", "WELCOME_EMAIL_TEXT": "Su cuenta ha sido verificada con éxito", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "Este es un mensaje de cortesía para confirmar que su contraseña acaba de cambiarse", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_1": "Aquí hay un enlace especial que le permitirá restablecer su contraseña. Tenga en cuenta que caducará en una hora para su protección:", "RESET_PASSWORD_REQUEST_EMAIL_LINK_TEXT": "Restablecer su contraseña", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_2": "Si no lo solicitó, ignore este correo electrónico y su contraseña no cambiará". -} \ No newline at end of file +} diff --git a/config/locales/fr.json b/config/locales/fr.json index c414c39e..79805849 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -4,14 +4,14 @@ "404_BODY": "%s n'est pas un chemin valide.", "500_BODY": "Une erreur inattendue semble s'être produite, pourquoi ne pas essayer d'actualiser votre page ? Ou vous pouvez nous contacter si le problème persiste.", "EMAIL_GREETING": "Bonjour !", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Bienvenue sur TellForm ! Voici un lien spécial pour activer votre nouveau compte : ", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Bienvenue sur OhMyForm ! Voici un lien spécial pour activer votre nouveau compte : ", "VERIFICATION_EMAIL_LINK_TEXT": "Activer mon compte", "VERIFICATION_EMAIL_PARAGRAPH_2": "Merci infiniment d'utiliser nos services ! Si vous avez des questions ou des suggestions, n'hésitez pas à nous envoyer un courriel ici", - "VERIFICATION_EMAIL_SUBJECT": "Activer votre nouveau compte TellForm !", + "VERIFICATION_EMAIL_SUBJECT": "Activer votre nouveau compte OhMyForm !", "VERIFICATION_EMAIL_TEXT": "Merci de vérifier votre compte en cliquant sur le lien suivant, ou en le copiant dans votre navigateur web : ${URL}", - "EMAIL_SIGNATURE": "- L'équipe TellForm", + "EMAIL_SIGNATURE": "- L'équipe OhMyForm", "WELCOME_EMAIL_PARAGRAPH_1": "Nous aimerions vous accueillir en tant que nouveau membre !", - "WELCOME_EMAIL_PARAGRAPH_2": "Nous espérons que vous apprécierez l'utilisation de TellForm ! Si vous avez des problèmes, n'hésitez pas à nous envoyer un e-mail ici", + "WELCOME_EMAIL_PARAGRAPH_2": "Nous espérons que vous apprécierez l'utilisation de OhMyForm ! Si vous avez des problèmes, n'hésitez pas à nous envoyer un e-mail ici", "WELCOME_EMAIL_SUBJECT": "Bienvenue dans %s!", "WELCOME_EMAIL_TEXT": "Votre compte a été vérifié avec succès.", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "Ceci est un message de courtoisie pour confirmer que votre mot de passe a été modifié.", diff --git a/config/locales/it.json b/config/locales/it.json index ea0140a3..7546f079 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -4,18 +4,18 @@ "404_BODY": "%s non è un percorso valido", "500_BODY": "Si è verificato un errore imprevisto: perché non provare a rinfrescare la tua pagina oppure puoi contattarci se il problema persiste", "EMAIL_GREETING": "Ciao!", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Benvenuti a TellForm! Ecco un collegamento speciale per attivare il tuo nuovo account:", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Benvenuti a OhMyForm! Ecco un collegamento speciale per attivare il tuo nuovo account:", "VERIFICATION_EMAIL_LINK_TEXT": "Attiva il mio account", "VERIFICATION_EMAIL_PARAGRAPH_2": "Grazie mille per l'utilizzo dei nostri servizi! Se hai domande o suggerimenti, non esitate a contattarci via", - "VERIFICATION_EMAIL_SUBJECT": "Attiva il tuo nuovo account TellForm", + "VERIFICATION_EMAIL_SUBJECT": "Attiva il tuo nuovo account OhMyForm", "VERIFICATION_EMAIL_TEXT": "Verifica il tuo account facendo clic sul seguente collegamento o copiandolo e incollandolo nel tuo browser: $ {URL}", - "EMAIL_SIGNATURE": "- Il team TellForm", + "EMAIL_SIGNATURE": "- Il team OhMyForm", "WELCOME_EMAIL_PARAGRAPH_1": "Vorremmo darVi il benvenuto come il nostro nuovo membro!", - "WELCOME_EMAIL_PARAGRAPH_2": "Speriamo che ti piace usare TellForm! Se hai problemi, non esitate a contattarci via", + "WELCOME_EMAIL_PARAGRAPH_2": "Speriamo che ti piace usare OhMyForm! Se hai problemi, non esitate a contattarci via", "WELCOME_EMAIL_SUBJECT": "Benvenuto a %s!", "WELCOME_EMAIL_TEXT": "Il tuo account è stato verificato correttamente.", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "Si tratta di un messaggio di cortesia per confermare che la password è stata appena modificata". "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_1": "Ecco un collegamento speciale che ti permetterà di reimpostare la tua password. Si prega di notare che scadrà in un'ora per la protezione:", "RESET_PASSWORD_REQUEST_EMAIL_LINK_TEXT": "Ripristina la tua password", "RESET_PASSWORD_REQUEST_EMAIL_PARAGRAPH_2": "Se non l'hai richiesta, ignora questa email e la tua password rimane invariata." -} \ No newline at end of file +} diff --git a/config/locales/sv.json b/config/locales/sv.json index 63c67daf..cae6508d 100644 --- a/config/locales/sv.json +++ b/config/locales/sv.json @@ -4,14 +4,14 @@ "404_BODY": "%s är inte en giltig sökväg", "500_BODY": "Ett oväntat fel verkar ha inträffat. Kan du prova med att uppdatera sidan? Eller kan du kontakta oss om problemet återuppstår igen?", "EMAIL_GREETING": "Hej där!", - "VERIFICATION_EMAIL_PARAGRAPH_1": "Välkommen till TellForm! Här är en speciell länk till dig för att aktivera ditt nya konto:", + "VERIFICATION_EMAIL_PARAGRAPH_1": "Välkommen till OhMyForm! Här är en speciell länk till dig för att aktivera ditt nya konto:", "VERIFICATION_EMAIL_LINK_TEXT": "Aktivera mitt konto", "VERIFICATION_EMAIL_PARAGRAPH_2": "Tack så mycket för att du använder våra tjänster! Om du har några frågor eller förslag är du varmt välkommen att e-posta oss här på", - "VERIFICATION_EMAIL_SUBJECT": "Aktivera ditt nya TellForm-konto!", + "VERIFICATION_EMAIL_SUBJECT": "Aktivera ditt nya OhMyForm-konto!", "VERIFICATION_EMAIL_TEXT": "Vänligen verifiera ditt konto genom att klicka på den följande länken, eller genom att kopiera och klistra in den i din webbläsare: ${URL}", - "EMAIL_SIGNATURE": "- TellForm-gruppen", + "EMAIL_SIGNATURE": "- OhMyForm-gruppen", "WELCOME_EMAIL_PARAGRAPH_1": "Vi skulle vilja välkomna dig som vår nyaste medlem!", - "WELCOME_EMAIL_PARAGRAPH_2": "Vi hoppas att du gillar att använda TellForm! Om du stöter på några problem är du varmt välkommen att e-posta oss här på", + "WELCOME_EMAIL_PARAGRAPH_2": "Vi hoppas att du gillar att använda OhMyForm! Om du stöter på några problem är du varmt välkommen att e-posta oss här på", "WELCOME_EMAIL_SUBJECT": "Välkommen till %s!", "WELCOME_EMAIL_TEXT": "Ditt konto har framgångsrikt blivit verifierat.", "RESET_PASSWORD_CONFIRMATION_EMAIL_PARAGRAPH_1": "Detta är ett artigt meddelande för att bekräfta att ditt lösenord just har ändrats.", diff --git a/config/logger.js b/config/logger.js index 33842f41..d4fe9f40 100755 --- a/config/logger.js +++ b/config/logger.js @@ -63,7 +63,6 @@ logger.setupFileLogger = function setupFileLogger() { return false; } - }; /** @@ -76,7 +75,7 @@ logger.getLogOptions = function getLogOptions() { var _config = _.clone(config, true); var configFileLogger = _config.log.fileLogger; - if (!_.has(_config, 'log.fileLogger.directoryPath') || !_.has(_config, 'log.fileLogger.fileName')) { + if (process.env.NODE_ENV !== 'test' && !_.has(_config, 'log.fileLogger.directoryPath') || !_.has(_config, 'log.fileLogger.fileName')) { console.log('unable to find logging file configuration'); return false; } @@ -97,7 +96,6 @@ logger.getLogOptions = function getLogOptions() { handleExceptions: true, humanReadableUnhandledException: true }; - }; /** diff --git a/config/passport_helpers.js b/config/passport_helpers.js index cb861289..9d5ce00a 100644 --- a/config/passport_helpers.js +++ b/config/passport_helpers.js @@ -6,14 +6,23 @@ module.exports.isAuthenticatedOrApiKey = function isAuthenticated(req, res, next if (req.isAuthenticated()) { return next(); } + // Try authenticate with API KEY if (req.headers.apikey || req.query.apikey || req.body.apikey) { - passport.authenticate('localapikey', function (err, user, info) { - if (err) - return res.sendStatus(500); + if(!req.body.apikey && req.headers.apikey){ + req.body.apikey = req.headers.apikey; + } else if(!req.query.apikey && req.headers.apikey){ + req.query.apikey = req.headers.apikey; + } - if (!user) + passport.authenticate('localapikey', function (err, user, info) { + if (err) { + return res.status(500).send('Internal Server Error with API. Sorry about that!'); + } + + if (!user) { return res.status(401).send(info.message || ''); + } req.login(user, function(loginErr) { if (loginErr) return res.sendStatus(500); @@ -28,23 +37,3 @@ module.exports.isAuthenticatedOrApiKey = function isAuthenticated(req, res, next } }; - -module.exports.hasRole = function hasRole(roleRequired) { - if (!roleRequired) { - throw new Error('Required role needs to be set'); - } - - return function(req, res, next) { - return module.exports.isAuthenticated(req, res, function() { - if (req.user && req.user.roles && req.user.roles.indexOf(roleRequired) !== -1){ - return next(); - } - return res.sendStatus(403); - }); - }; -}; - -module.exports.hasAdminRole = function hasAdminRole() { - return module.exports.hasRole('admin'); -}; - diff --git a/config/strategies/apikey.js b/config/strategies/apikey.js index c4fdf389..5677b730 100644 --- a/config/strategies/apikey.js +++ b/config/strategies/apikey.js @@ -11,13 +11,15 @@ module.exports = function() { return User.findOne({ 'apiKey': apiKey }, function(err, user) { - if (err) + if (err) { return done(err); + } - if (!user) + if (!user){ return done(null, false, { message: 'Unknown API Key' }); + } return done(null, user); }); diff --git a/design/screenshots/analytics.png b/design/screenshots/analytics.png deleted file mode 100644 index 6e828e82..00000000 Binary files a/design/screenshots/analytics.png and /dev/null differ diff --git a/design/screenshots/collapsed.png b/design/screenshots/collapsed.png deleted file mode 100644 index 88d28fa7..00000000 Binary files a/design/screenshots/collapsed.png and /dev/null differ diff --git a/design/screenshots/tellform_rating.png b/design/screenshots/tellform_rating.png deleted file mode 100644 index 24780599..00000000 Binary files a/design/screenshots/tellform_rating.png and /dev/null differ diff --git a/design/screenshots/tellform_screenshot1.png b/design/screenshots/tellform_screenshot1.png deleted file mode 100644 index cec6b75e..00000000 Binary files a/design/screenshots/tellform_screenshot1.png and /dev/null differ diff --git a/design/tellform_mascot.png b/design/tellform_mascot.png deleted file mode 100644 index 030bcd57..00000000 Binary files a/design/tellform_mascot.png and /dev/null differ diff --git a/dev_entrypoint.sh b/dev_entrypoint.sh deleted file mode 100644 index 97ae1436..00000000 --- a/dev_entrypoint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -## TODO: Reconsider this as I think that it's no longer relevant. -line=$(head -n 1 /etc/hosts) -echo "$line tellform.dev $(hostname)" >> /etc/hosts - -# Restart sendmail -service sendmail restart - -# Run Server -npm start diff --git a/dns_masq_setup_osx.md b/dns_masq_setup_osx.md deleted file mode 100644 index e3e19ce5..00000000 --- a/dns_masq_setup_osx.md +++ /dev/null @@ -1,41 +0,0 @@ - -# wildcard DNS in localhost development -- install [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) -``` -$ brew install dnsmasq - ... -$ cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf -``` -- edit `/usr/local/etc/dnsmasq.conf` -``` -address=/dev/127.0.0.1 -``` -- start **dnsmasq** -``` -$ sudo brew services start dnsmasq -``` -- any time we change `dnsmasq.conf` we have to re-start **dnsmasq**: -``` -$ sudo launchctl stop homebrew.mxcl.dnsmasq -$ sudo launchctl start homebrew.mxcl.dnsmasq -``` -- For OS X to _resolve_ requests from `*.dev` to **localhost** we need to add a _resolver_: -``` -$ sudo mkdir /etc/resolver -$ sudo touch /etc/resolver/dev -``` -- edit `/etc/resolver/dev` -``` -nameserver 127.0.0.1 -``` -- re-start the computer to enable the _resolver_ - -=== -**REFERENCES** - -- [Using Dnsmasq for local development on OS X - Passing Curiosity](https://passingcuriosity.com/2013/dnsmasq-dev-osx/) -- [Using Dnsmasq Configure Wildcard DNS Record on Mac | Ri Xu Online](https://xuri.me/2014/12/13/using-dnsmasq-configure-wildcard-dns-record-on-mac.html) -- [unix - In my /etc/hosts/ file on Linux/OSX, how do I do a wildcard subdomain? - Server Fault](http://serverfault.com/questions/118378/in-my-etc-hosts-file-on-linux-osx-how-do-i-do-a-wildcard-subdomain) -- [hostname - Wildcard in /etc/hosts file - Unix & Linux Stack Exchange](http://unix.stackexchange.com/questions/3352/wildcard-in-etc-hosts-file) -- [Mac OS Lion - Wildcard subdomain virtual host - Stack Overflow](http://stackoverflow.com/questions/9562059/mac-os-lion-wildcard-subdomain-virtual-host) -- [How to put wildcard entry into /etc/hosts? - Stack Overflow](http://stackoverflow.com/questions/20446930/how-to-put-wildcard-entry-into-etc-hosts) diff --git a/docker/compose/docker-compose.yml b/docker-compose.yml similarity index 65% rename from docker/compose/docker-compose.yml rename to docker-compose.yml index 39057c09..1782316f 100644 --- a/docker/compose/docker-compose.yml +++ b/docker-compose.yml @@ -9,14 +9,19 @@ services: - "./data/mongo:/data" tellform: image: ohmyform/ohmyform + #build: . + #volumes: + # - ".:/opt/app" environment: CREATE_ADMIN: "TRUE" + SOCKET_URL: 'localhost:5000' SOCKET_PORT: "5000" SOCKET_PORT_EXTERN_VISIBLE: "TRUE" MONGODB_URI: mongodb://mongo/tellform REDIS_URL: redis://redis MAILER_SMTP_HOST: mail MAILER_SMTP_PORT: 1025 + # command: grunt dev # override command to have livereloading on file change links: - mongo - redis @@ -30,3 +35,13 @@ services: image: mailhog/mailhog ports: - "5050:8025" + mongoexpress: + image: mongo-express + environment: + ME_CONFIG_MONGODB_SERVER: mongo + ports: + - "5051:8081" + links: + - mongo + depends_on: + - mongo diff --git a/ecosystem.json b/ecosystem.json deleted file mode 100644 index 6f922fbc..00000000 --- a/ecosystem.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "apps" : [{ - "name" : "tellform", - "script" : "server.js", - "instances": "max", - "exec_mode": "cluster", - "max_memory_restart" : "200M" - }], - "deploy" : { - "stage": { - "user": "polydaic", - "host": "159.203.42.158", - "ref": "origin/stage", - "repo": "git@github.com:tellform/tellform.git", - "path": "/opt/deploy", - "post-deploy": "npm install && pm2 startOrGracefulReload ecosystem.json --env production", - "env": { - "NODE_ENV": "production", - "BASE_URL": "stage.tellform.com" - } - }, - "prod": { - "user": "polydaic", - "host": "159.203.33.182", - "ref": "origin/master", - "repo": "git@github.com:tellform/tellform.git", - "path": "/opt/deploy", - "post-deploy": "npm install && pm2 startOrGracefulReload ecosystem.json --env production", - "env": { - "NODE_ENV": "production", - "BASE_URL": "admin.tellform.com" - } - } - } -} diff --git a/gruntfile.js b/gruntfile.js index 0b5539b9..f50e0223 100755 --- a/gruntfile.js +++ b/gruntfile.js @@ -18,16 +18,21 @@ var bowerArray = ['public/lib/angular/angular.min.js', 'public/lib/js-yaml/dist/js-yaml.js', 'public/lib/angular-sanitize/angular-sanitize.min.js']; +const bowerFiles = require('main-bower-files'); +const bowerDep = bowerFiles('**/**.js'); + module.exports = function(grunt) { require('jit-grunt')(grunt); + var angularTestDeps = ['public/lib/angular/angular.js', 'public/lib/angular-mocks/angular-mocks.js']; + // Unified Watch Object var watchFiles = { serverViews: ['app/views/**/*.pug'], serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js', '!app/tests/'], clientViews: ['public/modules/**/*.html', 'public/form_modules/forms/base/**/*.html', '!public/modules/forms/base/**/*.html',], - clientJS: ['public/form_modules/**/*.js', 'public/modules/**/*.js'], + clientJS: ['public/config.js', 'public/form-config.js', 'public/application.js', 'public/form-application.js', 'public/form_modules/**[!tests]/*.js', 'public/modules/**[!tests]/*.js'], clientCSS: ['public/modules/**/*.css'], serverTests: ['app/tests/**/*.js'], @@ -123,7 +128,7 @@ module.exports = function(grunt) { compress: true }, files: { - 'public/dist/form-vendor.min.js': bowerArray + 'public/dist/vendor.min.js': bowerArray } } }, @@ -138,7 +143,7 @@ module.exports = function(grunt) { dev: { script: 'server.js', options: { - nodeArgs: ['--debug'], + nodeArgs: ['--inspect'], ext: 'js,html', watch: watchFiles.serverViews.concat(watchFiles.serverJS) } @@ -201,66 +206,26 @@ module.exports = function(grunt) { level: 'log', terminal: true }, - singleRun: true + singleRun: false } }, - protractor: { - options: { - configFile: 'protractor.conf.js', - keepAlive: true, - noColor: false - }, - e2e: { - options: { - args: {} // Target-specific arguments - } - } - }, mocha_istanbul: { - coverage: { - src: watchFiles.allTests, // a folder works nicely - options: { - mask: '*.test.js', - require: ['server.js'] - } - }, - coverageClient: { - src: watchFiles.clientTests, // specifying file patterns works as well - options: { - coverageFolder: 'coverageClient', - mask: '*.test.js', - require: ['server.js'] - } - }, coverageServer: { src: watchFiles.serverTests, options: { coverageFolder: 'coverageServer', mask: '*.test.js', - require: ['server.js'] - } - }, - coveralls: { - src: watchFiles.allTests, // multiple folders also works - options: { - require: ['server.js'], - coverage: true, // this will make the grunt.event.on('coverage') event listener to be triggered - root: './lib', // define where the cover task should consider the root of libraries that are covered by tests - reportFormats: ['cobertura','lcovonly'] + require: ['server.js'], + reportFormats: ['html','lcovonly'] } } }, - istanbul_check_coverage: { - default: { - options: { - coverageFolder: 'coverage*', // will check both coverage folders and merge the coverage results - check: { - lines: 80, - statements: 80 - } - } - } - }, + lcovMerge: { + options: { + emitters: ['event'], + }, + src: ['./coverageServer/*.info', './coverageClient/**/*.info'] + }, html2js: { options: { base: 'public', @@ -285,9 +250,9 @@ module.exports = function(grunt) { }, main: { options: { - module: 'TellForm.templates' + module: 'app.templates' }, - src: ['public/modules/**/views/**.html', 'public/modules/**/views/**/*.html', 'public/form_modules/forms/base/**/*.html', '!public/modules/forms/base/**/*.html'], + src: ['public/modules/**/views/**.html', 'public/modules/**/views/**/*.html', 'public/form_modules/forms/base/**/*.html'], dest: 'public/dist/populate_template_cache.js' } }, @@ -323,9 +288,7 @@ module.exports = function(grunt) { }); // Code coverage tasks. - grunt.registerTask('coveralls', ['env:test','mocha_istanbul:coveralls']); - grunt.registerTask('coverage', ['env:test', 'mocha_istanbul:coverage']); - grunt.registerTask('coverage:client', ['env:test', 'mocha_istanbul:coverageClient']); + grunt.registerTask('coveralls', ['test:client', 'karma:unit', 'mocha_istanbul:coverageServer', 'lcovMerge']); grunt.registerTask('coverage:server', ['env:test', 'mocha_istanbul:coverageServer']); // Default task(s). @@ -334,9 +297,9 @@ module.exports = function(grunt) { // Debug task. grunt.registerTask('debug', ['lint', 'html2js:main', 'html2js:forms', 'concurrent:debug']); - + // Lint task(s). - grunt.registerTask('lint', ['jshint', 'csslint', 'i18nlint:client', 'i18nlint:server']); + grunt.registerTask('lint', ['jshint', 'csslint']); grunt.registerTask('lint:tests', ['jshint:allTests']); // Build task(s). @@ -346,9 +309,11 @@ module.exports = function(grunt) { grunt.registerTask('setup', ['execute']); // Test task(s). - grunt.registerTask('test', ['lint:tests', 'test:server', 'test:client']); + grunt.registerTask('test', ['test:server', 'test:client']); grunt.registerTask('test:server', ['lint:tests', 'env:test', 'mochaTest']); grunt.registerTask('test:client', ['lint:tests', 'html2js:main', 'html2js:forms', 'env:test', 'karma:unit']); + grunt.registerTask('test:travis', ['coverage:server', 'test:client', 'lcovMerge']); + grunt.registerTask('testdebug', ['env:test', 'karma:debug']); }; diff --git a/karma.conf.js b/karma.conf.js index d80e9196..979f49ea 100755 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,7 +18,7 @@ module.exports = function(config) { frameworks: ['jasmine'], // List of files / patterns to load in the browser - files: bowerDep.concat(['public/lib/socket.io-client/dist/socket.io.js', 'public/lib/mobile-detect/mobile-detect.js'], applicationConfiguration.assets.js, applicationConfiguration.assets.views, applicationConfiguration.assets.unit_tests), + files: bowerDep.concat(['public/lib/socket.io-client/dist/socket.io.js', 'public/lib/mobile-detect/mobile-detect.js', 'public/lib/quill/quill.js', 'public/lib/ng-quill/src/ng-quill.js'], applicationConfiguration.assets.js, applicationConfiguration.assets.views, applicationConfiguration.assets.unit_tests), // Test results reporter to use // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' @@ -29,11 +29,22 @@ module.exports = function(config) { 'public/modules/**/views/**/*.html': ['ng-html2js'], 'public/modules/**/views/*.html': ['ng-html2js'], 'public/form_modules/forms/base/views/**/*.html': ['ng-html2js'], - 'public/form_modules/forms/base/views/*.html': ['ng-html2js'] - //'public/modules/*/*.js': ['coverage'], - //'public/modules/*/*[!tests]*/*.js': ['coverage'], + 'public/form_modules/forms/base/views/*.html': ['ng-html2js'], + 'public/modules/*/*.js': ['coverage'], + 'public/modules/*/*[!tests]*/*.js': ['coverage'], + 'public/form_modules/*/*.js': ['coverage'], + 'public/form_modules/*/*[!tests]*/*.js': ['coverage'] }, + // configure coverage reporter + coverageReporter: { + reporters: [ + //{ type: 'html', subdir: 'report-html' }, + { type: 'lcov' }, + ], + dir : 'coverageClient/' + }, + ngHtml2JsPreprocessor: { stripPrefix: 'public/', prependPrefix: 'static/', diff --git a/package-lock.json b/package-lock.json index aad7f380..54607dd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "TellForm", + "name": "ohmyform", "version": "2.1.0", "lockfileVersion": 1, "requires": true, @@ -17,6 +17,11 @@ "@types/babel-types": "*" } }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -66,6 +71,11 @@ } } }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", @@ -344,6 +354,11 @@ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -436,8 +451,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "asynckit": { "version": "0.4.0", @@ -824,6 +838,11 @@ "repeat-element": "^1.1.2" } }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" + }, "browser-stdout": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", @@ -1678,6 +1697,19 @@ "parserlib": "~0.2.2" } }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "cssstyle": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.3.0.tgz", + "integrity": "sha512-wXsoRfsRfsLVNaVzoKdqvEmK/5PFaEXNspVT22Ots6K/cnJdpoDKuQFw+qlMiXnmaif1OgeC466X1zISgAOcGg==", + "requires": { + "cssom": "~0.3.6" + } + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1716,6 +1748,16 @@ "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==", "dev": true }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, "date-format": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.0.0.tgz", @@ -1773,8 +1815,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "define-property": { "version": "2.0.2", @@ -1948,6 +1989,14 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, "domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", @@ -2746,8 +2795,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "faye-websocket": { "version": "0.4.4", @@ -3019,7 +3067,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3040,12 +3089,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3060,17 +3111,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3187,7 +3241,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3199,6 +3254,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3213,6 +3269,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3220,12 +3277,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3244,6 +3303,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3324,7 +3384,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3336,6 +3397,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3421,7 +3483,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3457,6 +3520,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3476,6 +3540,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3519,12 +3584,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -4421,7 +4488,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4439,11 +4507,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4456,15 +4526,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4567,7 +4640,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4577,6 +4651,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4589,17 +4664,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4616,6 +4694,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4688,7 +4767,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4698,6 +4778,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4773,7 +4854,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4803,6 +4885,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4820,6 +4903,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4858,11 +4942,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -5005,8 +5091,45 @@ "from": "github:jwarby/grunt-i18nlint", "dev": true, "requires": { - "chalk": "^1.1.0", - "i18n-lint": "git://github.com/jwarby/i18n-lint.git#0a06373c0d880047ad680239c103d60ed414efc1" + "chalk": "^1.1.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "i18n-lint": { + "version": "git://github.com/jwarby/i18n-lint.git#0a06373c0d880047ad680239c103d60ed414efc1", + "from": "git://github.com/jwarby/i18n-lint.git#0a06373c0d880047ad680239c103d60ed414efc1", + "requires": { + "chalk": "^1.0.0", + "commander": "^2.0.0", + "glob": "^5.0.0", + "htmlparser2": "^3.0.0", + "lodash": "^3.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "grunt-karma": { @@ -5445,11 +5568,6 @@ "resolved": "https://registry.npmjs.org/hooks/-/hooks-0.2.1.tgz", "integrity": "sha1-D1kbGzRL3LPfWXc/Yvu6+Fv0Aos=" }, - "hooks-fixed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz", - "integrity": "sha1-DowVM2cI5mERhf45C0RofdUjDbs=" - }, "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", @@ -5475,6 +5593,14 @@ } } }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "html-minifier": { "version": "3.5.21", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", @@ -5638,49 +5764,6 @@ "sprintf-js": ">=1.0.3" } }, - "i18n-lint": { - "version": "git://github.com/jwarby/i18n-lint.git#0a06373c0d880047ad680239c103d60ed414efc1", - "from": "git://github.com/jwarby/i18n-lint.git", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "commander": "^2.0.0", - "glob": "^5.0.0", - "htmlparser2": "^3.0.0", - "lodash": "^3.0.0", - "node.extend": "^1.0.0" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "iconv-lite": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", @@ -5860,17 +5943,16 @@ "integrity": "sha1-x+NWzeoiWucbNtcPLnGpK6TkJZA=", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, - "is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", - "dev": true - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -6352,6 +6434,106 @@ "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", "dev": true }, + "jsdom": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.1.1.tgz", + "integrity": "sha512-cQZRBB33arrDAeCrAEWn1U3SvrvC8XysBua9Oqg1yWrsY/gYcusloJC3RZJXuY5eehSCmws8f2YeliCqGSkrtQ==", + "requires": { + "abab": "^2.0.0", + "acorn": "^6.1.1", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.3.6", + "cssstyle": "^1.2.2", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.1.4", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz", + "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==" + }, + "acorn-globals": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", + "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "escodegen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "ws": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", + "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, "jshint": { "version": "2.9.7", "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.7.tgz", @@ -6464,11 +6646,6 @@ "promise": "^7.0.1" } }, - "kareem": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz", - "integrity": "sha1-eAXSFbtTIU7Dr5aaHQsfF+PnuVw=" - }, "karma": { "version": "0.13.22", "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", @@ -7001,7 +7178,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7022,12 +7200,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7042,17 +7222,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7169,7 +7352,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7181,6 +7365,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7195,6 +7380,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7202,12 +7388,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7226,6 +7414,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7306,7 +7495,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7318,6 +7508,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7403,7 +7594,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7439,6 +7631,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7458,6 +7651,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7501,12 +7695,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -8097,7 +8293,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -8123,9 +8318,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==" }, "lodash._arraycopy": { "version": "3.0.0", @@ -8305,6 +8500,11 @@ "integrity": "sha1-lU73UEkmIDjJbR/Jiyj9r58Hcqo=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, "log-driver": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", @@ -8905,123 +9105,6 @@ "bson": "~0.4.20" } }, - "mongoose": { - "version": "4.4.20", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.4.20.tgz", - "integrity": "sha1-6XT/tq6MUPQJgBqEl6mOnztR8t0=", - "requires": { - "async": "1.5.2", - "bson": "~0.4.23", - "hooks-fixed": "1.1.0", - "kareem": "1.0.1", - "mongodb": "2.1.18", - "mpath": "0.2.1", - "mpromise": "0.5.5", - "mquery": "1.11.0", - "ms": "0.7.1", - "muri": "1.1.0", - "regexp-clone": "0.0.1", - "sliced": "1.0.1" - }, - "dependencies": { - "bluebird": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", - "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=" - }, - "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", - "requires": { - "ms": "0.7.1" - } - }, - "es6-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz", - "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "mongodb": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.1.18.tgz", - "integrity": "sha1-KNQLUVsr5NWmn/3UxTXw30MuQJc=", - "requires": { - "es6-promise": "3.0.2", - "mongodb-core": "1.3.18", - "readable-stream": "1.0.31" - } - }, - "mongodb-core": { - "version": "1.3.18", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.3.18.tgz", - "integrity": "sha1-kGhLO3xzVtZa41Y5HTCw8kiATHo=", - "requires": { - "bson": "~0.4.23", - "require_optional": "~1.0.0" - } - }, - "mpath": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.2.1.tgz", - "integrity": "sha1-Ok6Ck1mAHeljCcJ6ay4QLon56W4=" - }, - "mpromise": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", - "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" - }, - "mquery": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.11.0.tgz", - "integrity": "sha1-4MZd7bEDftv2z7iCYud3/uI1Udk=", - "requires": { - "bluebird": "2.10.2", - "debug": "2.2.0", - "regexp-clone": "0.0.1", - "sliced": "0.0.5" - }, - "dependencies": { - "sliced": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", - "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" - } - } - }, - "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" - }, - "readable-stream": { - "version": "1.0.31", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", - "integrity": "sha1-jyUC4LyeOw2huUUgqrtOJgPsr64=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, "morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -9287,15 +9370,6 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" }, - "node.extend": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", - "integrity": "sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y=", - "dev": true, - "requires": { - "is": "^3.1.0" - } - }, "nodemailer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.0.1.tgz", @@ -9601,7 +9675,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -9622,12 +9697,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9642,17 +9719,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9769,7 +9849,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -9781,6 +9862,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9795,6 +9877,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9802,12 +9885,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9826,6 +9911,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9906,7 +9992,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9918,6 +10005,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -10003,7 +10091,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -10039,6 +10128,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10058,6 +10148,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10101,12 +10192,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -10395,6 +10488,11 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwsapi": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -10513,7 +10611,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.4", @@ -10526,8 +10623,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" } } }, @@ -10697,6 +10793,11 @@ "error-ex": "^1.2.0" } }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, "parsejson": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz", @@ -11153,6 +11254,11 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -11161,8 +11267,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prepend-http": { "version": "1.0.4", @@ -12268,6 +12373,24 @@ "throttleit": "^1.0.0" } }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12280,15 +12403,6 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -12303,11 +12417,6 @@ "path-parse": "^1.0.6" } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -12397,6 +12506,14 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "requires": { + "xmlchars": "^2.1.1" + } + }, "scandirectory": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", @@ -13075,6 +13192,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, "stream-shift": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", @@ -13353,6 +13475,11 @@ } } }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, "taskgroup": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", @@ -13546,6 +13673,21 @@ "punycode": "^1.4.1" } }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", @@ -13573,7 +13715,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -13976,6 +14117,24 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, "watchr": { "version": "2.4.13", "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.4.13.tgz", @@ -13991,6 +14150,44 @@ "typechecker": "^2.0.8" } }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", @@ -14141,6 +14338,16 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.1.1.tgz", + "integrity": "sha512-7hew1RPJ1iIuje/Y01bGD/mXokXxegAgVS+e+E0wSi2ILHQkYAH1+JXARwTjZSM4Z4Z+c73aKspEcqj+zPPL/w==" + }, "xmlhttprequest-ssl": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz", diff --git a/package.json b/package.json index 9e0583ed..2a35745f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "TellForm", + "name": "ohmyform", "description": "Opensource alternative to TypeForm", "version": "2.1.0", - "homepage": "https://github.com/tellform/tellform", + "homepage": "https://github.com/ohmyform/ohmyform", "authors": [ "David Baldwynn (http://baldwynn.me)" ], @@ -10,11 +10,7 @@ "private": true, "repository": { "type": "git", - "url": "https://github.com/tellform/tellform.git" - }, - "engines": { - "node": "6.x.x", - "npm": "3.x.x" + "url": "https://github.com/ohmyform/ohmyform.git" }, "scripts": { "addcontrib": "all-contributors add", @@ -53,11 +49,12 @@ "helmet": "^3.16.0", "i18n": "^0.8.3", "jit-grunt": "^0.9.1", - "lodash": "^4.17.11", + "jsdom": "^15.1.1", + "lodash": "^4.17.13", "main-bower-files": "^2.13.1", "method-override": "~2.3.0", "mkdirp": "^0.5.1", - "mongoose": "~4.4.19", + "mongoose": "^5.6.4", "morgan": "^1.9.1", "nodemailer": "~4.0.0", "passport": "~0.3.0", diff --git a/process.yml b/process.yml deleted file mode 100644 index e361593c..00000000 --- a/process.yml +++ /dev/null @@ -1,5 +0,0 @@ -apps: - - script : 'server.js' - name : 'TellForm' - exec_mode: 'cluster' - instances: 4 diff --git a/public/application.js b/public/application.js index a99ca02d..251dc6ba 100755 --- a/public/application.js +++ b/public/application.js @@ -25,6 +25,9 @@ angular.module(ApplicationConfiguration.applicationModuleName).constant('USER_RO superuser: 'superuser' }); +//users url +angular.module(ApplicationConfiguration.applicationModuleName).constant('USERS_URL', '/users'); + //form url angular.module(ApplicationConfiguration.applicationModuleName).constant('FORM_URL', '/forms/:formId'); diff --git a/public/config.js b/public/config.js index d21482bf..326670cf 100755 --- a/public/config.js +++ b/public/config.js @@ -2,22 +2,29 @@ // Init the application configuration module for AngularJS application var ApplicationConfiguration = (function() { - // Init module configuration options - var applicationModuleName = 'TellForm'; - var applicationModuleVendorDependencies = ['duScroll', 'ui.select', 'ngSanitize', 'vButton', 'ngResource', 'TellForm.templates', 'ui.router', 'ui.bootstrap', 'ui.utils', 'pascalprecht.translate', 'view-form']; - // Add a new vertical module var registerModule = function(moduleName, dependencies) { // Create angular module angular.module(moduleName, dependencies || []); // Add the module to the AngularJS configuration file - angular.module(applicationModuleName).requires.push(moduleName); + angular.module('app').requires.push(moduleName); }; return { - applicationModuleName: applicationModuleName, - applicationModuleVendorDependencies: applicationModuleVendorDependencies, + applicationModuleName: 'app', + applicationModuleVendorDependencies: [ + 'duScroll', + 'ui.select', + 'ngSanitize', + 'vButton', + 'ngResource', + 'app.templates', + 'ui.router', + 'ui.bootstrap', + 'ui.utils', + 'pascalprecht.translate' + ], registerModule: registerModule }; })(); diff --git a/public/form_modules/forms/base/config/forms.client.config.js b/public/form_modules/forms/base/config/forms.client.config.js index 242581e0..efd9ee37 100644 --- a/public/form_modules/forms/base/config/forms.client.config.js +++ b/public/form_modules/forms/base/config/forms.client.config.js @@ -28,9 +28,8 @@ angular.module('view-form') } return 0; }; -}); - -angular.module('view-form').value('supportedFields', [ +}) +.value('supportedFields', [ 'textfield', 'textarea', 'date', @@ -44,7 +43,18 @@ angular.module('view-form').value('supportedFields', [ 'yes_no', 'number', 'natural' -]); +]) +.constant('VIEW_FORM_URL', '/forms/:formId/render') +.filter('indexToAlphabet', function(){ + return function(index){ + var char = String.fromCharCode(index + 65); + return char; + }; +}) -angular.module('view-form').constant('VIEW_FORM_URL', '/forms/:formId/render'); +//Angular-Scroll Settings +angular.module('view-form').value('duScrollActiveClass', 'activeField') + .value('duScrollGreedy', true) + .value('duScrollOffset', 100) + .value('duScrollSpyWait', 0); diff --git a/public/form_modules/forms/base/config/i18n/english.js b/public/form_modules/forms/base/config/i18n/english.js index fe12abaf..9941d5ae 100644 --- a/public/form_modules/forms/base/config/i18n/english.js +++ b/public/form_modules/forms/base/config/i18n/english.js @@ -34,6 +34,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr ADD_NEW_LINE_INSTR: 'Press SHIFT+ENTER to add a newline', ERROR: 'Error', + LOADING_LABEL: 'Loading', + WAIT_LABEL: 'Please wait', + FORM_404_HEADER: '404 - Form Does Not Exist', FORM_404_BODY: 'The form you are trying to access does not exist. Sorry about that!', diff --git a/public/form_modules/forms/base/config/i18n/french.js b/public/form_modules/forms/base/config/i18n/french.js index 2db54e34..0d2bca0c 100644 --- a/public/form_modules/forms/base/config/i18n/french.js +++ b/public/form_modules/forms/base/config/i18n/french.js @@ -34,11 +34,14 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr ADD_NEW_LINE_INSTR: 'Appuyez sur MAJ + ENTRÉE pour ajouter une nouvelle ligne', ERROR: 'Erreur', + LOADING_LABEL: 'Chargement', + WAIT_LABEL: "Veuillez patienter", + FORM_404_HEADER: '404 - Le formulaire n\'existe pas', FORM_404_BODY: 'Le formulaire auquel vous essayez d\'accéder n\'existe pas. Désolé pour ça !', FORM_UNAUTHORIZED_HEADER: 'Non autorisé à accéder au formulaire', -   FORM_UNAUTHORIZED_BODY1: 'Le formulaire auquel vous essayez d\'accéder est actuellement privé et inaccessible publiquement.', +   FORM_UNAUTHORIZED_BODY1: 'Le formulaire auquel vous essayez d\'accéder est actuellement privé et inaccessible publiquement.',    FORM_UNAUTHORIZED_BODY2: 'Si vous êtes le propriétaire du formulaire, vous pouvez le définir en "Public" dans le panneau "Configuration" du formulaire admin.', }); diff --git a/public/form_modules/forms/base/config/i18n/german.js b/public/form_modules/forms/base/config/i18n/german.js index df335ce5..f9d53732 100644 --- a/public/form_modules/forms/base/config/i18n/german.js +++ b/public/form_modules/forms/base/config/i18n/german.js @@ -33,13 +33,16 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr OPTION_PLACEHOLDER: 'Geben oder wählen Sie eine Option aus', ADD_NEW_LINE_INSTR: 'Drücken Sie UMSCHALT + EINGABETASTE, um eine neue Zeile hinzuzufügen', ERROR: 'Fehler', + + LOADING_LABEL: 'Laden', + WAIT_LABEL: 'Bitte warten', FORM_404_HEADER: '404 - Formular existiert nicht', FORM_404_BODY: 'Das Formular, auf das Sie zugreifen möchten, existiert nicht. Das tut mir leid!', FORM_UNAUTHORIZED_HEADER: 'Nicht zum Zugriffsformular berechtigt\' ', -   FORM_UNAUTHORIZED_BODY1: 'Das Formular, auf das Sie zugreifen möchten, ist derzeit privat und nicht öffentlich zugänglich.', -   FORM_UNAUTHORIZED_BODY2: 'Wenn Sie der Eigentümer des Formulars sind, können Sie es im Fenster "Konfiguration" im Formular admin auf "Öffentlich" setzen.', +   FORM_UNAUTHORIZED_BODY1: 'Das Formular, auf das Sie zugreifen möchten, ist derzeit privat und nicht öffentlich zugänglich.', +   FORM_UNAUTHORIZED_BODY2: 'Wenn Sie der Eigentümer des Formulars sind, können Sie es im Fenster "Konfiguration" im Formular admin auf "Öffentlich" setzen.', }); }]); diff --git a/public/form_modules/forms/base/config/i18n/italian.js b/public/form_modules/forms/base/config/i18n/italian.js index 62058a2e..20f34143 100644 --- a/public/form_modules/forms/base/config/i18n/italian.js +++ b/public/form_modules/forms/base/config/i18n/italian.js @@ -33,6 +33,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr OPTION_PLACEHOLDER: 'Digitare o selezionare un\'opzione', ADD_NEW_LINE_INSTR: 'Premere SHIFT + INVIO per aggiungere una nuova riga', ERROR: 'Errore', + + LOADING_LABEL: 'Caricamento', + WAIT_LABEL: "Attendere prego", FORM_404_HEADER: '404 - Il modulo non esiste', FORM_404_BODY: 'La forma che stai cercando di accedere non esiste. Ci dispiace!', diff --git a/public/form_modules/forms/base/config/i18n/spanish.js b/public/form_modules/forms/base/config/i18n/spanish.js index 86861eb3..90c0b8ca 100644 --- a/public/form_modules/forms/base/config/i18n/spanish.js +++ b/public/form_modules/forms/base/config/i18n/spanish.js @@ -34,6 +34,9 @@ angular.module('view-form').config(['$translateProvider', function ($translatePr ADD_NEW_LINE_INSTR: 'Presione MAYÚS + ENTRAR para agregar una nueva línea', ERROR: 'Error', + LOADING_LABEL: 'Cargando', + WAIT_LABEL: 'Espera', + FORM_404_HEADER: '404 - La forma no existe', FORM_404_BODY: 'El formulario al que intenta acceder no existe. ¡Lo siento por eso!', diff --git a/public/form_modules/forms/base/css/form.css b/public/form_modules/forms/base/css/form.css deleted file mode 100644 index f6b8eadc..00000000 --- a/public/form_modules/forms/base/css/form.css +++ /dev/null @@ -1,555 +0,0 @@ -.panel-default.startPage { - border-style: dashed; - border-color: #a9a9a9; - border-width:3px; -} - -.busy-updating-wrapper { - text-align: center; - font-size: 20px; - position: fixed; - bottom: 0; - right: 55px; - z-index: 1; -} - -.busy-submitting-wrapper { - position: fixed; - top: 50%; - left: 0; - right: 0; - bottom: 0; -} - -.dropzone h4.panel-title { - height: 17px; - overflow: hidden; -} - -.container.admin-form { - margin-top: 70px; -} - -.public-form input, .public-form textarea { - background-color: #000000; - background-color: rgba(0,0,0,0); - border: 2px dashed #ddd!important; -} - -.public-form input:focus, .public-form textarea:focus { - border: 2px dashed #ddd!important; - outline: 0; -} - -/*.public-form input.no-border.ng-invalid, .public-form textarea.no-border { - border-color: none; -}*/ -.public-form input.ng-valid, .public-form textarea.ng-valid { - border-color: #20FF20!important; - border-style: solid!important; - border-width: 3px!important; -} - -.public-form input.ng-invalid.ng-dirty, .public-form textarea.ng-invalid.ng-dirty { - border-color: #FA787E!important; - border-style: solid!important; - border-width: 3px!important; -} - -section.content p.breakwords { - word-break: break-all; -} - -.btn { - border: 1px solid #c6c6c6; -} - -.btn[type='submit'] { - font-size: 1.5em; - padding: 0.35em 1.2em 0.35em 1.2em; -} - -section.content > section > section.container { - margin-top: 70px; -} - -/* -** Modal CSS Styles -*/ -.modal-header { - padding: 15px; - border-bottom: 1px solid #e5e5e5; - font-size: 18px; - font-weight: normal; -} -.input-block { - display: block; - width: 100%; -} -.modal-footer input[type='text'] { - min-height: 34px; - padding: 7px 8px; - font-size: 13px; - color: #333; - vertical-align: middle; - background-color: #fff; - background-repeat: no-repeat; - background-position: right 8px center; - border: 1px solid #ccc; - border-radius: 3px; - box-shadow: inset 0 1px 2px rgba(0,0,0,0.075); -} -.modal-body > .modal-body-alert { - color: #796620; - background-color: #f8eec7; - border-color: #f2e09a; - margin: -16px -15px 15px; - padding: 10px 15px; - border-style: solid; - border-width: 1px 0; -} - -div.form-fields { - position: relative; - padding-top: 35vh; -} -.letter { - position: relative; - display: -moz-inline-stack; - display: inline-block; - vertical-align: top; - zoom: 1; - width: 16px; - margin-top: 1px; - padding: 0; - height: 17px; - font-size: 12px; - line-height: 19px; - border: 1px solid #000; - border: 1px solid rgba(0,0,0,.2); - margin-right: 7px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - text-align: center; - font-weight: 700; -} - -div.form-submitted > .field.row { - padding-bottom: 2%; - margin-top: 35vh; -} -div.form-submitted > .field.row > div { - font-size: 1.7em; -} - -/* Styles for accordion */ -form .accordion-edit { - width: inherit; -} - -/*Styles for ui-datepicker*/ -.ui-datepicker.ui-widget { - z-index: 99!important; -} - -form .row.field .field-number { - margin-right: 0.5em; -} - -/* Styles for form submission view (/forms/:formID) */ -form .row.field { - padding: 1em 0 0 0; - width: inherit; -} - form .row.field > .field-title { - margin-top:0.5em; - font-size:1.2em; - padding-bottom: 1.8em; - width: inherit; - } - form .row.field > .field-input { - font-size: 1.4em; - color: #777; - } - form.submission-form .row.field.statement > .field-title { - font-size:1.7em; - } - form.submission-form .row.field.statement > .field-input { - font-size:1em; - color:#ddd; - } - - form.submission-form .select.radio > .field-input input, form.submission-form .select > .field-input input { - width:20%; - } - - form.submission-form .field.row.radio .btn.activeBtn { - background-color: rgb(0,0,0)!important; - background-color: rgba(0,0,0,0.7)!important; - color: white; - } - form.submission-form .field.row.radio .btn { - margin-right:1.2em; - } - - form.submission-form .select > .field-input .btn { - text-align: left; - margin-bottom:0.7em; - } - form.submission-form .select > .field-input .btn > span { - font-size: 1.10em; - } - - /*form.submission-form .field-input > input:focus { - font-size:1em; - }*/ - - form .field-input > textarea{ - padding: 0.45em 0.9em; - width: 100%; - line-height: 160%; - } - - form .field-input > input.hasDatepicker{ - padding: 0.45em 0.9em; - width: 50%; - line-height: 160%; - } - form .field-input > input.text-field-input{ - padding: 0.45em 0.9em; - width: 100%; - line-height: 160%; - } - form .required-error{ - color: #ddd; - font-size:0.8em; - } - - form .row.field.dropdown > .field-input input { - min-height: 34px; - border-width: 0 0 2px 0; - border-radius: 5px; - } - - form .row.field.dropdown > .field-input input:focus { - border: none; - } - - form .dropdown > .field-input .ui-select-choices-row-inner { - border-radius: 3px; - margin: 5px; - padding: 10px; - background-color: #000000; - background-color: rgba(0,0,0,0.05); - } - - form .dropdown > .field-input .ui-select-choices-row-inner.active, form .dropdown > .field-input .ui-select-choices-row-inner.active:focus { - background-color: #000000; - background-color: rgba(0,0,0,0.1); - } -.config-form { - max-width: 100%; -} - -.config-form > .row { - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); -} - -div.config-form .row.field { - padding-top:1.5em; -} - - div.config-form > .row > .container:nth-of-type(odd){ - border-right: 1px #ddd solid; - } - div.config-form.design > .row > .container:nth-of-type(odd){ - border-right: none; - } - - div.config-form .row > .field-input { - padding-left:0.1em; - } - div.config-form .row > .field-input label { - padding-left:1.3em; - display: block; - } - - -/* Styles for form admin view (/forms/:formID/admin) */ -.admin-form > .page-header { - padding-bottom: 0; - margin-bottom: 40px; -} - .admin-form > .page-header h1 { - margin-bottom: 0; - margin-top: 0; - } - .admin-form > .page-header > .col-xs-3 { - padding-top: 1.4em; - } -.admin-form .form-controls .row { - padding: 5px; -} -.admin-form .page-header { - border: none; -} - -/*Styles for admin view tabs */ -.admin-form .tab-content { - padding-top: 3em; -} - -.admin-form .panel-heading { - background-color: #f1f1f1; - position: relative!important; -} - .admin-form .panel-heading:hover { - background-color: #fff; - cursor: pointer; - } - .admin-form .panel-heading a:hover { - text-decoration: none; - } - -.current-fields .panel-body .row.question input[type='text'], .current-fields .panel-body .row.description textarea{ - width: 100%; -} -.current-fields .panel-body .row.options input[type='text'] { - width: 80%; -} - -/*Override Select2 UI*/ -.ui-select-choices.ui-select-dropdown { - top:2.5em!important; -} -.ui-select-toggle { - box-shadow:none!important; - border:none!important; -} - -.current-fields .tool-panel > .panel-default:hover { - border-color: #9d9d9d; - cursor: pointer; -} - -.current-fields .tool-panel > .panel-default .panel-heading { - background-color: #fff; - color: #9d9d9d!important; -} - .current-fields .tool-panel > .panel-default .panel-heading:hover { - background-color: #eee; - color: #000!important; - cursor: pointer; - } -.current-fields .tool-panel > .panel-default .panel-heading a { - color: inherit; -} -.current-fields .tool-panel > .panel-default .panel-heading a:hover{ - text-decoration: none; -} - -/*Styles for submission table*/ -.submissions-table .table-outer.row { - margin: 1.5em 0 2em 0!important; -} -.submissions-table .table-outer .col-xs-12 { - padding-left: 0!important; - border:1px solid #ddd; - overflow-x: scroll; - border-radius:3px; -} -.submissions-table .table > thead > tr > th { - min-width:8em; -} -.submissions-table .table > tbody > tr.selected { - background-color:#efefef; -} - - -/*Styles for add fields tab*/ -.admin-form .add-field { - background-color: #ddd; - padding: 0 2% 0 2%; - border-radius: 3px; -} - .admin-form .add-field .col-xs-6 { - padding: 0.25em 0.4em; - } - .admin-form .add-field .col-xs-6 .panel-heading { - border-width: 1px; - border-style: solid; - border-color: #bbb; - border-radius: 4px; - } - - .admin-form .oscar-field-select { - margin: 10px 0 10px; - } - -.view-form-btn.span { - padding-right:0.6em; -} -.status-light.status-light-off { - color: #BE0000; -} -.status-light.status-light-on { - color: #33CC00; -} - -/* Styles for form list view (/forms) */ -section.public-form { - padding: 0 10% 0 10%; -} -section.public-form .form-submitted { - height: 100vh; -} - -section.public-form .btn { - border: 1px solid; -} - -.form-item { - text-align: center; - border-bottom: 6px inset #ccc; - background-color: #eee; - width: 180px; - /*width:100%;*/ - position: relative; - height: 215px; - /*padding-bottom: 25%;*/ - margin-bottom: 45px; -} -.form-item.create-new input[type='text']{ - width: inherit; - color:black; - border:none; -} - -.form-item.create-new { - background-color: rgb(131,131,131); - color: white; -} - -/*CREATE-NEW FORM MODAL*/ -.form-item.new-form { - background-color: rgb(300,131,131); - z-index: 11; -} -.form-item.new-form:hover { - background-color: rgb(300,100,100); -} - .form-item.new-form input[type='text'] { - margin-top:0.2em; - width: inherit; - color:black; - border:none; - padding: 0.3em 0.6em 0.3em 0.6em; - } - .form-item.new-form .custom-select { - margin-top: 0.2em - } - .form-item.new-form .custom-select select { - background-color: white; - } - - - .form-item.new-form .details-row { - margin-top: 1em; - } - .form-item.new-form .details-row.submit { - margin-top: 1.7em; - } - .form-item.new-form .details-row.submit .btn { - font-size: 0.95em; - } - - .form-item.new-form .title-row { - margin-top: 1em; - top:0; - } - -/*Modal overlay (for lightbox effect)*/ -.overlay { - position: fixed; - top: 0; - left: 0; - height: 100%; - width: 100%; - background-color: rgb(0,0,0); - background-color: rgba(0,0,0,0.5); - z-index: 10; -} -.overlay.submitform { - background-color: rgb(256,256,256); - background-color: rgba(256,256,256,0.8); -} -.field-directive { - z-index: 9; - padding: 10% 10% 10% 0; - border: 25px transparent solid; - position: relative; -} -.activeField { - z-index: 11; - position: relative; - background-color: transparent; -} -.activeField.field-directive { - display: inline-block; - border-radius: 7px; - width: 100%; - border: 25px transparent solid; -} - .activeField input { - background-color: transparent; - } - -.form-item:hover, .form-item.create-new:hover { - border-bottom: 8px inset #ccc; - background-color: #d9d9d9; -} - -.form-item.create-new:hover { - background-color: rgb(81,81,81); -} - -.form-item > .row.footer { - position: absolute; - bottom: 0; - left: 30%; -} - -.form-item .title-row{ - position: relative; - top: 15px; - padding-top:3em; - padding-bottom:3.65em; -} - .form-item .title-row h4 { - font-size: 1.3em; - } - -.form-item.create-new .title-row{ - padding: 0; -} - .form-item.create-new .title-row h4 { - font-size: 7em; - } - -.form-item .details-row{ - margin-top: 3.2em; -} - .form-item .details-row small { - font-size: 0.6em; - } - .form-item.create-new .details-row small { - font-size: 0.95em; - } diff --git a/public/form_modules/forms/base/directives/on-enter-key.client.directive.js b/public/form_modules/forms/base/directives/on-enter-key.client.directive.js index eb4e5cfd..c90fd5ac 100644 --- a/public/form_modules/forms/base/directives/on-enter-key.client.directive.js +++ b/public/form_modules/forms/base/directives/on-enter-key.client.directive.js @@ -65,8 +65,6 @@ angular.module('view-form').directive('onEnterKey', ['$rootScope', function($roo var keyCode = event.which || event.keyCode; if(keyCode === 9 && event.shiftKey) { - - console.log('onTabAndShiftKey'); event.preventDefault(); $rootScope.$apply(function() { $rootScope.$eval($attrs.onTabAndShiftKey); diff --git a/public/form_modules/forms/base/directives/submit-form.client.directive.js b/public/form_modules/forms/base/directives/submit-form.client.directive.js index 3798eb40..0c9b4254 100644 --- a/public/form_modules/forms/base/directives/submit-form.client.directive.js +++ b/public/form_modules/forms/base/directives/submit-form.client.directive.js @@ -13,31 +13,37 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun function ($http, TimeCounter, $filter, $rootScope, SendVisitorData, $translate, $timeout) { return { templateUrl: 'form_modules/forms/base/views/directiveViews/form/submit-form.client.view.html', - restrict: 'E', + restrict: 'E', scope: { - myform:'=', - ispreview: '=' + myform: '=' }, controller: function($document, $window, $scope){ - var NOSCROLL = false; - var FORM_ACTION_ID = 'submit_field'; + var FORM_ACTION_ID = 'submit_field'; $scope.forms = {}; - - //Don't start timer if we are looking at a design preview - if($scope.ispreview){ - TimeCounter.restartClock(); + + var form_fields_count = $scope.myform.visible_form_fields.filter(function(field){ + return field.fieldType !== 'statement'; + }).length; + + $scope.$watch('myform', function(oldVal, newVal){ + $scope.myform.visible_form_fields = $scope.myform.form_fields.filter(function(field){ + return !field.deletePreserved; + }); + }); + + $scope.updateFormValidity = function(){ + $timeout(function(){ + var nb_valid = $scope.myform.form_fields.filter(function(field){ + return (field.fieldType === 'statement' || field.fieldValue !== '' || !field.required); + }).length; + $scope.translateAdvancementData = { + done: nb_valid, + total: $scope.myform.visible_form_fields.length + }; + }); } - var form_fields_count = $scope.myform.visible_form_fields.filter(function(field){ - return field.fieldType !== 'statement'; - }).length; - - var nb_valid = $filter('formValidity')($scope.myform); - $scope.translateAdvancementData = { - done: nb_valid, - total: form_fields_count, - answers_not_completed: form_fields_count - nb_valid - }; + $scope.updateFormValidity(); $scope.reloadForm = function(){ //Reset Form @@ -47,7 +53,7 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun return field; }).value(); - $scope.loading = false; + $scope.loading = false; $scope.error = ''; $scope.selected = { @@ -63,240 +69,179 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun /* ** Field Controls */ - var evaluateLogicJump = function(field){ - var logicJump = field.logicJump; + var evaluateLogicJump = function(field){ + var logicJump = field.logicJump; - if(logicJump.enabled){ - if (logicJump.expressionString && logicJump.valueB && field.fieldValue) { - var parse_tree = jsep(logicJump.expressionString); - var left, right; + if(logicJump.enabled){ + if (logicJump.expressionString && logicJump.valueB && field.fieldValue) { + var parse_tree = jsep(logicJump.expressionString); + var left, right; - if(parse_tree.left.name === 'field'){ - left = field.fieldValue; - right = logicJump.valueB; - } else { - left = logicJump.valueB; - right = field.fieldValue; - } + if(parse_tree.left.name === 'field'){ + left = field.fieldValue; + right = logicJump.valueB; + } else { + left = logicJump.valueB; + right = field.fieldValue; + } - if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){ - switch(parse_tree.operator) { - case '==': - return (parseInt(left) === parseInt(right)); - case '!==': - return (parseInt(left) !== parseInt(right)); - case '>': - return (parseInt(left) > parseInt(right)); - case '>=': - return (parseInt(left) > parseInt(right)); - case '<': - return (parseInt(left) < parseInt(right)); - case '<=': - return (parseInt(left) <= parseInt(right)); - default: - return false; - } - } else { - switch(parse_tree.operator) { - case '==': - return (left === right); - case '!==': - return (left !== right); - case 'contains': - return (left.indexOf(right) > -1); - case '!contains': - /* jshint -W018 */ - return !(left.indexOf(right) > -1); - case 'begins': - return left.startsWith(right); - case '!begins': - return !left.startsWith(right); - case 'ends': - return left.endsWith(right); - case '!ends': - return left.endsWith(right); - default: - return false; - } - } - } - } - }; + if(field.fieldType === 'number' || field.fieldType === 'scale' || field.fieldType === 'rating'){ + switch(parse_tree.operator) { + case '==': + return (parseInt(left) === parseInt(right)); + case '!==': + return (parseInt(left) !== parseInt(right)); + case '>': + return (parseInt(left) > parseInt(right)); + case '>=': + return (parseInt(left) > parseInt(right)); + case '<': + return (parseInt(left) < parseInt(right)); + case '<=': + return (parseInt(left) <= parseInt(right)); + default: + return false; + } + } else { + switch(parse_tree.operator) { + case '==': + return (left === right); + case '!==': + return (left !== right); + case 'contains': + return (left.indexOf(right) > -1); + case '!contains': + /* jshint -W018 */ + return !(left.indexOf(right) > -1); + case 'begins': + return left.startsWith(right); + case '!begins': + return !left.startsWith(right); + case 'ends': + return left.endsWith(right); + case '!ends': + return left.endsWith(right); + default: + return false; + } + } + } + } + }; - var getActiveField = function(){ - if($scope.selected === null){ - console.error('current active field is null'); - throw new Error('current active field is null'); - } + $rootScope.getActiveField = function(){ + if($scope.selected === null){ + console.error('current active field is null'); + throw new Error('current active field is null'); + } - if($scope.selected._id === FORM_ACTION_ID) { - return $scope.myform.form_fields.length - 1; - } - return $scope.selected.index; - }; - - $scope.isActiveField = function(field){ - if($scope.selected._id === field._id) { - return true - } - return false; - }; + if($scope.selected._id === FORM_ACTION_ID) { + return $scope.myform.visible_form_fields[$scope.selected.index - 1]._id; + } + return $scope.selected._id; + }; $scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) { - if($scope.selected === null || (!field_id && field_index === null) ) { - return; + if(!field_id && field_index === null) { + return; } - - if(!field_id){ - field_id = $scope.myform.visible_form_fields[field_index]._id; - } else if(field_index === null){ - field_index = $scope.myform.visible_form_fields.length - for(var i=0; i < $scope.myform.visible_form_fields.length; i++){ - var currField = $scope.myform.visible_form_fields[i]; - if(currField['_id'] == field_id){ - field_index = i; - break; - } - } - } + if(field_id === FORM_ACTION_ID){ + field_index = $scope.myform.visible_form_fields.length; + } else if(!field_id) { + field_id = $scope.myform.visible_form_fields[field_index]._id; + } else if(field_index === null){ + field_index = $scope.myform.visible_form_fields.length - if($scope.selected._id === field_id){ - return; - } + for(var i=0; i < $scope.myform.visible_form_fields.length; i++){ + var currField = $scope.myform.visible_form_fields[i]; + if(currField['_id'] == field_id){ + field_index = i; + break; + } + } + } + + if(!$scope.selected){ + $scope.selected = { + _id: '', + index: 0 + } + } + if($scope.selected._id === field_id){ + return; + } $scope.selected._id = field_id; $scope.selected.index = field_index; - - var nb_valid = $filter('formValidity')($scope.myform); - $scope.translateAdvancementData = { - done: nb_valid, - total: form_fields_count, - answers_not_completed: form_fields_count - nb_valid - }; - if(animateScroll){ - NOSCROLL=true; - setTimeout(function() { - $document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() { - NOSCROLL = false; - setTimeout(function() { - if (document.querySelectorAll('.activeField .focusOn').length) { - //Handle default case - document.querySelectorAll('.activeField .focusOn')[0].focus(); - } else if(document.querySelectorAll('.activeField input').length) { - //Handle case for rating input - document.querySelectorAll('.activeField input')[0].focus(); - } else { - //Handle case for dropdown input - document.querySelectorAll('.activeField .selectize-input')[0].focus(); - } - }); - }); + $document.scrollToElement(angular.element('#'+field_id), -10, 300).then(function() { + if (angular.element('#'+field_id+' .focusOn').length) { + //Handle default case + angular.element('#'+field_id+' .focusOn')[0].focus(); + } else if(angular.element('#'+field_id+' input').length) { + //Handle case for rating input + angular.element('#'+field_id+' input')[0].focus(); + } else { + //Handle case for dropdown input + angular.element('#'+field_id+'.selectize-input')[0].focus(); + } }); + } else { + if (angular.element('#'+field_id+' .focusOn').length) { + //Handle default case + angular.element('#'+field_id+' .focusOn')[0].focus(); + } else if(angular.element('#'+field_id+' input').length) { + //Handle case for rating input + angular.element('#'+field_id+' input')[0].focus(); + } else if(angular.element('#'+field_id+'.selectize-input').length) { + //Handle case for dropdown input + angular.element('#'+field_id+'.selectize-input')[0].focus(); + } } }; - $scope.$watch('selected.index', function(oldValue, newValue){ - if(oldValue !== newValue && newValue < $scope.myform.form_fields.length){ - //Only send analytics data if form has not been submitted - if(!$scope.myform.submitted){ - console.log('SendVisitorData.send()'); - SendVisitorData.send($scope.myform, newValue, TimeCounter.getTimeElapsed()); - } - } + $rootScope.$on('duScrollspy:becameActive', function($event, $element, $target){ + $scope.setActiveField($element.prop('id'), null, false); + $scope.updateFormValidity(); + $scope.$apply() + if(!$scope.myform.submitted){ + SendVisitorData.send($scope.myform, $rootScope.getActiveField(), TimeCounter.getTimeElapsed()); + } }); - //Fire event when window is scrolled - $window.onscroll = function(){ - if(!NOSCROLL){ + $rootScope.nextField = $scope.nextField = function(){ + if($scope.selected && $scope.selected.index > -1){ + if($scope.selected._id !== FORM_ACTION_ID){ + var currField = $scope.myform.visible_form_fields[$scope.selected.index]; - var scrollTop = $(window).scrollTop(); - var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect(); - var fieldTop = elemBox.top; - var fieldBottom = elemBox.bottom; - - var field_id, field_index; - var elemHeight = $('.activeField').height(); - - var submitSectionHeight = $('.form-actions').height(); - var maxScrollTop = $(document).height() - $(window).height(); - var fieldWrapperHeight = $('form_fields').height(); - - var selector = 'form > .field-directive:nth-of-type(' + String($scope.myform.visible_form_fields.length - 1)+ ')' - var fieldDirectiveHeight = $(selector).height() - var scrollPosition = maxScrollTop - submitSectionHeight - fieldDirectiveHeight*1.2; - - var fractionToJump = 0.9; - - //Focus on field above submit form button - if($scope.selected.index === $scope.myform.visible_form_fields.length){ - if(scrollTop < scrollPosition){ - field_index = $scope.selected.index-1; - $scope.setActiveField(null, field_index, false); + //Jump to logicJump's destination if it is true + if(currField.logicJump && currField.logicJump.jumpTo && evaluateLogicJump(currField)){ + $scope.setActiveField(currField.logicJump.jumpTo, null, true); + } else if($scope.selected.index < $scope.myform.visible_form_fields.length-1){ + $scope.setActiveField(null, $scope.selected.index+1, true); + } else { + $scope.setActiveField(FORM_ACTION_ID, null, true); } } - - //Focus on submit form button - else if($scope.selected.index === $scope.myform.visible_form_fields.length-1 && scrollTop > scrollPosition){ - field_index = $scope.selected.index+1; - $scope.setActiveField(FORM_ACTION_ID, field_index, false); - } - - //If we scrolled bellow the current field, move to next field - else if(fieldBottom < elemHeight * fractionToJump && $scope.selected.index < $scope.myform.visible_form_fields.length-1 ){ - field_index = $scope.selected.index+1; - $scope.setActiveField(null, field_index, false); - } - //If we scrolled above the current field, move to prev field - else if ( $scope.selected.index !== 0 && fieldTop > elemHeight * fractionToJump) { - field_index = $scope.selected.index-1; - $scope.setActiveField(null, field_index, false); - } + } else { + //If selected is not defined go to the first field + $scope.setActiveField(null, 0, true); } - - $scope.$apply(); - }; - - $rootScope.nextField = $scope.nextField = function(){ - if($scope.selected && $scope.selected.index > -1){ - - if($scope.selected._id !== FORM_ACTION_ID){ - var currField = $scope.myform.visible_form_fields[$scope.selected.index]; - - //Jump to logicJump's destination if it is true - if(currField.logicJump && currField.logicJump.jumpTo && evaluateLogicJump(currField)){ - $scope.setActiveField(currField.logicJump.jumpTo, null, true); - } else if($scope.selected.index < $scope.myform.visible_form_fields.length-1){ - $scope.setActiveField(null, $scope.selected.index+1, true); - } else { - $scope.setActiveField(FORM_ACTION_ID, null, true); - } - } else { - //If we are at the submit actions page, go to the first field - $rootScope.setActiveField(null, 0, true); - } - } else { - //If selected is not defined go to the first field - $rootScope.setActiveField(null, 0, true); - } - }; $rootScope.prevField = $scope.prevField = function(){ - console.log('prevField'); - console.log($scope.selected); - var selected_index = $scope.selected.index - 1; + var selected_index = $scope.selected.index - 1; if($scope.selected.index > 0){ $scope.setActiveField(null, selected_index, true); } }; $rootScope.goToInvalid = $scope.goToInvalid = function() { - var field_id = $('.row.field-directive .ng-invalid.focusOn, .row.field-directive .ng-untouched.focusOn:not(.ng-valid)').first().parents('.row.field-directive').first().attr('data-id'); - $scope.setActiveField(field_id, null, true); - }; + var field_id = $('.ng-invalid, .ng-untouched').first().parents('.row.field-directive').first().attr('id'); + $scope.setActiveField(field_id, null, true); + }; /* ** Form Display Functions @@ -308,109 +253,98 @@ angular.module('view-form').directive('submitFormDirective', ['$http', 'TimeCoun } }; - var getDeviceData = function(){ - var md = new MobileDetect(window.navigator.userAgent); - var deviceType = 'other'; + var getDeviceData = function(){ + var md = new MobileDetect(window.navigator.userAgent); + var deviceType = 'other'; - if (md.tablet()){ - deviceType = 'tablet'; - } else if (md.mobile()) { - deviceType = 'mobile'; - } else if (!md.is('bot')) { - deviceType = 'desktop'; - } + if (md.tablet()){ + deviceType = 'tablet'; + } else if (md.mobile()) { + deviceType = 'mobile'; + } else if (!md.is('bot')) { + deviceType = 'desktop'; + } - return { - type: deviceType, - name: window.navigator.platform - }; - }; + return { + type: deviceType, + name: window.navigator.platform + }; + }; - var getIpAndGeo = function(){ - //Get Ip Address and GeoLocation Data - $.ajaxSetup( { 'async': false } ); - var geoData = $.getJSON('https://freegeoip.net/json/').responseJSON; - $.ajaxSetup( { 'async': true } ); + var getIpAndGeo = function(){ + //Get Ip Address and GeoLocation Data + $.ajaxSetup( { 'async': false } ); + var geoData = $.getJSON('https://freegeoip.net/json/').responseJSON; + $.ajaxSetup( { 'async': true } ); - if(!geoData || !geoData.ip){ - geoData = { - ip: 'Adblocker' - }; - } + if(!geoData || !geoData.ip){ + geoData = { + ip: 'Adblocker' + }; + } - return { - ipAddr: geoData.ip, - geoLocation: { - City: geoData.city, - Country: geoData.country_name - } - }; - }; + return { + ipAddr: geoData.ip, + geoLocation: { + City: geoData.city, + Country: geoData.country_name + } + }; + }; - $rootScope.submitForm = $scope.submitForm = function() { - if($scope.forms.myForm.$invalid){ - $scope.goToInvalid(); - return; - } + $rootScope.submitForm = $scope.submitForm = function() { + if($scope.forms.myForm.$invalid){ + $scope.goToInvalid(); + return; + } - var _timeElapsed = TimeCounter.stopClock(); - $scope.loading = true; + var _timeElapsed = TimeCounter.stopClock(); + $scope.loading = true; - var form = _.cloneDeep($scope.myform); + var form = _.cloneDeep($scope.myform); - var deviceData = getDeviceData(); - form.device = deviceData; + var deviceData = getDeviceData(); + form.device = deviceData; - var geoData = getIpAndGeo(); - form.ipAddr = geoData.ipAddr; - form.geoLocation = geoData.geoLocation; + var geoData = getIpAndGeo(); + form.ipAddr = geoData.ipAddr; + form.geoLocation = geoData.geoLocation; - form.timeElapsed = _timeElapsed; - form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100; - delete form.endPage - delete form.isLive - delete form.provider - delete form.startPage - delete form.visible_form_fields; - delete form.analytics; - delete form.design; - delete form.submissions; - delete form.submitted; - for(var i=0; i < $scope.myform.form_fields.length; i++){ - if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){ - $scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value; - } - - //Get rid of unnessecary attributes for each form field - delete form.form_fields[i].submissionId; - delete form.form_fields[i].disabled; - delete form.form_fields[i].ratingOptions; - delete form.form_fields[i].fieldOptions; - delete form.form_fields[i].logicJump; - delete form.form_fields[i].description; - delete form.form_fields[i].validFieldTypes; - delete form.form_fields[i].fieldType; - - } + form.timeElapsed = _timeElapsed; + form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100; + delete form.endPage; + delete form.isLive; + delete form.provider; + delete form.startPage; + delete form.visible_form_fields; + delete form.analytics; + delete form.design; + delete form.submissions; + delete form.submitted; + for(var i=0; i < $scope.myform.form_fields.length; i++){ + if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){ + $scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value; + } + } - setTimeout(function () { - $scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form) - .success(function (data, status) { - $scope.myform.submitted = true; - $scope.loading = false; - SendVisitorData.send(form, getActiveField(), _timeElapsed); - }) - .error(function (error) { - $scope.loading = false; - console.error(error); - $scope.error = error.message; - }); - }, 500); + setTimeout(function () { + $scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form) + .then(function (data, status) { + $scope.myform.submitted = true; + $scope.loading = false; + SendVisitorData.send($scope.myform, $rootScope.getActiveField(), _timeElapsed); + }, function (error) { + $scope.loading = false; + console.error(error); + $scope.error = error.message; + }); + }, 500); }; //Reload our form - $scope.reloadForm(); + $scope.reloadForm(); } }; } ]); + diff --git a/public/form_modules/forms/base/services/socket.io.client.service.js b/public/form_modules/forms/base/services/socket.io.client.service.js index 74c388e0..ecfdc829 100644 --- a/public/form_modules/forms/base/services/socket.io.client.service.js +++ b/public/form_modules/forms/base/services/socket.io.client.service.js @@ -20,6 +20,7 @@ } else { url = window.location.protocol + '//' + window.location.hostname; } + url = url.replace(/^http/, 'ws'); service.socket = io(url, {'transports': ['websocket', 'polling']}); } diff --git a/public/form_modules/forms/base/views/directiveViews/field/date.html b/public/form_modules/forms/base/views/directiveViews/field/date.html index bb8a9df6..705a38f4 100755 --- a/public/form_modules/forms/base/views/directiveViews/field/date.html +++ b/public/form_modules/forms/base/views/directiveViews/field/date.html @@ -16,7 +16,7 @@
+ ng-click="nextField()"/>
{{ 'Y' | translate }}
@@ -36,12 +37,13 @@
-
-
- - -
-
-
-
-
-
{{ 'BACKGROUND_COLOR' | translate }}
-
-
- -
-
- -
-
-
{{ 'QUESTION_TEXT_COLOR' | translate }}
-
- -
- -
-
- -
-
-
{{ 'ANSWER_TEXT_COLOR' | translate }}
-
- -
- -
-
- -
-
-
{{ 'BTN_BACKGROUND_COLOR' | translate }}
-
- -
- -
-
-
-
-
{{ 'BTN_TEXT_COLOR' | translate }}
-
- -
- -
-
-
- -
- -
-
- -
-
- -
-
-
+
+ +
+
+
+
diff --git a/public/modules/forms/admin/views/adminTabs/configure.html b/public/modules/forms/admin/views/adminTabs/configure.html index f4a971ff..e8b52e02 100644 --- a/public/modules/forms/admin/views/adminTabs/configure.html +++ b/public/modules/forms/admin/views/adminTabs/configure.html @@ -1,2 +1,4 @@ + + \ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/configureTabs/general.html b/public/modules/forms/admin/views/adminTabs/configureTabs/general.html new file mode 100644 index 00000000..5f3f8145 --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/configureTabs/general.html @@ -0,0 +1,106 @@ + +
+
+
+
+

{{ 'FORM_NAME' | translate }}

+
+ +
+ +
+
+ +
+

{{ 'LANGUAGE' | translate }}

+
+ + + {{ $root.langCodeToWord[$select.selected] }} + + + + + + + * {{ 'REQUIRED_FIELD' | translate }} +
+
+ +
+
+

{{ 'FORM_STATUS' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'GA_TRACKING_CODE' | translate }}

+
+ +
+ +
+
+
+
+

{{ 'DISPLAY_FOOTER' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'DISPLAY_START_PAGE' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'DISPLAY_END_PAGE' | translate }}

+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/configureTabs/respondent-notifications.html b/public/modules/forms/admin/views/adminTabs/configureTabs/respondent-notifications.html new file mode 100644 index 00000000..563f147a --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/configureTabs/respondent-notifications.html @@ -0,0 +1,109 @@ +
+
+ +
+ + {{ 'NO_EMAIL_FIELD_WARNING' | translate }} + +
+ +
+
+ + +
+
+

{{ 'ENABLE_RESPONDENT_NOTIFICATIONS' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'SEND_NOTIFICATION_TO' | translate }}

+
+ +
+ + + {{$select.selected.title}} + + + + + + +
+
+ +
+
+

{{ 'REPLY_TO' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'EMAIL_SUBJECT' | translate }}

+
+
+ + +
+ + + +
+
+
+
+
+ +
+
+

{{ 'EMAIL_MESSAGE' | translate }}

+
+ +
+ + +
+ + + + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/configureTabs/self-notifications.html b/public/modules/forms/admin/views/adminTabs/configureTabs/self-notifications.html new file mode 100644 index 00000000..570c0334 --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/configureTabs/self-notifications.html @@ -0,0 +1,103 @@ +
+
+ +
+
+ + +
+ +
+

{{ 'ENABLE_SELF_NOTIFICATIONS' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'SEND_NOTIFICATION_TO' | translate }}

+
+ +
+ +
+
+ +
+
+

{{ 'REPLY_TO' | translate }}

+
+ +
+ + + {{$select.selected.title}} + + + + + + +
+
+ +
+
+

{{ 'EMAIL_SUBJECT' | translate }}

+
+ +
+ + +
+ + + +
+
+
+
+
+ +
+
+

{{ 'EMAIL_MESSAGE' | translate }}

+
+ +
+ + +
+ + + + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/design.html b/public/modules/forms/admin/views/adminTabs/design.html new file mode 100644 index 00000000..06a465bf --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/design.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/share.html b/public/modules/forms/admin/views/adminTabs/share.html new file mode 100644 index 00000000..5e005983 --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/share.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/shareTabs/embed_form.html b/public/modules/forms/admin/views/adminTabs/shareTabs/embed_form.html new file mode 100644 index 00000000..d8b2a7d4 --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/shareTabs/embed_form.html @@ -0,0 +1,27 @@ + +
+
+ +
+ {{ embedCode }} +
+
+
+
+ +
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/adminTabs/shareTabs/share_form.html b/public/modules/forms/admin/views/adminTabs/shareTabs/share_form.html new file mode 100644 index 00000000..b43aecc9 --- /dev/null +++ b/public/modules/forms/admin/views/adminTabs/shareTabs/share_form.html @@ -0,0 +1,23 @@ + +
+
+ + + +
+
+ +
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/directiveViews/form/configure-form.client.view.html b/public/modules/forms/admin/views/directiveViews/form/configure-form.client.view.html index 67397b66..87a2b854 100644 --- a/public/modules/forms/admin/views/directiveViews/form/configure-form.client.view.html +++ b/public/modules/forms/admin/views/directiveViews/form/configure-form.client.view.html @@ -1,136 +1,21 @@
-
-
-
-
-
{{ 'FORM_NAME' | translate }}
-
+ + +
+
+
+
-
- -
-
- -
-
-
{{ 'FORM_STATUS' | translate }}
-
- -
- - - - - -
-
- -
-
{{ 'LANGUAGE' | translate }}
-
- - * {{ 'REQUIRED_FIELD' | translate }} -
-
-
-
-
-
-
{{ 'GA_TRACKING_CODE' | translate }}
-
- -
- -
-
-
-
-
{{ 'DISPLAY_FOOTER' | translate }}
-
- -
- - - -
-
- -
-
-
{{ 'DISPLAY_START_PAGE' | translate }}
-
- -
- - - - - -
-
- -
-
-
{{ 'DISPLAY_END_PAGE' | translate }}
-
- -
- - - - - -
-
-
-
- +
diff --git a/public/modules/forms/admin/views/directiveViews/form/design-form.client.view.html b/public/modules/forms/admin/views/directiveViews/form/design-form.client.view.html new file mode 100644 index 00000000..8c0783c0 --- /dev/null +++ b/public/modules/forms/admin/views/directiveViews/form/design-form.client.view.html @@ -0,0 +1,76 @@ +
+
+
+
+
+
{{ 'BACKGROUND_COLOR' | translate }}
+
+
+ +
+
+ +
+
+
{{ 'QUESTION_TEXT_COLOR' | translate }}
+
+ +
+ +
+
+ +
+
+
{{ 'ANSWER_TEXT_COLOR' | translate }}
+
+ +
+ +
+
+ +
+
+
{{ 'BTN_BACKGROUND_COLOR' | translate }}
+
+ +
+ +
+
+
+
+
{{ 'BTN_TEXT_COLOR' | translate }}
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/directiveViews/form/edit-form.client.view.html b/public/modules/forms/admin/views/directiveViews/form/edit-form.client.view.html index 2a8d8369..119dbf62 100644 --- a/public/modules/forms/admin/views/directiveViews/form/edit-form.client.view.html +++ b/public/modules/forms/admin/views/directiveViews/form/edit-form.client.view.html @@ -1,296 +1,5 @@
- - - - - - + + + + + +
@@ -499,11 +487,12 @@
-
-
+
+
- + @@ -538,13 +527,14 @@
-
-
+
+
- + + {{field.title}} @@ -563,7 +553,6 @@
-
diff --git a/public/modules/forms/admin/views/directiveViews/form/edit-submissions-form.client.view.html b/public/modules/forms/admin/views/directiveViews/form/edit-submissions-form.client.view.html index d33d81d6..966d004a 100644 --- a/public/modules/forms/admin/views/directiveViews/form/edit-submissions-form.client.view.html +++ b/public/modules/forms/admin/views/directiveViews/form/edit-submissions-form.client.view.html @@ -1,5 +1,5 @@ -
-
+
+
{{ 'TOTAL_VIEWS' | translate }} @@ -19,19 +19,19 @@
- {{myform.analytics.visitors.length}} + {{analyticsData.globalStatistics.visits | number:0}}
- {{myform.analytics.submissions}} + {{analyticsData.globalStatistics.responses | number:0}}
- {{myform.analytics.conversionRate | number:0}}% + {{analyticsData.globalStatistics.conversion_rate | number:2}}%
- {{ AverageTimeElapsed | secondsToDateTime | date:'mm:ss'}} + {{analyticsData.globalStatistics.average_time | secondsToDateTime | date:'mm:ss'}}
@@ -58,7 +58,7 @@ {{ 'UNIQUE_VISITS' | translate }}
- {{DeviceStatistics.desktop.visits}} + {{analyticsData.deviceStatistics.desktop.visits | number:0}}
@@ -67,7 +67,7 @@ {{ 'UNIQUE_VISITS' | translate }}
- {{DeviceStatistics.tablet.visits}} + {{analyticsData.deviceStatistics.tablet.visits | number:0}}
@@ -76,7 +76,7 @@ {{ 'UNIQUE_VISITS' | translate }}
- {{DeviceStatistics.tablet.visits}} + {{analyticsData.deviceStatistics.tablet.visits | number:0}}
@@ -85,7 +85,7 @@ {{ 'UNIQUE_VISITS' | translate }}
- {{DeviceStatistics.other.visits}} + {{analyticsData.deviceStatistics.other.visits | number:0}}
@@ -96,7 +96,7 @@ {{ 'RESPONSES' | translate }}
- {{DeviceStatistics.desktop.responses}} + {{analyticsData.deviceStatistics.desktop.responses | number:0}}
@@ -105,7 +105,7 @@ {{ 'RESPONSES' | translate }}
- {{DeviceStatistics.tablet.responses}} + {{analyticsData.deviceStatistics.tablet.responses | number:0}}
@@ -114,7 +114,7 @@ {{ 'RESPONSES' | translate }}
- {{DeviceStatistics.phone.responses}} + {{analyticsData.deviceStatistics.phone.responses | number:0}}
@@ -123,7 +123,7 @@ {{ 'RESPONSES' | translate }}
- {{DeviceStatistics.other.responses}} + {{analyticsData.deviceStatistics.other.responses | number:0}}
@@ -134,7 +134,7 @@ {{ 'COMPLETION_RATE' | translate }}
- {{DeviceStatistics.desktop.completion}}% + {{analyticsData.deviceStatistics.desktop.conversion_rate | number:2}}%
@@ -143,7 +143,7 @@ {{ 'COMPLETION_RATE' | translate }}
- {{DeviceStatistics.tablet.completion}}% + {{analyticsData.deviceStatistics.tablet.conversion_rate | number:2}}%
@@ -152,7 +152,7 @@ {{ 'COMPLETION_RATE' | translate }}
- {{DeviceStatistics.phone.completion}}% + {{analyticsData.deviceStatistics.phone.conversion_rate | number:2}}%
@@ -161,7 +161,7 @@ {{ 'COMPLETION_RATE' | translate }}
- {{DeviceStatistics.other.completion}}% + {{analyticsData.deviceStatistics.other.conversion_rate | number:2}}%
@@ -218,8 +218,8 @@
{{ 'FIELD_DROPOFF' | translate }} +
-
@@ -236,25 +236,28 @@ {{fieldStats.continueRate}}%
+ + +
-
+
-
+
-
+
-
+
diff --git a/public/modules/forms/admin/views/directiveViews/form/share-form.client.view.html b/public/modules/forms/admin/views/directiveViews/form/share-form.client.view.html new file mode 100644 index 00000000..1a143935 --- /dev/null +++ b/public/modules/forms/admin/views/directiveViews/form/share-form.client.view.html @@ -0,0 +1,9 @@ +
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/public/modules/forms/admin/views/list-forms.client.view.html b/public/modules/forms/admin/views/list-forms.client.view.html index cea95d84..dd46fee5 100644 --- a/public/modules/forms/admin/views/list-forms.client.view.html +++ b/public/modules/forms/admin/views/list-forms.client.view.html @@ -52,9 +52,9 @@
{{ 'LANGUAGE' | translate }}
- +
@@ -88,7 +88,7 @@
- {{ form.numberOfResponses }} {{ 'RESPONSES' | translate }} + {{ form.submissionNum }} {{ 'RESPONSES' | translate }}

diff --git a/public/modules/forms/base/css/form.css b/public/modules/forms/base/css/form.css index b285d2df..2edc70d7 100644 --- a/public/modules/forms/base/css/form.css +++ b/public/modules/forms/base/css/form.css @@ -34,6 +34,10 @@ form .btn { border-width: 0px; } +.public-form input[type='radio'] { + display: none; +} + form .btn { border-color: grey; } @@ -225,13 +229,13 @@ form .row.field { font-size:0.8em; } - form .row.field.dropdown > .field-input input { + form .dropdown > .field-input input { min-height: 34px; border-width: 0 0 2px 0; border-radius: 5px; } - form .row.field.dropdown > .field-input input:focus { + form .dropdown > .field-input input:focus { border: none; } diff --git a/public/modules/forms/config/forms.client.config.js b/public/modules/forms/config/forms.client.config.js index dec0f715..dd674c69 100644 --- a/public/modules/forms/config/forms.client.config.js +++ b/public/modules/forms/config/forms.client.config.js @@ -6,36 +6,19 @@ angular.module('forms').run(['Menus', // Set top bar menu items Menus.addMenuItem('topbar', 'My Forms', 'forms', '', '/forms', false); } +]).run(['$rootScope', '$state', + function($rootScope, $state) { + $rootScope.$on('$stateChangeStart', function(evt, to, params) { + if (to.redirectTo) { + evt.preventDefault(); + $state.go(to.redirectTo, params) + } + }); + } ]).filter('secondsToDateTime', [function() { return function(seconds) { - return new Date(1970, 0, 1).setSeconds(seconds); + return new Date(0).setSeconds(seconds); }; -}]).filter('formValidity', [function(){ - return function(formObj){ - if(formObj && formObj.form_fields && formObj.visible_form_fields){ - - //get keys - var formKeys = Object.keys(formObj); - - //we only care about things that don't start with $ - var fieldKeys = formKeys.filter(function(key){ - return key[0] !== '$'; - }); - - var fields = formObj.form_fields; - - var valid_count = fields.filter(function(field){ - if(typeof field === 'object' && field.fieldType !== 'statement' && field.fieldType !== 'rating'){ - return !!(field.fieldValue); - } else if(field.fieldType === 'rating'){ - return true; - } - - }).length; - return valid_count - (formObj.form_fields.length - formObj.visible_form_fields.length); - } - return 0; - }; }]).filter('trustSrc', ['$sce', function($sce){ return function(formUrl){ return $sce.trustAsResourceUrl(formUrl); @@ -46,4 +29,4 @@ angular.module('forms').run(['Menus', directive.replace = true; return $delegate; }); -}]); +}]); \ No newline at end of file diff --git a/public/modules/forms/config/forms.client.routes.js b/public/modules/forms/config/forms.client.routes.js index aa38bde0..41cb2eb6 100644 --- a/public/modules/forms/config/forms.client.routes.js +++ b/public/modules/forms/config/forms.client.routes.js @@ -43,6 +43,7 @@ angular.module('forms').config(['$stateProvider', controller: 'SubmitFormController', controllerAs: 'ctrl' }).state('viewForm', { + abstract: true, url: '/forms/:formId/admin', templateUrl: 'modules/forms/admin/views/admin-form.client.view.html', data: { @@ -63,18 +64,44 @@ angular.module('forms').config(['$stateProvider', }] }, controller: 'AdminFormController' - }).state('viewForm.configure', { - url: '/configure', - templateUrl: 'modules/forms/admin/views/adminTabs/configure.html' - }).state('viewForm.design', { - url: '/design', - templateUrl: 'modules/forms/admin/views/adminTabs/design.html' - }).state('viewForm.analyze', { - url: '/analyze', - templateUrl: 'modules/forms/admin/views/adminTabs/analyze.html' - }).state('viewForm.create', { + }).state('viewForm.create', { url: '/create', templateUrl: 'modules/forms/admin/views/adminTabs/create.html' + }) + + .state('viewForm.configure', { + abstract: true, + url: '/configure', + templateUrl: 'modules/forms/admin/views/adminTabs/configure.html' + }).state('viewForm.configure.general', { + url: '/general', + templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/general.html' + }).state('viewForm.configure.self_notifications', { + url: '/self_notifications', + templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/self-notifications.html' + }).state('viewForm.configure.respondent_notifications', { + url: '/respondent_notifications', + templateUrl: 'modules/forms/admin/views/adminTabs/configureTabs/respondent-notifications.html' + }) + + .state('viewForm.share', { + abstract: true, + url: '/share', + templateUrl: 'modules/forms/admin/views/adminTabs/share.html' + }).state('viewForm.share.share_form', { + url: '/share_form', + templateUrl: 'modules/forms/admin/views/adminTabs/shareTabs/share_form.html' + }).state('viewForm.share.embed_form', { + url: '/embed_form', + templateUrl: 'modules/forms/admin/views/adminTabs/shareTabs/embed_form.html' + }) + + .state('viewForm.design', { + url: '/design', + templateUrl: 'modules/forms/admin/views/adminTabs/design.html' + }).state('viewForm.analyze', { + url: '/analyze', + templateUrl: 'modules/forms/admin/views/adminTabs/analyze.html' }); } ]); diff --git a/public/modules/forms/forms.client.module.js b/public/modules/forms/forms.client.module.js index 07d98366..74379eea 100644 --- a/public/modules/forms/forms.client.module.js +++ b/public/modules/forms/forms.client.module.js @@ -3,5 +3,6 @@ // Use Application configuration module to register a new module ApplicationConfiguration.registerModule('forms', [ 'ngFileUpload', 'ui.date', 'ui.sortable', - 'angular-input-stars', 'users', 'ngclipboard' + 'angular-input-stars', 'users', 'ngclipboard', + 'frapontillo.bootstrap-switch', 'ngQuill' ]);//, 'colorpicker.module' @TODO reactivate this module diff --git a/public/modules/forms/tests/unit/controllers/admin-form.client.controller.test.js b/public/modules/forms/tests/unit/controllers/admin-form.client.controller.test.js index d49d8914..86faa40f 100644 --- a/public/modules/forms/tests/unit/controllers/admin-form.client.controller.test.js +++ b/public/modules/forms/tests/unit/controllers/admin-form.client.controller.test.js @@ -10,7 +10,8 @@ $httpBackend, $stateParams, $location, - $state; + $state, + $timeout; var sampleUser = { firstName: 'Full', @@ -64,7 +65,6 @@ }); })); - // The $resource service augments the response object with methods for updating and deleting the resource. // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. @@ -94,34 +94,32 @@ $provide.service('User', function($q) { return { getCurrent: function() { - var deferred = $q.defer(); - deferred.resolve( JSON.stringify(sampleUser) ); - return deferred.promise; + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; }, login: function(credentials) { - var deferred = $q.defer(); - if( credentials.password === sampleUser.password && credentials.username === sampleUser.username){ - deferred.resolve( JSON.stringify(sampleUser) ); - }else { - deferred.resolve('Error: User could not be loggedin'); - } - - return deferred.promise; + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; }, logout: function() { - var deferred = $q.defer(); - deferred.resolve(null); - return deferred.promise; + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(null); + } + }; }, signup: function(credentials) { - var deferred = $q.defer(); - if( credentials.password === sampleUser.password && credentials.username === sampleUser.username){ - deferred.resolve( JSON.stringify(sampleUser) ); - }else { - deferred.resolve('Error: User could not be signed up'); - } - - return deferred.promise; + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; } }; }); @@ -132,13 +130,14 @@ $provide.service('Auth', function() { return { ensureHasCurrentUser: function() { - return sampleUser; + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; }, isAuthenticated: function() { return true; - }, - getUserState: function() { - return true; } }; }); @@ -175,7 +174,7 @@ // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). // This allows us to inject a service but then attach it to a variable // with the same name as the service. - beforeEach(inject(function($controller, $rootScope, _$state_, _$location_, _$stateParams_, _$httpBackend_, CurrentForm, Forms) { + beforeEach(inject(function($controller, $rootScope, _$state_, _$location_, _$stateParams_, _$httpBackend_, CurrentForm, Forms, _$timeout_) { // Set a new global scope scope = $rootScope.$new(); @@ -187,6 +186,7 @@ $httpBackend = _$httpBackend_; $location = _$location_; $state = _$state_; + $timeout = _$timeout_; $httpBackend.whenGET(/\.html$/).respond(''); $httpBackend.whenGET('/users/me/').respond(''); @@ -197,60 +197,70 @@ }; })); - it('AdminFormController should fetch current Form when instantiated', function() { - // Run controller functionality - var controller = createAdminFormController(); + it('AdminFormController should fetch current Form when instantiated', inject(function($timeout) { + $timeout(function() { + // Run controller functionality + var controller = createAdminFormController(); - // Test scope value - expect(scope.myform).toEqualData(sampleForm); - }); - - it('$scope.removeCurrentForm() with valid form data should send a DELETE request with the id of form', inject(function($uibModal) { - var controller = createAdminFormController(); - - //Set $state transition - $state.expectTransitionTo('listForms'); - - // Set DELETE response - $httpBackend.expect('DELETE', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); - - //Run controller functionality - scope.openDeleteModal(); - scope.removeCurrentForm(); - - $httpBackend.flush(); - $state.ensureAllTransitionsHappened(); + // Test scope value + expect(scope.myform).toEqualData(sampleForm); + }); })); - it('$scope.update() should send a PUT request with the id of form', function() { - var controller = createAdminFormController(); + it('$scope.removeCurrentForm() with valid form data should send a DELETE request with the id of form', inject(function($timeout, $uibModal) { + $timeout(function() { + var controller = createAdminFormController(); - //Set PUT response - $httpBackend.expect('PUT', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); + //Set $state transition + $state.expectTransitionTo('listForms'); - //Run controller functionality - scope.update(false, sampleForm, false, false); + // Set DELETE response + $httpBackend.expect('DELETE', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); - $httpBackend.flush(); - }); + //Run controller functionality + scope.openDeleteModal(); + scope.removeCurrentForm(); - it('$scope.openDeleteModal() should open scope.deleteModal', function() { - var controller = createAdminFormController(); + $httpBackend.flush(); + $state.ensureAllTransitionsHappened(); + }); + })); - //Run controller functionality - scope.openDeleteModal(); - expect(scope.deleteModal.opened).toEqual(true); - }); + it('$scope.update() should send a PUT request with the id of form', inject(function($timeout) { + $timeout(function() { + var controller = createAdminFormController(); - it('$scope.cancelDeleteModal() should close $scope.deleteModal', inject(function($uibModal) { - var controller = createAdminFormController(); + //Set PUT response + $httpBackend.expect('PUT', /^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); - //Run controller functionality - scope.openDeleteModal(); + //Run controller functionality + scope.update(false, sampleForm, false, false); - //Run controller functionality - scope.cancelDeleteModal(); - expect( scope.deleteModal.opened ).toEqual(false); + $httpBackend.flush(); + }); + })); + + it('$scope.openDeleteModal() should open scope.deleteModal', inject(function($timeout) { + $timeout(function() { + var controller = createAdminFormController(); + + //Run controller functionality + scope.openDeleteModal(); + expect(scope.deleteModal.opened).toEqual(true); + }); + })); + + it('$scope.cancelDeleteModal() should close $scope.deleteModal', inject(function($uibModal, $timeout) { + $timeout(function() { + var controller = createAdminFormController(); + + //Run controller functionality + scope.openDeleteModal(); + + //Run controller functionality + scope.cancelDeleteModal(); + expect( scope.deleteModal.opened ).toEqual(false); + }); })); }); }()); diff --git a/public/modules/forms/tests/unit/controllers/list-forms.client.controller.test.js b/public/modules/forms/tests/unit/controllers/list-forms.client.controller.test.js index 2190264b..1ebd19ac 100644 --- a/public/modules/forms/tests/unit/controllers/list-forms.client.controller.test.js +++ b/public/modules/forms/tests/unit/controllers/list-forms.client.controller.test.js @@ -30,7 +30,8 @@ {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} ], - _id: '525a8422f6d0f87f0e407a33' + _id: '525a8422f6d0f87f0e407a33', + submissionNum: 0 },{ title: 'Form Title2', admin: '39223933b1f1dea0ce12fab9', @@ -40,7 +41,8 @@ {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} ], - _id: '52f6d0f87f5a407a384220e3' + _id: '52f6d0f87f5a407a384220e3', + submissionNum: 0 },{ title: 'Form Title3', admin: '2fab9ed873937f0e1dea0ce1', @@ -50,7 +52,8 @@ {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} ], - _id: '922f6d0f87fed8730e4e1233' + _id: '922f6d0f87fed8730e4e1233', + submissionNum: 0 } ]; @@ -102,6 +105,7 @@ $httpBackend.whenGET(/\.html$/).respond(''); $httpBackend.whenGET('/users/me/').respond(''); + $httpBackend.whenGET('/forms').respond(200, sampleFormList); // Initialize the Forms controller. createListFormsController = function(){ @@ -188,6 +192,5 @@ $httpBackend.flush(); $state.ensureAllTransitionsHappened(); })); - }); }()); diff --git a/public/modules/forms/tests/unit/controllers/submit-form.client.controller.test.js b/public/modules/forms/tests/unit/controllers/submit-form.client.controller.test.js deleted file mode 100644 index 574b97ae..00000000 --- a/public/modules/forms/tests/unit/controllers/submit-form.client.controller.test.js +++ /dev/null @@ -1,165 +0,0 @@ -'use strict'; - -(function() { - // Forms Controller Spec - describe('SubmitForm Controller Tests', function() { - // Initialize global variables - var SubmitFormController, - createSubmitFormController, - scope, - $httpBackend, - $stateParams, - $location, - $state, - vm; - - var sampleUser = { - firstName: 'Full', - lastName: 'Name', - email: 'test@test.com', - username: 'test@test.com', - password: 'password', - provider: 'local', - roles: ['user'], - _id: 'ed873933b1f1dea0ce12fab9' - }; - - var sampleForm = { - title: 'Form Title', - admin: 'ed873933b1f1dea0ce12fab9', - language: 'english', - form_fields: [ - {'fieldType':'textfield', 'title':'First Name', 'fieldValue': '', 'deletePreserved': false}, - {'fieldType':'checkbox', 'title':'nascar', 'fieldValue': '', 'deletePreserved': false}, - {'fieldType':'checkbox', 'title':'hockey', 'fieldValue': '', 'deletePreserved': false} - ], - isLive: false, - _id: '525a8422f6d0f87f0e407a33', - visible_form_fields: [ - {'fieldType':'textfield', 'title':'First Name', 'fieldValue': '', 'deletePreserved': false}, - {'fieldType':'checkbox', 'title':'nascar', 'fieldValue': '', 'deletePreserved': false}, - {'fieldType':'checkbox', 'title':'hockey', 'fieldValue': '', 'deletePreserved': false} - ] - }; - - // Load the main application module - beforeEach(function(){ - module('view-form'), - module(ApplicationConfiguration.applicationModuleName) - }); - beforeEach(module('module-templates')); - beforeEach(module('stateMock')); - - //Mock Users Service - beforeEach(module(function($provide) { - $provide.service('myForm', function($q) { - var deferred = $q.defer(); - deferred.resolve(sampleForm); - - return deferred.promise; - }); - })); - - //Mock Authentication Service - beforeEach(module(function($provide) { - $provide.service('Auth', function() { - return { - ensureHasCurrentUser: function() { - return sampleUser; - }, - isAuthenticated: function() { - return true; - }, - getUserState: function() { - return true; - } - }; - }); - })); - - //Mock Users Service - beforeEach(module(function($provide) { - $provide.service('User', function($q) { - return { - getCurrent: function() { - var deferred = $q.defer(); - deferred.resolve( JSON.stringify(sampleUser) ); - return deferred.promise; - }, - login: function(credentials) { - var deferred = $q.defer(); - if( credentials.password === sampleUser.password && credentials.username === sampleUser.username){ - deferred.resolve( JSON.stringify(sampleUser) ); - }else { - deferred.resolve('Error: User could not be loggedin'); - } - - return deferred.promise; - }, - logout: function() { - var deferred = $q.defer(); - deferred.resolve(null); - return deferred.promise; - }, - signup: function(credentials) { - var deferred = $q.defer(); - if( credentials.password === sampleUser.password && credentials.username === sampleUser.username){ - deferred.resolve( JSON.stringify(sampleUser) ); - }else { - deferred.resolve('Error: User could not be signed up'); - } - - return deferred.promise; - } - }; - }); - })); - - - // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). - // This allows us to inject a service but then attach it to a variable - // with the same name as the service. - beforeEach(inject(function($controller, $rootScope, _$state_, _$location_, _$stateParams_, _$httpBackend_, CurrentForm) { - // Set a new global scope - scope = $rootScope.$new(); - - //Set CurrentForm - CurrentForm.setForm(sampleForm); - - // Point global variables to injected services - $stateParams = _$stateParams_; - $httpBackend = _$httpBackend_; - $location = _$location_; - $state = _$state_; - - $httpBackend.whenGET('/users/me/').respond(''); - - // Initialize the Forms controller. - createSubmitFormController = function(){ - return $controller('SubmitFormController', { $scope: scope }); - }; - - vm = createSubmitFormController(); - })); - - /* - - //FIX ME: Need to get thi sto work with view-form dependency - it('on controller instantiation it should populate $scope.myform with current Form', inject(function() { - - //var controller = createSubmitFormController(); - - console.log(vm); - $stateParams.formId = '525a8422f6d0f87f0e407a33'; - - // Set GET response - $httpBackend.expectGET(/^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); - - // Test scope value - expect( scope.myform ).toEqualData(sampleForm); - expect( scope.hideNav ).toEqual(false); - })); - - */ - }); -}()); diff --git a/public/modules/forms/tests/unit/directives/configure-form.client.directive.test.js b/public/modules/forms/tests/unit/directives/configure-form.client.directive.test.js index d4388481..af3e20b5 100644 --- a/public/modules/forms/tests/unit/directives/configure-form.client.directive.test.js +++ b/public/modules/forms/tests/unit/directives/configure-form.client.directive.test.js @@ -26,15 +26,11 @@ {fieldType:'checkbox', title:'nascar', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed83b0ce121f17393deafab9'}, {fieldType:'checkbox', title:'hockey', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed8317393deab0ce121ffab9'} ], - pdf: {}, - pdfFieldMap: {}, startPage: { showStart: false }, - hideFooter: false, - isGenerated: false, + showFooter: false, isLive: false, - autofillPDFs: false, _id: '525a8422f6d0f87f0e407a33' }; diff --git a/public/modules/forms/tests/unit/directives/edit-form-submissions.client.directive.test.js b/public/modules/forms/tests/unit/directives/edit-form-submissions.client.directive.test.js index 75ac6b0d..ec3d5076 100644 --- a/public/modules/forms/tests/unit/directives/edit-form-submissions.client.directive.test.js +++ b/public/modules/forms/tests/unit/directives/edit-form-submissions.client.directive.test.js @@ -59,8 +59,7 @@ endPage: { showEnd: false }, - hideFooter: false, - isGenerated: false, + showFooter: false, isLive: false, _id: '525a8422f6d0f87f0e407a33' }; @@ -141,8 +140,7 @@ sampleForm.submissions = sampleSubmissions; $httpBackend.whenGET('/users/me/').respond(''); - $httpBackend.whenGET(/^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); - $httpBackend.whenGET('/forms').respond(200, sampleForm); + $httpBackend.whenGET('/forms').respond(200, [sampleForm]); $httpBackend.whenGET(/^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); $httpBackend.whenGET(/^(\/forms\/)([0-9a-fA-F]{24})\/submissions$/).respond(200, sampleSubmissions); $httpBackend.whenGET(/^(\/forms\/)([0-9a-fA-F]{24})\/visitors$/).respond(200, sampleVisitors); diff --git a/public/modules/forms/tests/unit/directives/edit-form.client.directive.test.js b/public/modules/forms/tests/unit/directives/edit-form.client.directive.test.js index 5ad0047b..b2c2282a 100644 --- a/public/modules/forms/tests/unit/directives/edit-form.client.directive.test.js +++ b/public/modules/forms/tests/unit/directives/edit-form.client.directive.test.js @@ -26,14 +26,11 @@ {fieldType:'checkbox', title:'nascar', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed83b0ce121f17393deafab9'}, {fieldType:'checkbox', title:'hockey', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed8317393deab0ce121ffab9'} ], - pdf: {}, - pdfFieldMap: {}, startPage: { showStart: false, buttons: [] }, - hideFooter: false, - isGenerated: false, + showFooter: false, isLive: false, _id: '525a8422f6d0f87f0e407a33' }; @@ -109,40 +106,39 @@ })); beforeEach(inject(function($compile, $controller, $rootScope, _$httpBackend_) { + // Point global variables to injected services + $httpBackend = _$httpBackend_; + + $httpBackend.whenGET('/users/me/').respond(''); + $httpBackend.whenGET('/forms').respond(200, [sampleForm]); + $httpBackend.whenGET(/^(\/forms\/)([0-9a-fA-F]{24})$/).respond(200, sampleForm); + //Instantiate directive. var tmp_scope = $rootScope.$new(); tmp_scope.myform = _.cloneDeep(sampleForm); + tmp_scope.user = _.cloneDeep(sampleUser) //gotacha: Controller and link functions will execute. el = angular.element(''); $compile(el)(tmp_scope); $rootScope.$digest(); - // Point global variables to injected services - $httpBackend = _$httpBackend_; - - //$httpBackend.whenGET(/.+\.html$/).respond(''); - $httpBackend.whenGET('/users/me/').respond(''); - //Grab controller instance controller = el.controller(); //Grab scope. Depends on type of scope. //See angular.element documentation. scope = el.isolateScope() || el.scope(); - - scope.update = function(updateImmediately, data, isDiffed, refreshAfterUpdate, cb){ - if(cb) cb(); - }; - + scope.update = function(updateImmediately, data, isDiffed, refreshAfterUpdate, cb){ + if(cb) cb(); + }; })); describe('> Form Field >',function(){ - - beforeEach(function(){ - scope.myform = _.cloneDeep(sampleForm); - }); - + beforeEach(function(){ + scope.myform = _.cloneDeep(sampleForm); + }) + it('$scope.addNewField() should open the new field modal', function() { //Run controller methods @@ -152,9 +148,10 @@ }); it('$scope.deleteField() should DELETE a field to $scope.myform.form_fields', function() { - spyOn(scope, 'update'); + expect(scope.myform.form_fields).toEqualData(sampleForm.form_fields); + //Run controller methods scope.deleteField(0); diff --git a/public/modules/forms/tests/unit/directives/field-icon.client.directive.test.js b/public/modules/forms/tests/unit/directives/field-icon.client.directive.test.js index 7dfc6823..728e16db 100644 --- a/public/modules/forms/tests/unit/directives/field-icon.client.directive.test.js +++ b/public/modules/forms/tests/unit/directives/field-icon.client.directive.test.js @@ -97,7 +97,9 @@ currType = FormFields.types[i]; currClass = faClasses[currType.name]; - var element = $compile('')(scope); + scope.currType = currType; + + var element = $compile('')(scope); scope.$digest(); expect(currClass).toBeDefined(); diff --git a/public/modules/forms/tests/unit/directives/submit-form.client.directive.test.js b/public/modules/forms/tests/unit/directives/submit-form.client.directive.test.js index f3c055a4..176b77db 100644 --- a/public/modules/forms/tests/unit/directives/submit-form.client.directive.test.js +++ b/public/modules/forms/tests/unit/directives/submit-form.client.directive.test.js @@ -46,16 +46,13 @@ var MobileDetect = function(userAgentStr){ visible_form_fields: [ {fieldType:'textfield', title:'First Name', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed873933b0ce121f1deafab9'}, {fieldType:'checkbox', title:'nascar', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed83b0ce121f17393deafab9'}, - {fieldType:'checkbox', title:'hockey', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed8317393deab0ce121ffab9'} ], - pdf: {}, - pdfFieldMap: {}, + {fieldType:'checkbox', title:'hockey', fieldOptions: [], fieldValue: '', required: true, disabled: false, deletePreserved: false, _id: 'ed8317393deab0ce121ffab9'} + ], startPage: { showStart: false }, - hideFooter: false, - isGenerated: false, + showFooter: false, isLive: false, - autofillPDFs: false, _id: '525a8422f6d0f87f0e407a33' }; @@ -130,7 +127,7 @@ var MobileDetect = function(userAgentStr){ // Point global variables to injected services $httpBackend = _$httpBackend_; $httpBackend.whenGET('/users/me/').respond(''); - + $httpBackend.whenGET('/forms').respond(''); //Instantiate directive. var tmp_scope = $rootScope.$new(); diff --git a/public/modules/forms/tests/unit/services/time-counter.client.service.test.js b/public/modules/forms/tests/unit/services/time-counter.client.service.test.js index 9eb61b1e..d1dea7dd 100644 --- a/public/modules/forms/tests/unit/services/time-counter.client.service.test.js +++ b/public/modules/forms/tests/unit/services/time-counter.client.service.test.js @@ -18,11 +18,13 @@ var timeSpent = 0; TimeCounter.restartClock(); - setTimeout(function(){ + // TODO: David - come up with a better way to test this that is time-efficient + /*setTimeout(function(){ timeSpent = TimeCounter.stopClock(); - expect(timeSpent).toEqual(1); - },1000); - + expect(timeSpent).toBeGreaterThanOrEqual(3); + done(); + }, 3000); + */ }); }); }()); \ No newline at end of file diff --git a/public/modules/forms/tests/unit/stateMock.js b/public/modules/forms/tests/unit/stateMock.js index 3906ea8a..8a354476 100644 --- a/public/modules/forms/tests/unit/stateMock.js +++ b/public/modules/forms/tests/unit/stateMock.js @@ -9,7 +9,7 @@ angular.module('stateMock').service('$state', function($q){ if(expectedState !== stateName){ throw Error('Expected transition to state: ' + expectedState + ' but transitioned to ' + stateName ); } - }else{ + } else { throw Error('No more transitions were expected! Tried to transition to '+ stateName ); } var deferred = $q.defer(); diff --git a/public/modules/users/config/users.client.routes.js b/public/modules/users/config/users.client.routes.js index b51f4771..ac8f6ff9 100755 --- a/public/modules/users/config/users.client.routes.js +++ b/public/modules/users/config/users.client.routes.js @@ -5,25 +5,26 @@ angular.module('users').config(['$stateProvider', function($stateProvider) { var checkLoggedin = function($q, $timeout, $state, User, Auth) { - var deferred = $q.defer(); - - if (Auth.currentUser && Auth.currentUser.email) { - $timeout(deferred.resolve); + var deferred = $q.defer(); + + if (Auth.currentUser && Auth.currentUser.email) { + return; } else { - Auth.currentUser = User.getCurrent( - function() { - Auth.login(); - $timeout(deferred.resolve()); - }, - function() { - Auth.logout(); + return User.getCurrent().then( + function(user) { + Auth.login(user); + return; + }, + function() { + Auth.logout(); $timeout(deferred.reject()); - $state.go('signin', {reload: true}); - }); - } + $state.go('signin', {reload: true}); + return; + }); + } + - return deferred.promise; }; var checkSignupDisabled = function($window, $timeout, $q) { @@ -40,24 +41,19 @@ angular.module('users').config(['$stateProvider', $stateProvider. state('profile', { resolve: { - loggedin: checkLoggedin + currentUser: ['$q', '$timeout', '$state', 'User', 'Auth', checkLoggedin] }, url: '/settings/profile', - templateUrl: 'modules/users/views/settings/edit-profile.client.view.html' + templateUrl: 'modules/users/views/settings/edit-profile.client.view.html', + controller: 'SettingsController' }). state('password', { resolve: { - loggedin: checkLoggedin - }, + currentUser: ['$q', '$timeout', '$state', 'User', 'Auth', checkLoggedin] + }, url: '/settings/password', - templateUrl: 'modules/users/views/settings/change-password.client.view.html' - }). - state('accounts', { - resolve: { - loggedin: checkLoggedin - }, - url: '/settings/accounts', - templateUrl: 'modules/users/views/settings/social-accounts.client.view.html' + templateUrl: 'modules/users/views/settings/change-password.client.view.html', + controller: 'SettingsController' }). state('signup', { resolve: { diff --git a/public/modules/users/controllers/authentication.client.controller.js b/public/modules/users/controllers/authentication.client.controller.js index c0b1d5a3..22dbaee5 100755 --- a/public/modules/users/controllers/authentication.client.controller.js +++ b/public/modules/users/controllers/authentication.client.controller.js @@ -1,46 +1,45 @@ 'use strict'; -angular.module('users').controller('AuthenticationController', ['$scope', '$location', '$state', '$rootScope', 'User', 'Auth', '$translate', '$window', - function($scope, $location, $state, $rootScope, User, Auth, $translate, $window) { - - $scope = $rootScope; - $scope.credentials = {}; +angular.module('users').controller('AuthenticationController', ['$scope', '$location', '$state', '$rootScope', 'User', 'Auth', + function($scope, $location, $state, $rootScope, User, Auth) { + + //This helps us test the controller by allowing tests to inject their own scope variables + if(!$scope.credentials) $scope.credentials = {}; + if(!$scope.forms) $scope.forms = {}; + $scope.error = ''; - $scope.forms = {}; var statesToIgnore = ['', 'home', 'signin', 'resendVerifyEmail', 'verify', 'signup', 'signup-success', 'forgot', 'reset-invalid', 'reset', 'reset-success']; $scope.signin = function() { - if(!$scope.forms.signinForm.$invalid){ + if($scope.credentials.hasOwnProperty('username') && $scope.forms.hasOwnProperty('signinForm') && $scope.forms.signinForm.$valid){ User.login($scope.credentials).then( - function(response) { - Auth.login(response); - $scope.user = $rootScope.user = Auth.ensureHasCurrentUser(); + function(currUser) { + Auth.login(currUser); + $rootScope.user = $scope.user = currUser; - if(statesToIgnore.indexOf($state.previous.state.name) === -1) { + if($state.previous && statesToIgnore.indexOf($state.previous.state.name) === -1) { $state.go($state.previous.state.name, $state.previous.params); } else { $state.go('listForms'); } }, function(error) { - $rootScope.user = Auth.ensureHasCurrentUser(); - $scope.user = $rootScope.user; - $scope.error = error; console.error('loginError: '+error); } ); - } + } }; $scope.signup = function() { - if($scope.credentials === 'admin'){ + //TODO - David : need to put this somewhere more appropriate + if($scope.credentials.username === 'admin'){ $scope.error = 'Username cannot be \'admin\'. Please pick another username.'; return; } - if(!$scope.forms.signupForm.$invalid){ + if($scope.credentials && $scope.forms.hasOwnProperty('signupForm') && $scope.forms.signupForm.$valid){ User.signup($scope.credentials).then( function(response) { $state.go('signup-success'); diff --git a/public/modules/users/controllers/change_password.client.controller.js b/public/modules/users/controllers/change_password.client.controller.js new file mode 100644 index 00000000..4d2152e3 --- /dev/null +++ b/public/modules/users/controllers/change_password.client.controller.js @@ -0,0 +1,27 @@ +'use strict'; + +angular.module('users').controller('ChangePasswordController', ['$scope', '$rootScope', '$http', '$state', 'Users', 'Auth', + function($scope, $rootScope, $http, $state, Users, Auth) { + $scope.user = Auth.currentUser; + console.log($scope.user); + + $scope.cancel = function(){ + $scope.user = Auth.currentUser; + }; + + // Change user password + $scope.changeUserPassword = function() { + $scope.success = $scope.error = null; + + $http.post('/users/password', $scope.passwordDetails).success(function(response) { + // If successful show success message and clear form + $scope.success = true; + $scope.error = null; + $scope.passwordDetails = null; + }).error(function(response) { + $scope.success = null; + $scope.error = response.message; + }); + }; + } +]); diff --git a/public/modules/users/controllers/password.client.controller.js b/public/modules/users/controllers/password.client.controller.js index 2744bdc3..d66b369e 100755 --- a/public/modules/users/controllers/password.client.controller.js +++ b/public/modules/users/controllers/password.client.controller.js @@ -1,11 +1,10 @@ 'use strict'; -angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$state', 'User', '$translate', '$window', - function($scope, $stateParams, $state, User, $translate, $window) { - $translate.use($window.locale); +angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$state', 'User', + function($scope, $stateParams, $state, User) { $scope.error = ''; - $scope.forms = {}; + if(!$scope.forms) $scope.forms = {}; // Submit forgotten password account id $scope.askForPasswordReset = function() { @@ -25,7 +24,7 @@ angular.module('users').controller('PasswordController', ['$scope', '$stateParam // Change user password $scope.resetUserPassword = function() { - if(!$scope.forms.resetPasswordForm.$invalid){ + if($scope.forms.hasOwnProperty('resetPasswordForm') && $scope.forms.resetPasswordForm.$valid){ $scope.success = $scope.error = null; User.resetPassword($scope.passwordDetails, $stateParams.token).then( function(response){ diff --git a/public/modules/users/controllers/settings.client.controller.js b/public/modules/users/controllers/settings.client.controller.js index fef50189..3b26c04b 100755 --- a/public/modules/users/controllers/settings.client.controller.js +++ b/public/modules/users/controllers/settings.client.controller.js @@ -5,42 +5,10 @@ angular.module('users').controller('SettingsController', ['$scope', '$rootScope' $scope.user = Auth.currentUser; - // Check if there are additional accounts - $scope.hasConnectedAdditionalSocialAccounts = function(provider) { - for (var i in $scope.user.additionalProvidersData) { - return true; - } - return false; - }; - $scope.cancel = function(){ $scope.user = Auth.currentUser; }; - // Check if provider is already in use with current user - $scope.isConnectedSocialAccount = function(provider) { - return $scope.user.provider === provider || ($scope.user.additionalProvidersData && $scope.user.additionalProvidersData[provider]); - }; - - // Remove a user social account - $scope.removeUserSocialAccount = function(provider) { - $scope.success = $scope.error = null; - - $http.delete('/users/accounts', { - params: { - provider: provider - } - }).success(function(response) { - // If successful show success message and clear form - $scope.success = true; - $scope.error = null; - $scope.user = response; - }).error(function(response) { - $scope.success = null; - $scope.error = response.message; - }); - }; - // Update a user profile $scope.updateUserProfile = function(isValid) { if (isValid) { @@ -51,6 +19,7 @@ angular.module('users').controller('SettingsController', ['$scope', '$rootScope' $scope.success = true; $scope.error = null; $scope.user = response; + $scope.$apply(); }, function(response) { $scope.success = null; $scope.error = response.data.message; @@ -59,21 +28,5 @@ angular.module('users').controller('SettingsController', ['$scope', '$rootScope' $scope.submitted = true; } }; - - // Change user password - $scope.changeUserPassword = function() { - $scope.success = $scope.error = null; - - $http.post('/users/password', $scope.passwordDetails).success(function(response) { - // If successful show success message and clear form - $scope.success = true; - $scope.error = null; - $scope.passwordDetails = null; - }).error(function(response) { - $scope.success = null; - $scope.error = response.message; - }); - }; - } ]); diff --git a/public/modules/users/controllers/verify.client.controller.js b/public/modules/users/controllers/verify.client.controller.js index 1c7ec933..80ca6e90 100644 --- a/public/modules/users/controllers/verify.client.controller.js +++ b/public/modules/users/controllers/verify.client.controller.js @@ -1,35 +1,34 @@ 'use strict'; -angular.module('users').controller('VerifyController', ['$scope', '$state', '$rootScope', 'User', 'Auth', '$stateParams', '$translate', '$window', - function($scope, $state, $rootScope, User, Auth, $stateParams, $translate, $window) { - $translate.use($window.locale); - +angular.module('users').controller('VerifyController', ['$scope', '$state', '$rootScope', 'User', 'Auth', '$stateParams', + function($scope, $state, $rootScope, User, Auth, $stateParams) { $scope.isResetSent = false; - $scope.credentials = {}; + if(!$scope.credentials) $scope.credentials = {}; $scope.error = ''; // Submit forgotten password account id $scope.resendVerifyEmail = function() { - User.resendVerifyEmail($scope.credentials.email).then( - function(response){ - $scope.success = response.message; - $scope.error = null; - $scope.credentials = null; - $scope.isResetSent = true; - }, - function(error){ - $scope.error = error; - $scope.success = null; - $scope.credentials.email = null; - $scope.isResetSent = false; - } - ); + if($scope.credentials.hasOwnProperty('email')){ + User.resendVerifyEmail($scope.credentials.email).then( + function(response){ + $scope.success = response.message; + $scope.error = null; + $scope.credentials = null; + $scope.isResetSent = true; + }, + function(error){ + $scope.error = error.message || error; + $scope.success = null; + $scope.credentials.email = null; + $scope.isResetSent = false; + } + ); + } }; //Validate Verification Token $scope.validateVerifyToken = function() { if($stateParams.token){ - console.log($stateParams.token); User.validateVerifyToken($stateParams.token).then( function(response){ $scope.success = response.message; @@ -47,4 +46,4 @@ angular.module('users').controller('VerifyController', ['$scope', '$state', '$ro } }; } -]); \ No newline at end of file +]); diff --git a/public/modules/users/services/auth.client.service.js b/public/modules/users/services/auth.client.service.js index 2c315001..48ea544f 100644 --- a/public/modules/users/services/auth.client.service.js +++ b/public/modules/users/services/auth.client.service.js @@ -1,7 +1,7 @@ 'use strict'; -angular.module('users').factory('Auth', ['$window', 'User', - function($window, User) { +angular.module('users').factory('Auth', ['$window', '$q', 'User', + function($window, $q, User) { var userState = { isLoggedIn: false @@ -17,45 +17,49 @@ angular.module('users').factory('Auth', ['$window', 'User', // because that would create a circular dependency // Auth <- $http <- $resource <- LoopBackResource <- User <- Auth ensureHasCurrentUser: function() { - if (service._currentUser && service._currentUser.username) { - return service._currentUser; - } else if ($window.user){ - service._currentUser = $window.user; - return service._currentUser; + var deferred = $q.defer(); + + if (this._currentUser && this._currentUser.username) { + deferred.resolve(this._currentUser); + } else if ($window.user) { + this._currentUser = $window.user; + deferred.resolve(this._currentUser) } else { - User.getCurrent().then(function(user) { - // success - service._currentUser = user; + var that = this; + User.getCurrent().then(function(fetchedUser) { + that._currentUser = fetchedUser; + $window.user = fetchedUser; userState.isLoggedIn = true; - $window.user = service._currentUser; - return service._currentUser; + deferred.resolve(fetchedUser); }, function(response) { - userState.isLoggedIn = false; - service._currentUser = null; + that._currentUser = null; $window.user = null; - return null; + userState.isLoggedIn = false; + deferred.reject('User data could not be fetched from server'); }); } + + return deferred.promise; }, isAuthenticated: function() { - return !!service._currentUser; - }, - - getUserState: function() { - return userState; + return !!this._currentUser && this._currentUser.username; }, login: function(new_user) { userState.isLoggedIn = true; - service._currentUser = new_user; + this._currentUser = new_user; + }, + + update: function(new_user) { + this._currentUser = new_user; }, logout: function() { $window.user = null; userState.isLoggedIn = false; - service._currentUser = null; + this._currentUser = null; } }; return service; diff --git a/public/modules/users/services/user.client.service.js b/public/modules/users/services/user.client.service.js index cf745fa7..2bec0664 100644 --- a/public/modules/users/services/user.client.service.js +++ b/public/modules/users/services/user.client.service.js @@ -6,19 +6,16 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' var userService = { getCurrent: function() { var deferred = $q.defer(); - $http.get('/users/me') - .success(function(response) { - deferred.resolve(response); - }) - .error(function() { + .then(function(response) { + deferred.resolve(response.data); + }, function() { deferred.reject('User\'s session has expired'); }); return deferred.promise; }, login: function(credentials) { - var deferred = $q.defer(); $http.post('/auth/signin', credentials).then(function(response) { deferred.resolve(response.data); @@ -29,7 +26,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' return deferred.promise; }, logout: function() { - var deferred = $q.defer(); $http.get('/auth/signout').then(function(response) { deferred.resolve(null); @@ -40,7 +36,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' return deferred.promise; }, signup: function(credentials) { - var deferred = $q.defer(); $http.post('/auth/signup', credentials).then(function(response) { // If successful we assign the response to the global user model @@ -53,7 +48,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' }, resendVerifyEmail: function(_email) { - var deferred = $q.defer(); $http.post('/auth/verify', {email: _email}).then(function(response) { deferred.resolve(response.data); @@ -65,7 +59,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' }, validateVerifyToken: function(token) { - //DAVID: TODO: The valid length of a token should somehow be linked to server config values //DAVID: TODO: SEMI-URGENT: Should we even be doing this? var validTokenRe = /^([A-Za-z0-9]{48})$/g; @@ -82,7 +75,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' }, resetPassword: function(passwordDetails, token) { - var deferred = $q.defer(); $http.post('/auth/reset/'+token, passwordDetails).then(function(response) { deferred.resolve(response); @@ -95,7 +87,6 @@ angular.module('users').factory('User', ['$window', '$q', '$timeout', '$http', ' // Submit forgotten password account id askForPasswordReset: function(credentials) { - var deferred = $q.defer(); $http.post('/auth/forgot', credentials).then(function(response) { // Show user success message and clear form diff --git a/public/modules/users/services/users.client.service.js b/public/modules/users/services/users.client.service.js index 664828f0..642dbcc7 100755 --- a/public/modules/users/services/users.client.service.js +++ b/public/modules/users/services/users.client.service.js @@ -1,9 +1,9 @@ 'use strict'; // Users service used for communicating with the users REST endpoint -angular.module('users').factory('Users', ['$resource', - function($resource) { - return $resource('users', {}, { +angular.module('users').factory('Users', ['$resource', 'USERS_URL', + function($resource, USERS_URL) { + return $resource(USERS_URL, {}, { update: { method: 'PUT' } diff --git a/public/modules/users/tests/unit/controllers/authentication.client.controller.test.js b/public/modules/users/tests/unit/controllers/authentication.client.controller.test.js new file mode 100644 index 00000000..018ccc8a --- /dev/null +++ b/public/modules/users/tests/unit/controllers/authentication.client.controller.test.js @@ -0,0 +1,243 @@ +'use strict'; + +(function() { + // Forms Controller Spec + describe('Authentication Controller Tests', function() { + // Initialize global variables + var ctrl, + scope, + $httpBackend, + $stateParams, + $state; + + var sampleUser = { + firstName: 'Full', + lastName: 'Name', + email: 'test@test.com', + username: 'test@test.com', + password: 'password', + provider: 'local', + roles: ['user'], + _id: 'ed873933b1f1dea0ce12fab9' + }; + + var sampleForm = { + title: 'Form Title', + admin: 'ed873933b1f1dea0ce12fab9', + language: 'english', + form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + _id: '525a8422f6d0f87f0e407a33' + }; + + var expectedForm = { + title: 'Form Title', + admin: 'ed873933b1f1dea0ce12fab9', + language: 'english', + form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + visible_form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + _id: '525a8422f6d0f87f0e407a33' + }; + + var sampleCredentials = { + username: sampleUser.username, + password: sampleUser.password, + }; + + + + // The $resource service augments the response object with methods for updating and deleting the resource. + // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match + // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. + // When the toEqualData matcher compares two objects, it takes only object properties into + // account and ignores methods. + beforeEach(function() { + jasmine.addMatchers({ + toEqualData: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + return { + pass: angular.equals(actual, expected) + }; + } + }; + } + }); + }); + + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + beforeEach(module('module-templates')); + beforeEach(module('stateMock')); + + // Mock Users Service + beforeEach(module(function($provide) { + $provide.service('User', function($q) { + return { + getCurrent: function() { + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; + }, + login: function(credentials) { + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; + }, + logout: function() { + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(null); + } + }; + }, + signup: function(credentials) { + return { + then: function(onFulfilled, onRejected, progressBack) { + return onFulfilled(sampleUser); + } + }; + } + }; + }); + })); + + // Mock Authentication Service + beforeEach(module(function($provide) { + $provide.service('Auth', function() { + return { + _currentUser: null, + get currentUser(){ + return sampleUser + }, + login: function(user) { + }, + ensureHasCurrentUser: function() { + return sampleUser; + }, + isAuthenticated: function() { + return true; + } + }; + }); + })); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function($controller, $rootScope, _$state_, _$stateParams_, _$httpBackend_, Auth, User) { + // Set a new global scope + scope = $rootScope.$new(); + scope.forms = { + signinForm: { + $valid: true + }, + signupForm: { + $valid: true + }, + }; + + scope.credentials = _.cloneDeep(sampleCredentials); + + // Point global variables to injected services + $stateParams = _$stateParams_; + $httpBackend = _$httpBackend_; + $state = _$state_; + + $httpBackend.whenGET('/forms').respond(''); + $httpBackend.whenGET('/users/me/').respond(''); + + // Initialize the Forms controller. + + this.init = function(){ + ctrl = $controller('AuthenticationController', { + $scope: scope + }); + } + })); + + it('$scope.signin should sign-in in user with valid credentials', inject(function(Auth) { + this.init(); + + //Set $state transition + $state.expectTransitionTo('listForms'); + spyOn(Auth, 'login'); + + //Run Controller Logic to Test + scope.signin(); + + // Test scope value + expect(Auth.ensureHasCurrentUser()).toEqualData(sampleUser); + expect(Auth.login).toHaveBeenCalledTimes(1); + expect(scope.user).toEqualData(sampleUser); + + $state.ensureAllTransitionsHappened(); + })); + + it('$scope.signin should sign-in in user and redirect to previous state', inject(function(Auth) { + this.init(); + + $state.previous = { + state: { + name: 'profile' + }, + fromParams: {} + } + + //Set $state transition + $state.expectTransitionTo('profile'); + spyOn(Auth, 'login'); + + //Run Controller Logic to Test + scope.signin(); + + // Test scope value + expect(Auth.ensureHasCurrentUser()).toEqualData(sampleUser); + expect(Auth.login).toHaveBeenCalledTimes(1); + expect(scope.user).toEqualData(sampleUser); + + $state.ensureAllTransitionsHappened(); + })); + + + it('$scope.signup should sign-up user with valid credentials', inject(function(Auth) { + this.init(); + + //Set $state transition + $state.expectTransitionTo('signup-success'); + spyOn(Auth, 'isAuthenticated').and.returnValue(false); + + //Run Controller Logic to Test + scope.signup(); + + $state.ensureAllTransitionsHappened(); + })); + + it('$scope.signup should not sign-up user if username is admin', function() { + scope.credentials.username = 'admin'; + scope.credentials.email = 'test@example.com'; + this.init(); + + //Run Controller Logic to Test + scope.signup(); + + expect(scope.error).toEqual('Username cannot be \'admin\'. Please pick another username.'); + }); + }); +}()); \ No newline at end of file diff --git a/public/modules/users/tests/unit/controllers/password.client.controller.test.js b/public/modules/users/tests/unit/controllers/password.client.controller.test.js new file mode 100644 index 00000000..9059b0b6 --- /dev/null +++ b/public/modules/users/tests/unit/controllers/password.client.controller.test.js @@ -0,0 +1,128 @@ +'use strict'; + +(function() { + // Forms Controller Spec + describe('Password Controller Tests', function() { + // Initialize global variables + var ctrl, + scope, + $httpBackend, + $state; + + var sampleUser = { + firstName: 'Full', + lastName: 'Name', + email: 'test@test.com', + username: 'test@test.com', + password: 'password', + provider: 'local', + roles: ['user'], + _id: 'ed873933b1f1dea0ce12fab9' + }; + + var sampleForm = { + title: 'Form Title', + admin: 'ed873933b1f1dea0ce12fab9', + language: 'english', + form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + _id: '525a8422f6d0f87f0e407a33' + }; + + var sampleCredentials = { + username: sampleUser.username, + password: sampleUser.password, + }; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + beforeEach(module('module-templates')); + beforeEach(module('stateMock')); + + var thenFunction = function(onFulfilled, onRejected, progressBack){ + onFulfilled(sampleForm) + }; + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function($controller, $rootScope, _$state_, _$httpBackend_, Auth, User) { + // Set a new global scope + scope = $rootScope.$new(); + + scope.credentials = _.cloneDeep(sampleCredentials); + scope.passwordDetails = { + newPassword: 'aoeeaoaeo', + verifyPassword: 'aoeeaoaeo' + } + + // Point global variables to injected services + $httpBackend = _$httpBackend_; + $state = _$state_; + + $httpBackend.whenGET('/forms').respond(''); + $httpBackend.whenGET('/users/me/').respond(''); + + // Initialize the Forms controller. + + this.init = function(){ + ctrl = $controller('PasswordController', { + $scope: scope + }); + } + })); + + it('$scope.resetUserPassword should call User.resetPassword if form is valid', inject(function(User) { + scope.forms = { + resetPasswordForm: { + $valid: true + } + }; + this.init(); + + //Set $state transition + $state.expectTransitionTo('reset-success'); + spyOn(User, 'resetPassword').and.returnValue({ then: thenFunction }); + + //Run Controller Logic to Test + scope.resetUserPassword(); + + // Test scope value + expect(User.resetPassword).toHaveBeenCalledTimes(1); + $state.ensureAllTransitionsHappened(); + })); + + it('$scope.resetUserPassword should not call User.resetPassword if form is invalid', inject(function(User) { + scope.forms = { + resetPasswordForm: { + $valid: false + } + }; + this.init(); + + //Set $state transition + spyOn(User, 'resetPassword').and.returnValue({ then: thenFunction }); + + //Run Controller Logic to Test + scope.resetUserPassword(); + + // Test scope value + expect(User.resetPassword).toHaveBeenCalledTimes(0); + })); + + it('$scope.askForPasswordReset should call User.askForPasswordReset', inject(function(User) { + this.init(); + + spyOn(User, 'askForPasswordReset').and.returnValue({ then: thenFunction }); + + //Run Controller Logic to Test + scope.askForPasswordReset(); + + // Test scope value + expect(User.askForPasswordReset).toHaveBeenCalledTimes(1); + })); + }); +}()); \ No newline at end of file diff --git a/public/modules/users/tests/unit/controllers/settings.client.controller.test.js b/public/modules/users/tests/unit/controllers/settings.client.controller.test.js new file mode 100644 index 00000000..bec5df87 --- /dev/null +++ b/public/modules/users/tests/unit/controllers/settings.client.controller.test.js @@ -0,0 +1,134 @@ +'use strict'; + +(function() { + // Forms Controller Spec + describe('Settings Controller Tests', function() { + // Initialize global variables + var ctrl, + scope, + $httpBackend, + $state, + $http; + + var sampleUser = { + firstName: 'Full', + lastName: 'Name', + email: 'test@test.com', + username: 'test@test.com', + password: 'password', + provider: 'local', + roles: ['user'], + _id: 'ed873933b1f1dea0ce12fab9', + language: 'en' + }; + + var sampleForm = { + title: 'Form Title', + admin: 'ed873933b1f1dea0ce12fab9', + language: 'english', + form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + _id: '525a8422f6d0f87f0e407a33' + }; + + var sampleCredentials = { + username: sampleUser.username, + password: sampleUser.password, + }; + + var samplePasswordDetails = { + newPassword: sampleUser.password, + verifyPassword: sampleUser.password + }; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + beforeEach(module('module-templates')); + + beforeEach(function() { + jasmine.addMatchers({ + toEqualData: function(util, customEqualityTesters) { + return { + compare: function(actual, expected) { + return { + pass: angular.equals(actual, expected) + }; + } + }; + } + }); + }); + + // Mock currentUser Service + beforeEach(module(function($provide) { + $provide.service('currentUser', function() { + return sampleUser; + }); + })); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function($controller, $rootScope, _$state_, _$httpBackend_, Auth, User, _$http_) { + // Set a new global scope + scope = $rootScope.$new(); + scope.passwordDetails = samplePasswordDetails; + + // Point global variables to injected services + $httpBackend = _$httpBackend_; + $state = _$state_; + $http = _$http_; + + $httpBackend.whenGET('/forms').respond(''); + $httpBackend.whenGET('/users/me/').respond(sampleUser); + + // Initialize the Forms controller. + ctrl = $controller('SettingsController', { + $scope: scope + }); + })); + + var thenFunction = function(onFulfilled, onRejected, progressBack){ + onFulfilled({ data: sampleUser }) + }; + + it('$scope.updateUserProfile should update my user profile if isValid is TRUE', inject(function($http) { + spyOn($http, 'put').and.returnValue({then: thenFunction}); + + //Run Controller Logic to Test + scope.updateUserProfile(true); + + expect($http.put).toHaveBeenCalledTimes(1); + expect($http.put).toHaveBeenCalledWith('/users', sampleUser); + + expect(scope.success).toBeTruthy(); + expect(scope.error).toBeNull(); + })); + + it('$scope.updateUserProfile should NOT update my user profile if isValid is FALSE', function() { + + //Run Controller Logic to Test + scope.updateUserProfile(false); + + $httpBackend.flush(); + }); + + it('$scope.changeUserPassword should update the user\'s password', inject(function($http) { + + spyOn($http, 'post').and.returnValue({then: thenFunction}); + + //Run Controller Logic to Test + scope.changeUserPassword(); + + expect(scope.success).toBeTruthy(); + expect(scope.error).toBeNull(); + expect(scope.user).toEqualData(sampleUser); + + expect($http.post).toHaveBeenCalledTimes(1); + expect($http.post).toHaveBeenCalledWith('/users/password', samplePasswordDetails); + })); + }); +}()); \ No newline at end of file diff --git a/public/modules/users/tests/unit/controllers/verify.client.controller.test.js b/public/modules/users/tests/unit/controllers/verify.client.controller.test.js new file mode 100644 index 00000000..a46869c1 --- /dev/null +++ b/public/modules/users/tests/unit/controllers/verify.client.controller.test.js @@ -0,0 +1,108 @@ +'use strict'; + +(function() { + // Forms Controller Spec + describe('Verify Controller Tests', function() { + // Initialize global variables + var ctrl, + scope, + $httpBackend, + $stateParams; + + var sampleUser = { + firstName: 'Full', + lastName: 'Name', + email: 'test@test.com', + username: 'test@test.com', + password: 'password', + provider: 'local', + roles: ['user'], + _id: 'ed873933b1f1dea0ce12fab9' + }; + + var sampleForm = { + title: 'Form Title', + admin: 'ed873933b1f1dea0ce12fab9', + language: 'english', + form_fields: [ + {fieldType:'textfield', title:'First Name', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'nascar', fieldValue: '', deletePreserved: false}, + {fieldType:'checkbox', title:'hockey', fieldValue: '', deletePreserved: false} + ], + _id: '525a8422f6d0f87f0e407a33' + }; + + var sampleCredentials = { + email: sampleUser.email + }; + + // Load the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + beforeEach(module('module-templates')); + + // Mock currentUser Service + beforeEach(module(function($provide) { + $provide.service('currentUser', function() { + return sampleUser; + }); + })); + + var thenFunction = function(onFulfilled, onRejected, progressBack){ + onFulfilled(sampleForm) + }; + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function($controller, $rootScope, _$state_, _$stateParams_, _$httpBackend_, Auth, User) { + // Set a new global scope + scope = $rootScope.$new(); + + // Point global variables to injected services + $httpBackend = _$httpBackend_; + $stateParams = _$stateParams_; + + $httpBackend.whenGET('/forms').respond(''); + $httpBackend.whenGET('/users/me/').respond(''); + + // Initialize the Forms controller. + + this.init = function(){ + ctrl = $controller('VerifyController', { + $scope: scope + }); + } + })); + + it('$scope.resendVerifyEmail should update my user profile if credentials are valid', inject(function(User) { + scope.credentials = sampleCredentials; + this.init(); + + spyOn(User, 'resendVerifyEmail').and.returnValue({ then: thenFunction }); + + //Run Controller Logic to Test + scope.resendVerifyEmail(); + + // Test scope value + expect(User.resendVerifyEmail).toHaveBeenCalledTimes(1); + expect(User.resendVerifyEmail).toHaveBeenCalledWith(sampleCredentials.email); + })); + + it('$scope.validateVerifyToken should update my user profile if credentials are valid', inject(function(User, $stateParams) { + scope.credentials = sampleCredentials; + this.init(); + + var verifyToken = 'ed8730ce12fab9933b1f1dea'; + + $stateParams.token = verifyToken; + spyOn(User, 'validateVerifyToken').and.returnValue({ then: thenFunction }); + + //Run Controller Logic to Test + scope.validateVerifyToken(); + + // Test scope value + expect(User.validateVerifyToken).toHaveBeenCalledTimes(1); + expect(User.validateVerifyToken).toHaveBeenCalledWith(verifyToken); + })); + }); +}()); \ No newline at end of file diff --git a/public/modules/users/tests/unit/services/auth.client.service.test.js b/public/modules/users/tests/unit/services/auth.client.service.test.js index d6417a92..26c2715a 100644 --- a/public/modules/users/tests/unit/services/auth.client.service.test.js +++ b/public/modules/users/tests/unit/services/auth.client.service.test.js @@ -17,7 +17,6 @@ _id: 'ed873933b1f1dea0ce12fab9' }; - // The $resource service augments the response object with methods for updating and deleting the resource. // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. @@ -44,8 +43,9 @@ // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). // This allows us to inject a service but then attach it to a variable // with the same name as the service. - beforeEach(inject(function(_Auth_) { + beforeEach(inject(function(_Auth_, $httpBackend) { Auth = _Auth_; + $httpBackend.whenGET('/users/me').respond(200, sampleUser); })); it('Auth.login() should save user in Auth.currentUser', function() { @@ -61,31 +61,14 @@ expect($window.user).toEqual(null); expect(Auth.currentUser).toEqual(null); expect(Auth.isAuthenticated()).toBe(false); - expect(Auth.getUserState().isLoggedIn).toBe(false); })); - - it('Auth.getUserState() should fetch current user state', function() { - //Run Service Logic to Test - Auth.login(sampleUser); - var currUserState = Auth.getUserState(); - - expect(currUserState.isLoggedIn).toBe(true); - - //Run Service Logic to Test - Auth.logout(); - currUserState = Auth.getUserState(); - - expect(currUserState.isLoggedIn).toBe(false); - }); - + it('Auth.ensureHasCurrentUser() should fetch most current user if it exists in $window, currentUser or fetch it from /users/me', function() { - Auth.login(sampleUser); - //Run Service Logic to Test - var currUser = Auth.ensureHasCurrentUser(sampleUser); - - expect(currUser).not.toEqual(null); - expect(currUser).toEqualData(sampleUser); + Auth.ensureHasCurrentUser().then(function onSuccess(currUser){ + expect(currUser).not.toEqual(null); + expect(currUser).toEqualData(sampleUser); + }); }); }); diff --git a/public/modules/users/tests/unit/services/user.client.service.test.js b/public/modules/users/tests/unit/services/user.client.service.test.js index 601ff20e..4fe9052d 100644 --- a/public/modules/users/tests/unit/services/user.client.service.test.js +++ b/public/modules/users/tests/unit/services/user.client.service.test.js @@ -62,6 +62,8 @@ // Point global variables to injected services $httpBackend = _$httpBackend_; User = _User_; + + $httpBackend.whenGET('/forms').respond(''); })); it('User.login() should send a POST request to /auth/signin', function() { diff --git a/public/modules/users/views/settings/change-password.client.view.html b/public/modules/users/views/settings/change-password.client.view.html index 3a8565fa..cd039916 100755 --- a/public/modules/users/views/settings/change-password.client.view.html +++ b/public/modules/users/views/settings/change-password.client.view.html @@ -1,6 +1,6 @@
-
+

{{ 'CHANGE_PASSWORD' | translate }}

diff --git a/public/modules/users/views/settings/edit-profile.client.view.html b/public/modules/users/views/settings/edit-profile.client.view.html index cf157b71..c4df4e75 100755 --- a/public/modules/users/views/settings/edit-profile.client.view.html +++ b/public/modules/users/views/settings/edit-profile.client.view.html @@ -1,6 +1,7 @@
+ -
+

{{ 'EDIT_PROFILE' | translate }}

@@ -15,53 +16,56 @@
- {{ 'FIRST_NAME_LABEL' | translate }} +

{{ 'FIRST_NAME_LABEL' | translate }}

- +
- {{ 'LAST_NAME_LABEL' | translate }} +

{{ 'LAST_NAME_LABEL' | translate }}

- +

-
-
- {{ 'LANGUAGE_LABEL' | translate }} -
-
- -
-
+
+
+

{{ 'LANGUAGE' | translate }}

+
+
+ + + {{ $root.langCodeToWord[$select.selected] }} + + + + + + + * {{ 'REQUIRED_FIELD' | translate }} +
+
- {{ 'USERNAME_LABEL' | translate }} +

{{ 'USERNAME_LABEL' | translate }}

- +
- {{ 'EMAIL_LABEL' | translate }} +

{{ 'EMAIL_LABEL' | translate }}

- +
diff --git a/public/modules/users/views/settings/social-accounts.client.view.html b/public/modules/users/views/settings/social-accounts.client.view.html deleted file mode 100755 index 9588947a..00000000 --- a/public/modules/users/views/settings/social-accounts.client.view.html +++ /dev/null @@ -1,31 +0,0 @@ -
- -
-

{{ 'CONNECTED_SOCIAL_ACCOUNTS' | translate }}:

-
- -
-

{{ 'CONNECT_OTHER_SOCIAL_ACCOUNTS' | translate }}

- -
diff --git a/public/swagger.json b/public/swagger.json index 37a4fdf3..44bdb810 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -811,7 +811,7 @@ } } }, - "hideFooter": { + "showFooter": { "type": "boolean", "default": false, "description": "Specifies whether to hide or show Form Footer", diff --git a/scripts/build/compiler.jar b/scripts/build/compiler.jar deleted file mode 100644 index 1846644f..00000000 Binary files a/scripts/build/compiler.jar and /dev/null differ diff --git a/scripts/create_admin.js b/scripts/create_admin.js index b676f92e..53b83103 100644 --- a/scripts/create_admin.js +++ b/scripts/create_admin.js @@ -1,13 +1,14 @@ var config = require('../config/config'), mongoose = require('mongoose'), - chalk = require('chalk'); + chalk = require('chalk'); exports.run = function(app, db, cb) { + console.log(chalk.green('Creating the Admin Account')); var User = mongoose.model('User'); var email = config.admin.email || 'admin@admin.com'; - - var newUser = new User({ + + var newUserObj = { firstName: 'Admin', lastName: 'Account', email: email, @@ -15,24 +16,32 @@ exports.run = function(app, db, cb) { password: config.admin.password || 'root', provider: 'local', roles: ['admin', 'user'] - }); + }; - User.findOne({email: email}, function (err, user) { + var options = { + upsert: true, + new: true, + setDefaultsOnInsert: true + }; + + User.findOneAndUpdate({username: newUserObj.username}, newUserObj, options, function (err, currUser1) { if (err) { - cb(err); + return cb(err); } - if(!user){ - newUser.save(function (userErr) { - if (userErr) { - return cb(userErr); - } - console.log(chalk.green('Successfully created Admin Account')); - - cb(); - }); + if(!currUser1){ + return cb(new Error('Couldn\'t create admin account')); } else { - cb('User already exists!'); + + currUser1.password = config.admin.password; + currUser1.save(function(err, currUser2){ + if (err) { + return cb(err); + } + + console.log(chalk.green('Successfully created/updated Admin Account')); + return cb(); + }); } }); -} +}; diff --git a/scripts/generate-ssl-certs.sh b/scripts/generate-ssl-certs.sh deleted file mode 100755 index 6f5cb2ab..00000000 --- a/scripts/generate-ssl-certs.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -if [ ! -e server.js ] -then - echo "Error: could not find main application server.js file" - echo "You should run the generate-ssl-certs.sh script from the main MEAN application root directory" - echo "i.e: bash scripts/generate-ssl-certs.sh" - exit -1 -fi - -echo "Generating self-signed certificates..." -mkdir -p ./config/sslcerts -openssl genrsa -out ./config/sslcerts/key.pem -aes256 1024 -openssl rsa -in ./config/sslcerts/key.pem -out ./config/sslcerts/newkey.pem -rm ./config/sslcerts/key.pem -mv ./config/sslcerts/newkey.pem ./config/sslcerts/key.pem -openssl req -new -key ./config/sslcerts/key.pem -out ./config/sslcerts/csr.pem -openssl x509 -req -days 9999 -in ./config/sslcerts/csr.pem -signkey ./config/sslcerts/key.pem -out ./config/sslcerts/cert.pem -rm ./config/sslcerts/csr.pem -chmod 600 ./config/sslcerts/key.pem ./config/sslcerts/cert.pem diff --git a/scripts/git-remove-history.sh b/scripts/git-remove-history.sh deleted file mode 100644 index 25b28faa..00000000 --- a/scripts/git-remove-history.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -o errexit - -# Author: David Underhill -# Script to permanently delete files/folders from your git repository. To use -# it, cd to your repository's root and then run the script with a list of paths -# you want to delete, e.g., git-delete-history path1 path2 - -if [ $# -eq 0 ]; then - exit 0 -fi - -# make sure we're at the root of git repo -if [ ! -d .git ]; then - echo "Error: must run this script from the root of a git repository" - exit 1 -fi - -# remove all paths passed as arguments from the history of the repo -files=$@ -git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch $files" HEAD - -# remove the temporary history git-filter-branch otherwise leaves behind for a long time -rm -rf .git/refs/original/ && git reflog expire --all && git gc --aggressive --prune diff --git a/scripts/setup.js b/scripts/setup.js index d8e5350c..e91f29fa 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -3,239 +3,188 @@ /** * Module dependencies. */ -process.env.NODE_ENV = 'production'; -var config = require('../config/config'), - mongoose = require('mongoose'), +var mongoose = require('mongoose'), inquirer = require('inquirer'), envfile = require('envfile'), fs = require('fs-extra'), - chalk = require('chalk'); + chalk = require('chalk'), + constants = require('./setup_constants'), + _ = require('lodash'); -// Bootstrap db connection -var db = mongoose.connect(config.db.uri, config.db.options, function(err) { - if (err) { - console.error(chalk.red('Could not connect to MongoDB!')); - console.log(chalk.red(err)); +var exitSuccess = function(cb) { + console.log(chalk.green('TellForm has been successfully setup')); + console.log(chalk.green('Have fun using TellForm!')); + + if(require.main === module){ + process.exit(1); + } else if(cb && typeof cb === 'function'){ + cb(); } -}); -mongoose.connection.on('error', function(err) { - console.error(chalk.red('MongoDB connection error: ' + err)); - process.exit(-1); -}); - -// Init the express application -require('../config/express')(db); - -// Bootstrap passport config -require('../config/passport')(); - -var User = mongoose.model('User'); -require('../app/models/user.server.model.js'); - -var nodemailer_providers = [ - '1und1', - 'AOL', - 'DebugMail.io', - 'DynectEmail', - 'FastMail', - 'GandiMail', - 'Gmail', - 'Godaddy', - 'GodaddyAsia', - 'GodaddyEurope', - 'hot.ee', - 'Hotmail', - 'iCloud', - 'mail.ee', - 'Mail.ru', - 'Mailgun', - 'Mailjet', - 'Mandrill', - 'Naver', - 'OpenMailBox', - 'Postmark', - 'QQ', - 'QQex', - 'SendCloud', - 'SendGrid', - 'SES', - 'SES-US-EAST-1', - 'SES-US-WEST-1', - 'SES-EU-WEST-1', - 'Sparkpost', - 'Yahoo', - 'Yandex', - 'Zoho' -]; - -var bool_options = [ - "TRUE", - "FALSE" -]; - -var questions = [ - { - type: 'confirm', - name: 'shouldContinue', - message: 'Do you wish to configure your deployment now?' - }, - { - type: 'input', - name: 'APP_NAME', - message: 'What do you want to name your TellForm deployment?' - }, - { - type: 'input', - name: 'APP_DESC', - message: 'Describe your project (for SEO) (optional)' - }, - { - type: 'input', - name: 'APP_KEYWORDS', - message: 'What keywords are relevant to your project (seperate by commas) (optional)' - }, - { - type: 'confirm', - name: 'SIGNUP_DISABLED', - message: 'Do you want to disable signups?', - default: false - }, - { - type: 'list', - name: 'SUBDOMAINS_DISABLED', - message: 'Do you want to have subdomains? (i.e. are you using a custom domain)', - choices: bool_options - }, - { - type: 'list', - name: 'MAILER_SERVICE_PROVIDER', - message: 'What email service provider are you using?', - choices: nodemailer_providers - }, - { - type: 'input', - name: 'MAILER_EMAIL_ID', - message: 'What is your SMTP username?' - }, - { - type: 'password', - name: 'MAILER_PASSWORD', - message: 'What is your SMTP password?' - }, - { - type: 'input', - name: 'MAILER_FROM', - message: 'What do you want the default "from" email address to be?' - }, - { - type: 'input', - name: 'BASE_URL', - message: 'What is the url your TellForm will be hosted at?', - default: 'localhost' - }, - { - type: 'input', - name: 'PORT', - message: 'What port should the TellForm server run on?', - default: '3000' - }, - { - type: 'input', - name: 'GOOGLE_ANALYTICS_ID', - message: 'What is your Google Analytics Tag? (optional)' - }, - { - type: 'input', - name: 'RAVEN_DSN', - message: 'What is your Private Raven DSN key? (optional)' - }, - { - type: 'input', - name: 'PRERENDER_TOKEN', - message: 'What is your Prerender.io token? (optional)' - }, - { - type: 'input', - name: 'COVERALLS_REPO_TOKEN', - message: 'What is your Coveralls.io token? (optional)' - }, - { - type: 'input', - name: 'COVERALLS_REPO_TOKEN', - message: 'What is your reCAPTCHA token? (optional)' - }, - { - type: 'input', - name: 'email', - message: 'What should be the email for your admin account?' - }, - { - type: 'input', - name: 'username', - message: 'What should be the username for your admin account?' - }, - { - type: 'password', - name: 'password', - message: 'What should be the password for your admin account?' - } -]; - -if(!fs.existsSync('./\.env')) { - console.log(chalk.green('\n\nHi, welcome to TellForm Setup')); - - console.log(chalk.green('You should only run this the first time you run TellForm\n--------------------------------------------------\n\n')); - - inquirer.prompt([questions[0]]).then(function (confirmAns) { - if (confirmAns['shouldContinue']) { - - inquirer.prompt(questions.slice(1)).then(function (answers) { - answers['NODE_ENV'] = 'production'; - - var email = answers['email']; - var username = answers['username']; - var pass = answers['password']; - delete answers['email']; - delete answers['password']; - - envfile.stringify(answers, function (err, str) { - try { - fs.outputFileSync('./\.env', str); - } catch (fileErr) { - return console.error(chalk.red(fileErr)); - } - - console.log(chalk.green('Successfully created .env file')); - - user = new User({ - firstName: 'Admin', - lastName: 'Account', - email: email, - username: username, - password: pass, - provider: 'local', - roles: ['admin', 'user'] - }); - - user.save(function (userSaveErr) { - if (err) { - return console.error(chalk.red(userSaveErr)); - } - - console.log(chalk.green('Successfully created user')); - - console.log(chalk.green('Have fun using TellForm!')); - process.exit(1); - }); - }); - }); - } else { - console.log(chalk.green('Have fun using TellForm!')); - process.exit(1); - } - }); -} else { - console.log(chalk.red('You already have a .env file')); - process.exit(1); +} + +var exitError = function(err, cb){ + console.error(chalk.red(err.message || err)); + if(require.main === module){ + process.exit(-1); + } else if(cb && typeof cb === 'function'){ + cb(); + } +} + +var removeENVFile = function() { + fs.unlinkSync('./\.env') +} + +var createOrUpdateAdminUser = function(username, email, password, cb){ + //Command Line Bootstrapping Code + if (require.main === module) { + var config = require('../config/config'); + + // Bootstrap db connection + var db = mongoose.connect(config.db.uri, config.db.options, function(err) { + if (err) { + console.error(chalk.red('Could not connect to MongoDB!')); + return cb(new Error(err)); + } + }); + mongoose.connection.on('error', function(err) { + return cb(new Error('MongoDB connection error: ' + err)); + }); + + // Init the express application + require('../config/express')(db); + + // Bootstrap passport config + require('../config/passport')(); + } + + var User = require('../app/models/user.server.model.js'); + + var updateObj = { + firstName: 'Admin', + lastName: 'Account', + username: username, + email: email, + provider: 'local', + roles: ['admin', 'user'] + } + + var options = { + upsert: true, + new: true, + setDefaultsOnInsert: true + } + + User.findOneAndUpdate({ username: username }, updateObj, options, function (err, user) { + if (err) { + delete pass; + delete email; + delete username; + return cb(err); + } + + if(!user){ + delete pass; + delete email; + delete username; + return cb(new Error('Admin User could not be created')); + } + + user.password = password + user.save(function(err) { + if(err){ + delete pass; + delete email; + delete username; + return cb(err); + } + + delete pass; + delete email; + delete username; + + console.log(chalk.green('Successfully created user')); + + cb(); + }); + }); + +} + +var createENVFile = function(cb) { + inquirer.prompt(constants.questionsPart1).then(function (answersPart1) { + var nextQuestions = constants.mailerWellKnownQuestions.concat(constants.questionsPart2); + if(answersPart1['MAILER_SERVICE_PROVIDER'] === 'Custom Mailserver'){ + nextQuestions = constants.mailerCustomQuestions.concat(constants.questionsPart2); + } + + inquirer.prompt(nextQuestions).then(function (answersPart2) { + var answers = _.chain(anwsersPart1)._extend(answersPart2).mapValues(function(val){ + if(_.isBoolean(val)){ + return val ? 'TRUE' : 'FALSE'; + } + return val; + }).values(); + + var email = answers['email']; + var username = answers['username']; + var pass = answers['password']; + delete answers['email']; + delete answers['username']; + delete answers['password']; + + envfile.stringify(answers, function (err, str) { + try { + fs.outputFileSync('./\.env', str); + } catch (fileErr) { + console.error(chalk.red(fileErr)); + process.exit(-1); + } + + console.log(chalk.green('Successfully created .env file')); + + createOrUpdateAdminUser(username, email, pass, function(err){ + if(err) { + return exitError(err, cb); + } + exitSuccess(cb); + }); + + }); + }); + }); +} + +var checkENVAndRunSetup = function(cb) { + console.log(chalk.green(constants.asciiArt)); + if(require.main === module){ + console.log(chalk.green('Welcome to TellForm\'s Setup Tool')); + console.log(chalk.green('Follow the prompts to begin.\n-------------------------------------------\n\n')); + } + + if(fs.existsSync('./\.env') && require.main === module) { + inquirer.prompt([constants.replaceENVQuestion]).then(function (envAnswer) { + if (envAnswer['replaceENVFile']) { + removeENVFile(); + createENVFile(cb); + } else { + exitSuccess(cb); + } + }); + } else { + + if(require.main !== module){ + console.log(chalk.green('Welcome to TellForm\'s Initial Setup\n')); + console.log(chalk.green('The following prompts will help you properly configure your TellForm instance.')); + console.log(chalk.green('If you want to run this tool after your inital setup, run `node scripts/setup.js`.\n---------------------------------------------------------------------\n\n')); + } + createENVFile(); + } +} + +module.exports.checkENVAndRunSetup = checkENVAndRunSetup; + +if(require.main === module) { + checkENVAndRunSetup(); } diff --git a/scripts/setup_constants.js b/scripts/setup_constants.js new file mode 100644 index 00000000..29397906 --- /dev/null +++ b/scripts/setup_constants.js @@ -0,0 +1,201 @@ +var constants = require('../app/libs/constants'); + +var createRegexValidator = function(regex, message){ + return function(value) { + var isValid = new RegExp(regex, 'g').test(value); + + if(!isValid){ + return message + } else { + return true; + } + } +} + +var validateEmail = createRegexValidator(constants.regex.email, 'Please enter a valid email'); +var validateUsername = createRegexValidator(constants.regex.username, 'Usernames can only contain alphanumeric characters and \'-\''); + +module.exports = { + asciiArt: " _____ _ _______ \n" + + " |_ _| | | | ___| \n" + + " | | ___| | | |_ ___ _ __ _ __ ___ \n" + + " | |/ _ \\ | | _/ _ \\| '__| '_ ` _ \\ \n" + + " | | __/ | | || (_) | | | | | | | |\n" + + " \\_/\\___|_|_\\_| \\___/|_| |_| |_| |_|\n", + + + replaceENVQuestion: { + type: 'confirm', + name: 'replaceENVFile', + message: 'An older .env file already exists. Do you want to replace it?', + default: false + }, + + questionsPart1: [ + { + type: 'list', + name: 'NODE_ENV', + message: 'What mode do you want to run TellForm in?', + choices: ['development', 'production', 'test'], + default: 'development' + }, + { + type: 'input', + name: 'APP_NAME', + message: 'What do you want to name your TellForm deployment?' + }, + { + type: 'input', + name: 'APP_DESC', + message: 'Describe your project (for SEO) (optional)' + }, + { + type: 'confirm', + name: 'SIGNUP_DISABLED', + message: 'Do you want to disable signups?', + default: false + }, + { + type: 'confirm', + name: 'SUBDOMAINS_DISABLED', + message: 'Do you want to disable subdomains? (i.e. are you using a custom domain)' + }, + { + type: 'list', + name: 'MAILER_SERVICE_PROVIDER', + message: 'What email service provider are you using?', + choices: [ + 'Custom Mailserver', + '1und1', + 'AOL', + 'DebugMail.io', + 'DynectEmail', + 'FastMail', + 'GandiMail', + 'Gmail', + 'Godaddy', + 'GodaddyAsia', + 'GodaddyEurope', + 'hot.ee', + 'Hotmail', + 'iCloud', + 'mail.ee', + 'Mail.ru', + 'Mailgun', + 'Mailjet', + 'Mandrill', + 'Naver', + 'OpenMailBox', + 'Postmark', + 'QQ', + 'QQex', + 'SendCloud', + 'SendGrid', + 'SES', + 'SES-US-EAST-1', + 'SES-US-WEST-1', + 'SES-EU-WEST-1', + 'Sparkpost', + 'Yahoo', + 'Yandex', + 'Zoho' + ] + } + ], + + mailerWellKnownQuestions: [ + { + type: 'input', + name: 'MAILER_EMAIL_ID', + message: 'What is your SMTP username?' + }, + { + type: 'password', + name: 'MAILER_PASSWORD', + message: 'What is your SMTP password?' + } + ], + + mailerCustomQuestions: [ + { + type: 'input', + name: 'MAILER_SMTP_HOST', + message: 'What is your SMTP server url?' + }, + { + type: 'input', + name: 'MAILER_SMTP_PORT', + message: 'What is your SMTP server port?' + }, + { + type: 'confirm', + name: 'MAILER_SMTP_SECURE', + message: 'Is your SMTP server using SSL/TLS?' + }, + { + type: 'input', + name: 'MAILER_SMTP_HOST', + message: 'What is your SMTP host domain?' + }, + { + type: 'input', + name: 'MAILER_EMAIL_ID', + message: 'What is your SMTP username?' + }, + { + type: 'password', + name: 'MAILER_PASSWORD', + message: 'What is your SMTP password?' + } + ], + + questionsPart2: [ + { + type: 'input', + name: 'MAILER_FROM', + message: 'What do you want the default "from" email address to be?', + validate: validateEmail + }, + { + type: 'input', + name: 'MONGODB_URI', + message: 'What is the URI of your Mongo database?', + default: 'mongodb://localhost/mean' + }, + { + type: 'input', + name: 'REDIS_URL', + message: 'What is the URI of your Redis installation?', + default: 'redis://127.0.0.1:6379' + }, + { + type: 'input', + name: 'BASE_URL', + message: 'What is the (root) url your TellForm will be hosted at?', + default: 'localhost' + }, + { + type: 'input', + name: 'PORT', + message: 'What port should the TellForm server run on?', + default: '3000' + }, + { + type: 'input', + name: 'email', + message: 'What should be the email for your admin account?', + validate: validateEmail + }, + { + type: 'input', + name: 'username', + message: 'What should be the username for your admin account?', + validate: validateUsername + }, + { + type: 'password', + name: 'password', + message: 'What should be the password for your admin account?' + } + ] +}; \ No newline at end of file diff --git a/selenium/.editorconfig b/selenium/.editorconfig new file mode 100644 index 00000000..5a13b97d --- /dev/null +++ b/selenium/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +# Apply for all files +[*] + +charset = utf-8 + +indent_style = space +indent_size = 4 + +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/selenium/.gitignore b/selenium/.gitignore new file mode 100644 index 00000000..704c683c --- /dev/null +++ b/selenium/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.idea +node_modules +npm-debug.log +uirecorder.log +reports +screenshots/**/*.png +screenshots/**/*.html +screenshots/**/*.json diff --git a/selenium/.vscode/launch.json b/selenium/.vscode/launch.json new file mode 100644 index 00000000..ec025318 --- /dev/null +++ b/selenium/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug UIRecorder Local", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "cwd": "${workspaceRoot}", + "args": ["--reporter", "mochawesome-uirecorder", "${file}"], + "env": { + "webdriver": "127.0.0.1" + } + }, + { + "type": "node", + "request": "launch", + "name": "Debug UIRecorder Default", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "cwd": "${workspaceRoot}", + "args": ["--reporter", "mochawesome-uirecorder", "${file}"] + } + ] +} \ No newline at end of file diff --git a/selenium/README.md b/selenium/README.md new file mode 100644 index 00000000..62ab2ba6 --- /dev/null +++ b/selenium/README.md @@ -0,0 +1,28 @@ +UI Recorder test sample project +================ + +It's a UI Recorder test sample project. + +Save your test code here. + +Get more info: [http://uirecorder.com/](http://uirecorder.com/) + +How to run test case? +================ + +1. npm install +2. source run.sh ( Linux|Mac ) or run.bat ( Windows ) + +How to dock jenkins? +================ + +1. Add commands + + source ./install.sh + source ./run.sh + +2. Add reports + + > JUnit: reports/index.xml + + > HTML: reports/ diff --git a/selenium/commons/commons.md b/selenium/commons/commons.md new file mode 100644 index 00000000..730f82ee --- /dev/null +++ b/selenium/commons/commons.md @@ -0,0 +1 @@ +Please save common test case here. \ No newline at end of file diff --git a/selenium/config.json b/selenium/config.json new file mode 100644 index 00000000..6a55c28d --- /dev/null +++ b/selenium/config.json @@ -0,0 +1,23 @@ +{ + "webdriver": { + "host": "127.0.0.1", + "port": "4444", + "browsers": "chrome" + }, + "vars": { + "LoginUsername": "root", + "LoginPassword": "root", + "ShortTextTitle": "SeleniumShortText", + "Profile_NewFirstName": "SeleniumUser_FirstName", + "Profile_NewLastName": "SeleniumUser_LastName", + "Profile_OldFirstName": "Admin", + "Profile_OldLastName": "Account", + "Profile_NewInvalidEmail": "SeleniumInvalidEmail" + }, + "recorder": { + "pathAttrs": "data-id,data-name,type,data-type,role,data-role,data-value", + "attrValueBlack": "", + "classValueBlack": "", + "hideBeforeExpect": "" + } +} \ No newline at end of file diff --git a/selenium/hosts b/selenium/hosts new file mode 100644 index 00000000..e69de29b diff --git a/selenium/install.sh b/selenium/install.sh new file mode 100644 index 00000000..67759d80 --- /dev/null +++ b/selenium/install.sh @@ -0,0 +1,4 @@ +ls ~/nvm || git clone https://github.com/creationix/nvm.git ~/nvm +source ~/nvm/nvm.sh +nvm install v7.10.0 +npm install diff --git a/selenium/package.json b/selenium/package.json new file mode 100644 index 00000000..ae2c41f2 --- /dev/null +++ b/selenium/package.json @@ -0,0 +1,24 @@ +{ + "name": "uirecorderTest", + "version": "1.0.0", + "description": "", + "main": "", + "dependencies": { + "chai": "3.5.0", + "jwebdriver": "2.2.4", + "mocha": "3.1.2", + "mocha-parallel-tests": "1.2.4", + "mochawesome-uirecorder": "1.5.22", + "resemblejs-node": "1.0.0", + "selenium-standalone": "6.x.x" + }, + "devDependencies": {}, + "scripts": { + "installdriver": "./node_modules/.bin/selenium-standalone install --drivers.firefox.baseURL=http://npm.taobao.org/mirrors/geckodriver --baseURL=http://npm.taobao.org/mirrors/selenium --drivers.chrome.baseURL=http://npm.taobao.org/mirrors/chromedriver --drivers.ie.baseURL=http://npm.taobao.org/mirrors/selenium", + "server": "./node_modules/.bin/selenium-standalone start", + "test": "./node_modules/.bin/mocha \"*/**/*.spec.js\" --reporter mochawesome-uirecorder --bail", + "singletest": "./node_modules/.bin/mocha --reporter mochawesome-uirecorder --bail", + "paralleltest": "./node_modules/.bin/mocha-parallel-tests \"*/**/*.spec.js\" --reporter mochawesome-uirecorder --max-parallel 5 --bail" + }, + "author": "" +} diff --git a/selenium/run.bat b/selenium/run.bat new file mode 100644 index 00000000..da1ec242 --- /dev/null +++ b/selenium/run.bat @@ -0,0 +1,7 @@ +@echo off + +if "%1" neq "" ( + npm run singletest %1 %2 +) else ( + npm run paralleltest +) diff --git a/selenium/run.sh b/selenium/run.sh new file mode 100755 index 00000000..6f479e13 --- /dev/null +++ b/selenium/run.sh @@ -0,0 +1,5 @@ +if [ "$1" = "" ]; then + npm run paralleltest +else + npm run singletest $1 $2 +fi diff --git a/selenium/test/loginAndChangeProfile.js b/selenium/test/loginAndChangeProfile.js new file mode 100644 index 00000000..fdf5c0d9 --- /dev/null +++ b/selenium/test/loginAndChangeProfile.js @@ -0,0 +1,378 @@ +const fs = require('fs'); +const path = require('path'); +const chai = require("chai"); +const should = chai.should(); +const JWebDriver = require('jwebdriver'); +chai.use(JWebDriver.chaiSupportChainPromise); +const resemble = require('resemblejs-node'); +resemble.outputSettings({ + errorType: 'flatDifferenceIntensity' +}); + +const rootPath = getRootPath(); + +module.exports = function(){ + + let driver, testVars; + + before(function(){ + let self = this; + driver = self.driver; + testVars = self.testVars; + }); + + it('url: http://localhost:5000', async function(){ + await driver.url(_(`http://localhost:5000`)); + }); + + it('waitBody: ', async function(){ + await driver.sleep(500).wait('body', 30000).html().then(function(code){ + isPageError(code).should.be.false; + }); + }); + + it('insertVar: username ( #username, {{LoginUsername}} )', async function(){ + await driver.sleep(300).wait('#username', 30000) + .val(_(`{{LoginUsername}}`)); + }); + + it('insertVar: password ( #password, {{LoginUsername}} )', async function(){ + await driver.sleep(300).wait('#password', 30000) + .val(_(`{{LoginUsername}}`)); + }); + + it('expect: displayed, .btn-signup, equal, true', async function(){ + await driver.sleep(300).wait('.btn-signup', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('click: Sign in ( button, 174, 18, 0 )', async function(){ + await driver.sleep(300).wait('button.btn-signup', 30000) + .sleep(300).click(); + }); + + it('expect: displayed, div.new-button, equal, true', async function(){ + await driver.sleep(300).wait('div.new-button', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('expect: displayed, a.dropdown-toggle, equal, true', async function(){ + await driver.sleep(300).wait('a.dropdown-toggle', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('click: My Settings ( a.dropdown-toggle )', async function(){ + await driver.sleep(300).wait('a.dropdown-toggle', 30000) + .sleep(300).click(); + }); + + it('× expect: display, ul.dropdown-menu > li:nth-child(1) > a.ng-binding, equal, true', async function(){ + await driver.sleep(300).wait('ul.dropdown-menu > li:nth-child(1) > a.ng-binding', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('× expect: display, ul.dropdown-menu > li:nth-child(3) > a.ng-binding, equal, true', async function(){ + await driver.sleep(300).wait('ul.dropdown-menu > li:nth-child(3) > a.ng-binding', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('click: Edit Profile ( ul.dropdown-menu > li:nth-child(1) > a.ng-binding )', async function(){ + await driver.sleep(300).wait('ul.dropdown-menu > li:nth-child(1) > a.ng-binding', 30000) + .sleep(300).click(); + }); + + it('waitBody: ', async function(){ + await driver.sleep(500).wait('body', 30000).html().then(function(code){ + isPageError(code).should.be.false; + }); + }); + + it('× insertVar: firstName ( #firstName, {{Profile_NewFirstName}} )', async function(){ + await driver.sleep(300).wait('#firstName', 30000) + .val(_(`{{Profile_NewFirstName}}`)); + }); + + it('× insertVar: lastName ( #lastName, {{Profile_NewLastName}} )', async function(){ + await driver.sleep(300).wait('#lastName', 30000) + .val(_(`{{Profile_NewLastName}}`)); + }); + + it('× click: Save Changes ( button.btn-signup )', async function(){ + await driver.sleep(300).wait('button.btn-signup', 30000) + .sleep(300).click(); + }); + + it('× expect: displayed, div.text-success, equal, true', async function(){ + await driver.sleep(300).wait('div.text-success', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + it('× expect: displayed, .text-danger, notEqual, true', async function(){ + await driver.sleep(300).wait('.text-danger', 300) + .displayed() + .should.not.be.a('error') + .should.not.equal(_(true)); + }); + + /* + ** Revert back to expected names + */ + + it('× insertVar: firstName ( #firstName, {{Profile_OldFirstName}} )', async function(){ + await driver.sleep(300).wait('#firstName', 30000) + .val(_(`{{Profile_OldFirstName}}`)); + }); + + it('× insertVar: lastName ( #lastName, {{Profile_OldLastName}} )', async function(){ + await driver.sleep(300).wait('#lastName', 30000) + .val(_(`{{Profile_OldLastName}}`)); + }); + + it('× click: Save Changes ( button.btn-signup, 95, 10, 0 )', async function(){ + await driver.sleep(300).wait('button.btn-signup', 30000) + .sleep(300).click(); + }); + + it('× expect: displayed, .text-danger, notEqual, true', async function(){ + await driver.sleep(300).wait('.text-danger', 300) + .displayed() + .should.not.be.a('error') + .should.not.equal(_(true)); + }); + + + //Check that we can't save an invalid email + it('× insertVar: email ( #email, {{Profile_NewInvalidEmail}} )', async function(){ + await driver.sleep(300).wait('#email', 30000) + .val(_(`{{Profile_NewInvalidEmail}}`)); + }); + + it('× click: Save Changes ( button.btn-signup )', async function(){ + await driver.sleep(300).wait('button.btn-signup', 30000) + .sleep(300).click(); + }); + + it('url: http://localhost:5000/#!/settings/profile', async function(){ + await driver.url(_(`http://localhost:5000/#!/settings/profile`)); + }); + + it('waitBody: ', async function(){ + await driver.sleep(500).wait('body', 30000).html().then(function(code){ + isPageError(code).should.be.false; + }); + }); + + it('expect: text, #email, notEqual, {{Profile_NewInvalidEmail}}', async function(){ + await driver.sleep(300).wait('#email', 300) + .text() + .should.not.be.a('error') + .should.not.equal(_(`{{Profile_NewInvalidEmail}}`)); + }); + + + /* + ** Logout + */ + it('click: Signout ( //a[text()="Signout"], 31, 31, 0 )', async function(){ + await driver.sleep(300).wait('//a[text()="Signout"]', 30000) + .sleep(300).mouseMove(31, 31).click(0); + }); + + it('expect: displayed, button.btn-signup, equal, true', async function(){ + await driver.sleep(300).wait('button.btn-signup', 30000) + .displayed() + .should.not.be.a('error') + .should.equal(_(true)); + }); + + function _(str){ + if(typeof str === 'string'){ + return str.replace(/\{\{(.+?)\}\}/g, function(all, key){ + return testVars[key] || ''; + }); + } + else{ + return str; + } + } + +}; + +if(module.parent && /mocha\.js/.test(module.parent.id)){ + runThisSpec(); +} + +function runThisSpec(){ + // read config + let webdriver = process.env['webdriver'] || ''; + let proxy = process.env['wdproxy'] || ''; + let config = require(rootPath + '/config.json'); + let webdriverConfig = Object.assign({},config.webdriver); + let host = webdriverConfig.host; + let port = webdriverConfig.port || 4444; + let match = webdriver.match(/([^\:]+)(?:\:(\d+))?/); + if(match){ + host = match[1] || host; + port = match[2] || port; + } + let testVars = config.vars; + let browsers = webdriverConfig.browsers; + browsers = browsers.replace(/^\s+|\s+$/g, ''); + delete webdriverConfig.host; + delete webdriverConfig.port; + delete webdriverConfig.browsers; + + // read hosts + let hostsPath = rootPath + '/hosts'; + let hosts = ''; + if(fs.existsSync(hostsPath)){ + hosts = fs.readFileSync(hostsPath).toString(); + } + let specName = path.relative(rootPath, __filename).replace(/\\/g,'/').replace(/\.js$/,''); + + browsers.split(/\s*,\s*/).forEach(function(browserName){ + let caseName = specName + ' : ' + browserName; + + let browserInfo = browserName.split(' '); + browserName = browserInfo[0]; + let browserVersion = browserInfo[1]; + + describe(caseName, function(){ + + this.timeout(600000); + this.slow(1000); + + let driver; + before(function(){ + let self = this; + let driver = new JWebDriver({ + 'host': host, + 'port': port + }); + let sessionConfig = Object.assign({}, webdriverConfig, { + 'browserName': browserName, + 'version': browserVersion, + 'ie.ensureCleanSession': true, + 'chromeOptions': { + 'args': ['--enable-automation'] + } + }); + if(proxy){ + sessionConfig.proxy = { + 'proxyType': 'manual', + 'httpProxy': proxy, + 'sslProxy': proxy + } + } + else if(hosts){ + sessionConfig.hosts = hosts; + } + self.driver = driver.session(sessionConfig).maximize().config({ + pageloadTimeout: 30000, // page onload timeout + scriptTimeout: 5000, // sync script timeout + asyncScriptTimeout: 10000 // async script timeout + }); + self.testVars = testVars; + let casePath = path.dirname(caseName); + self.screenshotPath = rootPath + '/screenshots/' + casePath; + self.diffbasePath = rootPath + '/diffbase/' + casePath; + self.caseName = caseName.replace(/.*\//g, '').replace(/\s*[:\.\:\-\s]\s*/g, '_'); + mkdirs(self.screenshotPath); + mkdirs(self.diffbasePath); + self.stepId = 0; + return self.driver; + }); + + module.exports(); + + beforeEach(function(){ + let self = this; + self.stepId ++; + if(self.skipAll){ + self.skip(); + } + }); + + afterEach(async function(){ + let self = this; + let currentTest = self.currentTest; + let title = currentTest.title; + if(currentTest.state === 'failed' && /^(url|waitBody|switchWindow|switchFrame):/.test(title)){ + self.skipAll = true; + } + if(!/^(closeWindow):/.test(title)){ + let filepath = self.screenshotPath + '/' + self.caseName + '_' + self.stepId; + let driver = self.driver; + try{ + // catch error when get alert msg + await driver.getScreenshot(filepath + '.png'); + let url = await driver.url(); + let html = await driver.source(); + html = '\n' + html; + fs.writeFileSync(filepath + '.html', html); + let cookies = await driver.cookies(); + fs.writeFileSync(filepath + '.cookie', JSON.stringify(cookies)); + } + catch(e){} + } + }); + + after(function(){ + return this.driver.close(); + }); + + }); + }); +} + +function getRootPath(){ + let rootPath = path.resolve(__dirname); + while(rootPath){ + if(fs.existsSync(rootPath + '/config.json')){ + break; + } + rootPath = rootPath.substring(0, rootPath.lastIndexOf(path.sep)); + } + return rootPath; +} + +function mkdirs(dirname){ + if(fs.existsSync(dirname)){ + return true; + }else{ + if(mkdirs(path.dirname(dirname))){ + fs.mkdirSync(dirname); + return true; + } + } +} + +function callSpec(name){ + try{ + require(rootPath + '/' + name)(); + } + catch(e){ + console.log(e) + process.exit(1); + } +} + +function isPageError(code){ + return code == '' || / jscontent="errorCode" jstcache="\d+"|diagnoseConnectionAndRefresh|dnserror_unavailable_header|id="reportCertificateErrorRetry"|400 Bad Request|403 Forbidden|404 Not Found|500 Internal Server Error|502 Bad Gateway|503 Service Temporarily Unavailable|504 Gateway Time-out/i.test(code); +} + +function catchError(error){ + +} diff --git a/server.js b/server.js index 276531b5..4edc6fed 100755 --- a/server.js +++ b/server.js @@ -1,19 +1,9 @@ 'use strict'; + /** * Module dependencies. */ - -require('dotenv').config({path: './.env'}); - -if(!process.env.NODE_ENV){ - process.env.NODE_ENV = 'development'; -} - - -require('events').EventEmitter.prototype._maxListeners = 0; - -var config = require('./config/config'), - mongoose = require('mongoose'), +var mongoose = require('mongoose'), chalk = require('chalk'), nodemailer = require('nodemailer'); @@ -21,66 +11,80 @@ var config = require('./config/config'), * Main application entry file. * Please note that the order of loading is important. */ - -// Bootstrap db connection -var db = mongoose.connect(config.db.uri, config.db.options, function (err) { - if (err) { - console.error(chalk.red('Could not connect to MongoDB!')); - console.log(chalk.red(err)); +var bootstrap = function() { + //Don't check .env file if we are in travis-ci + if(!process.env.TRAVIS) { + require('dotenv').config({path: './.env'}); } -}); -mongoose.connection.on('error', function (err) { - console.error(chalk.red('MongoDB connection error: ' + err)); - process.exit(-1); -}); -const smtpTransport = nodemailer.createTransport(config.mailer.options); - -// verify connection configuration on startup -smtpTransport.verify(function(error, success) { - if (error) { - console.error(chalk.red('Your mail configuration is incorrect: ' + error)); - // verify but to abort! - // process.exit(-1); + if(!process.env.NODE_ENV) { + process.env.NODE_ENV = 'development'; } -}); -// Init the express application -var app = require('./config/express')(db); + var config = require('./config/config'); -//Create admin account -if (process.env.CREATE_ADMIN === 'TRUE') { - var create_admin = require('./scripts/create_admin'); - - create_admin.run(app, db, function(err){ - if(err){ - console.error(chalk.red('Could not create Admin Account: ' + err)); + // Bootstrap db connection + var db = mongoose.connect(config.db.uri, config.db.options, function (err) { + if (err) { + console.error(chalk.red('Could not connect to MongoDB!')); + console.log(chalk.red(err)); } }); + mongoose.connection.on('error', function (err) { + console.error(chalk.red('MongoDB connection error: ' + err)); + process.exit(-1); + }); + + const smtpTransport = nodemailer.createTransport(config.mailer.options); + + // verify connection configuration on startup + smtpTransport.verify(function(error, success) { + if (error) { + console.error(chalk.red('Your mail configuration is incorrect: ' + error)); + // verify but to abort! + // process.exit(-1); + } + }); + + // Init the express application + var app = require('./config/express')(db); + + //Create admin account +if (process.env.CREATE_ADMIN === 'TRUE') { + var create_admin = require('./scripts/create_admin'); + + create_admin.run(app, db, function(err){ + if(err){ + console.error(chalk.red('Could not create Admin Account: ' + err)); + } + }); + } + + // Bootstrap passport config + require('./config/passport')(); + + // Start the app by listening on + app.listen(config.port); + + // Logging initialization + console.log('--'); + console.log(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV)); + console.log(chalk.green('Port:\t\t\t\t' + config.port)); + console.log(chalk.green('Database:\t\t\t' + config.db.uri)); + console.log('--'); + + process.on('uncaughtException', function (err) { + console.error((new Date()).toUTCString() + ' uncaughtException:', err.message); + console.error(err.stack); + process.exit(1); + }); + + return app; +}; + +// To maintain backwards compatibility, run bootstrap when called as a file +if(require.main === module) { + bootstrap(); +} else { + module.exports = bootstrap(); } - - -// Bootstrap passport config -require('./config/passport')(); - -// Start the app by listening on -app.listen(config.port); - -// Expose app -exports = module.exports = app; - -// Logging initialization -console.log('--'); -console.log(chalk.green('Environment:\t\t\t' + process.env.NODE_ENV)); -console.log(chalk.green('Port:\t\t\t\t' + config.port)); -console.log(chalk.green('Database:\t\t\t' + config.db.uri)); -if (process.env.NODE_ENV === 'secure') { - console.log(chalk.green('HTTPs:\t\t\t\ton')); -} -console.log('--'); - -process.on('uncaughtException', function (err) { - console.error((new Date()).toUTCString() + ' uncaughtException:', err.message); - console.error(err.stack); - process.exit(1); -}); diff --git a/start.js b/start.js new file mode 100644 index 00000000..d1458d0e --- /dev/null +++ b/start.js @@ -0,0 +1,15 @@ +var fs = require('fs'), + setup = require('./scripts/setup'); + +//Set this to infinity to increase server capacity +require('events').EventEmitter.prototype._maxListeners = 0; + + +//Run setup script if no .env file is detected +if(process.stdout.isTTY) { + setup.checkENVAndRunSetup(function() { + require('./server'); + }); +} else { + require('./server'); +} diff --git a/start.sh b/start.sh deleted file mode 100755 index 846f7b1b..00000000 --- a/start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -## TODO: Reconsider this as I have no idea what the point of it is. -node server.js -#pm2 start process.yml