diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..0bbf9fa8
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+.git
+.idea
+.vagrant
+coverage
+design
+e2e_coverage
+Vagrantfile
+Procfile
diff --git a/Dockerfile b/Dockerfile
index 25f20cb9..6b145fd1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,25 +1,17 @@
# Build:
-# docker build -t tellform -f ./Dockerfile .
+# docker build -t tellform-prod -f ./Dockerfile-production .
#
# Run:
-# docker run -it tellform
+# docker run -it tellform-prod
+
FROM phusion/baseimage:0.9.19
MAINTAINER David Baldwynn
${URL}
', + subject: '✔ Activate your new TellForm account!', + html: verificationEmail, text: 'Please verify your account by clicking the following link, or by copying and pasting it into your browser: ${URL}' }, confirmMailOptions: { from: config.mailer.from, - subject: 'Account successfully verified!', - html: 'Your account has been successfully verified.
', + subject: '✔ Welcome to {{app.title}}!', + html: welcomeEmail, text: 'Your account has been successfully verified.' }, verifySendMailCallback: function(err, info) { diff --git a/app/controllers/users/users.password.server.controller.js b/app/controllers/users/users.password.server.controller.js index a9f2596e..1eff3a0b 100755 --- a/app/controllers/users/users.password.server.controller.js +++ b/app/controllers/users/users.password.server.controller.js @@ -18,7 +18,7 @@ var smtpTransport = nodemailer.createTransport(config.mailer.options); /** * Forgot for reset password (forgot POST) */ -exports.forgot = function(req, res, next) { +exports.forgot = function(req, res) { async.waterfall([ // Generate random token function(done) { @@ -81,22 +81,33 @@ exports.forgot = function(req, res, next) { subject: 'Password Reset', html: emailHTML }; - smtpTransport.sendMail(mailOptions, function(err) { - if (!err) { - res.send({ - message: 'An email has been sent to ' + user.email + ' with further instructions.' - }); - } else { - return res.status(400).send({ - message: 'Failure sending email' - }); - } - done(err); + var userEmail = user.email; + var user = userEmail.split('@')[0]; + var domain = userEmail.split('@')[1]; + + var obfuscatedUser = user.substring(0, 1) + user.substring(1).replace(/./g, '*'); + var domainName = domain.split('.')[0]; + var tld = domain.split('.')[1]; + + var obfuscatedDomainName = domainName.replace(/./g, '*'); + var obfuscatedEmail = obfuscatedUser + '@' + obfuscatedDomainName + '.' + tld; + + smtpTransport.sendMail(mailOptions, function(err) { + done(err, obfuscatedEmail); + }); + } + ], function(err, obfuscatedEmail) { + if (err) { + console.log(err); + return res.status(400).send({ + message: 'Couldn\'t send reset password email due to internal server errors. Please contact support at team@tellform.com.' + }); + } else { + return res.send({ + message: 'An email has been sent to ' + obfuscatedEmail + ' with further instructions.' }); } - ], function(err) { - if (err) return next(err); }); }; diff --git a/app/libs/timestamp.server.plugin.js b/app/libs/timestamp.server.plugin.js new file mode 100644 index 00000000..084e2c1b --- /dev/null +++ b/app/libs/timestamp.server.plugin.js @@ -0,0 +1,39 @@ +'use strict'; + +// Plugin +module.exports = function timestamp (schema, options) { + options || (options = {}) + + // Options + 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 } + } + if (useVirtual) { + // Use the ObjectID for extracting the created time + schema.virtual(createdPath).get(function () { + return new Date(this._id.generationTime * 1000) + }) + } else { + if (!schema.paths[createdPath]) { + fields[createdPath] = { + type: Date + , default: Date.now + } + } + } + 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 diff --git a/app/models/form.server.model.js b/app/models/form.server.model.js index 570d4714..3252489c 100644 --- a/app/models/form.server.model.js +++ b/app/models/form.server.model.js @@ -6,7 +6,7 @@ var mongoose = require('mongoose'), Schema = mongoose.Schema, _ = require('lodash'), - mUtilities = require('mongoose-utilities'), + timeStampPlugin = require('../libs/timestamp.server.plugin'), async = require('async'), Random = require('random-js'), mt = Random.engines.mt19937(); @@ -292,7 +292,7 @@ FormSchema.virtual('analytics.fields').get(function () { return fieldDropoffs; }); -FormSchema.plugin(mUtilities.timestamp, { +FormSchema.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', useVirtual: false @@ -313,7 +313,6 @@ FormSchema.pre('save', function (next) { this.language = 'de'; break; default: - this.language = 'en'; break; } next(); @@ -345,54 +344,55 @@ FormSchema.pre('save', function (next) { var that = this; var _original; - async.series([function(cb) { - that.constructor - .findOne({_id: that._id}).exec(function (err, original) { - if (err) { - return cb(err); - } else if (!original){ - return next(); - } else { - _original = original; - return cb(null); - } - }); - }, - function(cb) { - if(that.form_fields && that.isModified('form_fields') && formFieldsAllHaveIds(that.toObject().form_fields)){ + async.series([ + function(cb) { + that.constructor + .findOne({_id: that._id}).exec(function (err, original) { + if (err) { + return cb(err); + } else if (!original){ + return next(); + } else { + _original = original; + return cb(null); + } + }); + }, + function(cb) { + if(that.form_fields && that.isModified('form_fields') && formFieldsAllHaveIds(that.toObject().form_fields)){ - var current_form = that.toObject(), - old_form_fields = _original.toObject().form_fields, - new_ids = _.map(_.map(current_form.form_fields, 'globalId'), function(id){ return ''+id;}), - old_ids = _.map(_.map(old_form_fields, 'globalId'), function(id){ return ''+id;}), - deletedIds = getDeletedIndexes(old_ids, new_ids); + var current_form = that.toObject(), + old_form_fields = _original.toObject().form_fields, + new_ids = _.map(_.map(current_form.form_fields, 'globalId'), function(id){ return ''+id;}), + old_ids = _.map(_.map(old_form_fields, 'globalId'), function(id){ return ''+id;}), + deletedIds = getDeletedIndexes(old_ids, new_ids); - //Check if any form_fileds were deleted - if( deletedIds.length > 0 ){ + //Check if any form_fileds were deleted + if( deletedIds.length > 0 ){ - var modifiedSubmissions = []; + var modifiedSubmissions = []; - async.forEachOfSeries(deletedIds, - function (deletedIdIndex, key, cb_id) { + 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); - } + 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); - } + //Preserve fields that have at least one submission + if (submissions.length) { + //Add submissions + modifiedSubmissions.push.apply(modifiedSubmissions, submissions); + } - return cb_id(null); - }); - }, + return cb_id(null); + }); + }, function (err) { if(err){ console.error(err.message); @@ -434,17 +434,20 @@ FormSchema.pre('save', function (next) { }, function (err) { return cb(err); }); - } - ); + }); + } else { + return cb(null); + } } else { return cb(null); } - } else { - return cb(null); } - }], - function(err, results){ - next(err); + ], + function(err){ + if(err){ + return next(err); + } + next(); }); }); diff --git a/app/models/form_field.server.model.js b/app/models/form_field.server.model.js index 71704614..31f96e71 100644 --- a/app/models/form_field.server.model.js +++ b/app/models/form_field.server.model.js @@ -5,13 +5,11 @@ */ var mongoose = require('mongoose'), util = require('util'), - mUtilities = require('mongoose-utilities'), + timeStampPlugin = require('../libs/timestamp.server.plugin'), _ = require('lodash'), Schema = mongoose.Schema, - LogicJumpSchema = require('./logic_jump.server.model'); - -const UIDGenerator = require('uid-generator'); -const uidgen3 = new UIDGenerator(256, UIDGenerator.BASE62); + LogicJumpSchema = require('./logic_jump.server.model'), + tokgen = require('../libs/tokenGenerator'); var FieldOptionSchema = new Schema({ option_id: { @@ -76,8 +74,7 @@ function BaseFieldSchema(){ }, title: { type: String, - trim: true, - required: 'Field Title cannot be blank' + trim: true }, description: { type: String, @@ -106,7 +103,6 @@ function BaseFieldSchema(){ }, fieldType: { type: String, - required: true, enum: [ 'textfield', 'date', @@ -134,7 +130,7 @@ function BaseFieldSchema(){ fieldValue: Schema.Types.Mixed }); - this.plugin(mUtilities.timestamp, { + this.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', useVirtual: false @@ -196,11 +192,8 @@ FormFieldSchema.pre('validate', function(next) { //LogicJump Save FormFieldSchema.pre('save', function(next) { - if(this.logicJump && this.logicJump.fieldA) { - if(this.logicJump.jumpTo === '') delete this.logicJump.jumpTo; - } if(!this.globalId){ - this.globalId = uidgen3.generateSync(); + this.globalId = tokgen(); } next(); }); diff --git a/app/models/form_submission.server.model.js b/app/models/form_submission.server.model.js index e2029e4e..a3505f3a 100644 --- a/app/models/form_submission.server.model.js +++ b/app/models/form_submission.server.model.js @@ -5,17 +5,13 @@ */ var mongoose = require('mongoose'), Schema = mongoose.Schema, - mUtilities = require('mongoose-utilities'), + timeStampPlugin = require('../libs/timestamp.server.plugin'), FieldSchema = require('./form_field.server.model.js'); /** * Form Submission Schema */ var FormSubmissionSchema = new Schema({ - title: { - type: String - }, - form_fields: [FieldSchema], form: { @@ -58,6 +54,19 @@ FormSubmissionSchema.pre('save', function (next) { if(this.form_fields[i].fieldType === 'dropdown'){ 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; } next(); }); @@ -68,6 +77,17 @@ 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; } @@ -75,7 +95,7 @@ FormSubmissionSchema.path('form_fields', { } }); -FormSubmissionSchema.plugin(mUtilities.timestamp, { +FormSubmissionSchema.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', useVirtual: false diff --git a/app/models/logic_jump.server.model.js b/app/models/logic_jump.server.model.js index c8d39b82..0226aad8 100644 --- a/app/models/logic_jump.server.model.js +++ b/app/models/logic_jump.server.model.js @@ -44,6 +44,10 @@ var LogicJumpSchema = new Schema({ jumpTo: { type: Schema.Types.ObjectId, ref: 'FormField' + }, + enabled: { + type: Schema.Types.Boolean, + default: false } }, schemaOptions); diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index a22f53d4..98e2d613 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -7,8 +7,7 @@ var mongoose = require('mongoose'), Schema = mongoose.Schema, crypto = require('crypto'), config = require('../../config/config'), - fs = require('fs-extra'), - mUtilities = require('mongoose-utilities'), + timeStampPlugin = require('../libs/timestamp.server.plugin'), path = require('path'), querystring = require('querystring'), nodemailer = require('nodemailer'); @@ -143,7 +142,7 @@ UserSchema.virtual('displayName').get(function () { return this.firstName + ' ' + this.lastName; }); -UserSchema.plugin(mUtilities.timestamp, { +UserSchema.plugin(timeStampPlugin, { createdPath: 'created', modifiedPath: 'lastModified', useVirtual: false diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js index 06b5b0f3..f99ddc95 100755 --- a/app/routes/users.server.routes.js +++ b/app/routes/users.server.routes.js @@ -34,34 +34,7 @@ module.exports = function(app) { app.route('/auth/signout').get(users.signout); app.route('/auth/genkey').get(users.requiresLogin, users.generateAPIKey); - - // // Setting the facebook oauth routes - // app.route('/auth/facebook').get(passport.authenticate('facebook', { - // scope: ['email'] - // })); - // app.route('/auth/facebook/callback').get(users.oauthCallback('facebook')); - - // // Setting the twitter oauth routes - // app.route('/auth/twitter').get(passport.authenticate('twitter')); - // app.route('/auth/twitter/callback').get(users.oauthCallback('twitter')); - - // // Setting the google oauth routes - // app.route('/auth/google').get(passport.authenticate('google', { - // scope: [ - // 'https://www.googleapis.com/auth/userinfo.profile', - // 'https://www.googleapis.com/auth/userinfo.email' - // ] - // })); - // app.route('/auth/google/callback').get(users.oauthCallback('google')); - - // // Setting the linkedin oauth routes - // app.route('/auth/linkedin').get(passport.authenticate('linkedin')); - // app.route('/auth/linkedin/callback').get(users.oauthCallback('linkedin')); - - // // Setting the github oauth routes - // app.route('/auth/github').get(passport.authenticate('github')); - // app.route('/auth/github/callback').get(users.oauthCallback('github')); - + // 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 bb5f8ab2..993a0468 100644 --- a/app/sockets/analytics_service.js +++ b/app/sockets/analytics_service.js @@ -5,13 +5,14 @@ */ var mongoose = require('mongoose'), errorHandler = require('../controllers/errors.server.controller'), - Form = mongoose.model('Form'); + Form = mongoose.model('Form'), + request = require('request'); // Create the chat configuration module.exports = function (io, socket) { var visitorsData = {}; - var saveVisitorData = function (data, cb){ + var saveVisitorData = function (data, socket, cb){ Form.findById(data.formId, function(err, form) { if (err) { console.error(err); @@ -25,22 +26,25 @@ module.exports = function (io, socket) { timeElapsed: data.timeElapsed, isSubmitted: data.isSubmitted, language: data.language, - ipAddr: data.ipAddr, + ipAddr: '', deviceType: data.deviceType }; form.analytics.visitors.push(newVisitor); - form.save(function (formSaveErr) { - if (err) { - console.error(err); - throw new Error(errorHandler.getErrorMessage(formSaveErr)); - } - if(cb){ - return cb(); - } - }); + 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(); + } + }); }); }; @@ -50,6 +54,8 @@ module.exports = function (io, socket) { visitorsData[current_socket.id] = data; 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() { @@ -71,3 +77,4 @@ module.exports = function (io, socket) { }); }); }; + diff --git a/app/tests/form.server.model.test.js b/app/tests/form.server.model.test.js index 697fe87e..573e631b 100644 --- a/app/tests/form.server.model.test.js +++ b/app/tests/form.server.model.test.js @@ -1,5 +1,7 @@ 'use strict'; +require('../../server.js'); + /** * Module dependencies. */ diff --git a/app/tests/form.server.routes.test.js b/app/tests/form.server.routes.test.js index c2f7cb4e..e1aa2654 100644 --- a/app/tests/form.server.routes.test.js +++ b/app/tests/form.server.routes.test.js @@ -1,5 +1,4 @@ 'use strict'; -process.env.NODE_ENV = 'test'; var should = require('should'), lodash = require('lodash'), @@ -19,8 +18,8 @@ var user, myForm, userSession; // Create user credentials var credentials = { - username: 'test1234', - email: 'test1234@test.com', + username: 'aeokjqjqkqaeoaoe', + email: 'aeoaekjqjqqjkoeoa@test.com', password: 'password' }; @@ -30,18 +29,17 @@ var credentials = { describe('Form Routes Unit tests', function() { beforeEach(function(done) { - // Create a new user user = new User({ firstName: 'Full', lastName: 'Name', - displayName: 'Full Name', email: credentials.email, username: credentials.username, password: credentials.password, provider: 'local' }); + // Save a user to the test db and create new Form user.save(function(err) { should.not.exist(err); @@ -65,10 +63,12 @@ describe('Form Routes Unit tests', function() { }); it(' > should not be able to create a Form if not logged in', function(done) { + userSession.post('/forms') .send({form: myForm}) .expect(401) .end(function(FormSaveErr, FormSaveRes) { + // Call the assertion callback done(FormSaveErr); }); @@ -153,7 +153,7 @@ describe('Form Routes Unit tests', function() { // Save a new Form authenticatedSession.post('/forms') .send({form: myForm}) - .expect(405) + .expect(500) .end(function(FormSaveErr, FormSaveRes) { // Handle Form save error if (FormSaveErr) { diff --git a/app/tests/form_submission.model.test.js b/app/tests/form_submission.model.test.js index de87d09f..f60e599e 100644 --- a/app/tests/form_submission.model.test.js +++ b/app/tests/form_submission.model.test.js @@ -168,7 +168,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, admin: user, form_fields: {$elemMatch: {_id: myForm.form_fields[0]._id} } }) + FormSubmission.findOne({ form: myForm._id, form_fields: {$elemMatch: {globalId: myForm.form_fields[0].globalId} } }) .exec(function(err, submission){ should.not.exist(err); should.exist(submission); diff --git a/app/tests/form_submission.routes.test.js b/app/tests/form_submission.routes.test.js index 9cde8744..13d5e780 100644 --- a/app/tests/form_submission.routes.test.js +++ b/app/tests/form_submission.routes.test.js @@ -21,7 +21,8 @@ var credentials, user; * Form routes tests */ describe('Form Submission Routes Unit tests', function() { - var FormObj, _Submission, submissionSession; + var FormObj, _Submission, submissionSession, _SubmissionBody + beforeEach(function(done) { @@ -60,15 +61,43 @@ describe('Form Submission Routes Unit tests', function() { if (formSaveErr) done(formSaveErr); _Submission = { - form_fields: [ - {'fieldType':'textfield', 'title':'First Name', 'fieldValue': 'David'}, - {'fieldType':'checkbox', 'title':'nascar', 'fieldValue': true}, - {'fieldType':'checkbox', 'title':'hockey', 'fieldValue': false} - ], form: form._id, - admin: user._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} + ], percentageComplete: 100, - timeElapsed: 11.55 + timeElapsed: 11.55, + ipAddr: '123.233.232.232', + geoLocation: { + Country: 'Canada', + City: 'Vancouver' + }, + device:{ + type: 'Mobile', + name: 'iPhone' + } + }; + + _SubmissionBody ={ + _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} + ], + percentageComplete: 100, + timeElapsed: 11.55, + ipAddr: '123.233.232.232', + geoLocation: { + Country: 'Canada', + City: 'Vancouver' + }, + device:{ + type: 'Mobile', + name: 'iPhone' + } }; FormObj = form; @@ -86,12 +115,11 @@ describe('Form Submission Routes Unit tests', function() { //Create Submission submissionSession.post('/forms/' + FormObj._id) - .send(_Submission) + .send(_SubmissionBody) .expect(200) .end(function(err, res) { should.not.exist(err); - done(); }); }); @@ -99,7 +127,7 @@ describe('Form Submission Routes Unit tests', function() { it(' > should be able to get Form Submissions if signed in', function(done) { //Create Submission submissionSession.post('/forms/' + FormObj._id) - .send(_Submission) + .send(_SubmissionBody) .expect(200) .end(function(err, res) { diff --git a/app/tests/libs/timestamp.server.plugin.test.js b/app/tests/libs/timestamp.server.plugin.test.js new file mode 100644 index 00000000..2901fe72 --- /dev/null +++ b/app/tests/libs/timestamp.server.plugin.test.js @@ -0,0 +1,70 @@ +// Dependencies +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() + + before(function () { + FooModel.remove(function (err) { + 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() + }) + + 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() + }) + }) + }) + + describe('#custom()', function () { + var FooSchema = new Schema() + FooSchema.plugin(timestamp, { + 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) + }) + }) + + it('should have custom properties', function (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 diff --git a/app/views/form.server.view.html b/app/views/form.server.view.html index b07466ed..be1bd664 100644 --- a/app/views/form.server.view.html +++ b/app/views/form.server.view.html @@ -76,11 +76,6 @@ - diff --git a/app/views/templates/reset-password-confirm-email.server.view.html b/app/views/templates/reset-password-confirm-email.server.view.html index bfbcb157..4654ff1f 100755 --- a/app/views/templates/reset-password-confirm-email.server.view.html +++ b/app/views/templates/reset-password-confirm-email.server.view.html @@ -1,13 +1,64 @@ - - - - -Dear {{name}},
- -This is a confirmation that the password for your account has just been changed
-The {{appName}} Support Team
- - + + @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext); + + + + +
+
|
+ |||||||||||||||||
Dear {{name}},
-- You have requested to have your password reset for your account at {{appName}} -
-Please visit this url to reset your password:
-{{url}}
- If you didn't make this request, you can ignore this email. -The {{appName}} Support Team
- - + + @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&subset=latin,latin-ext); + + + + +
+
|
+ |||||||||||||||||
-
|
- |||||||||||||||||
+
|
+ |||||||||||||||||
+
|
+ |||||||||||||||||
| # | {{value.title}} | {{ 'PERCENTAGE_COMPLETE' | translate }} | {{ 'TIME_ELAPSED' | translate }} | {{ 'DEVICE' | translate }} | {{ 'LOCATION' | translate }} | {{ 'IP_ADDRESS' | translate }} | {{ 'DATE_SUBMITTED' | translate }} (UTC) | |
|---|---|---|---|---|---|---|---|---|
| {{$index+1}} | {{field.fieldValue}} | {{row.percentageComplete}}% | {{row.timeElapsed | secondsToDateTime | date:'mm:ss'}} | {{row.device.name}}, {{row.device.type}} | {{row.geoLocation.City}}, {{row.geoLocation.Country}} | {{row.ipAddr}} | {{row.created | date:'yyyy-MM-dd HH:mm:ss'}} |
{{pageData.introParagraph}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}
{{field.description}}