diff --git a/app/controllers/forms.server.controller.js b/app/controllers/forms.server.controller.js index a117a3dc..d800f827 100644 --- a/app/controllers/forms.server.controller.js +++ b/app/controllers/forms.server.controller.js @@ -19,12 +19,12 @@ var mongoose = require('mongoose'), */ exports.uploadPDF = function(req, res, next) { - console.log('inside uploadPDF'); + //console.log('inside uploadPDF'); // console.log('\n\nProperty Descriptor\n-----------'); // console.log(Object.getOwnPropertyDescriptor(req.files.file, 'path')); - console.log(req.file); + //console.log(req.file); if(req.file){ var pdfFile = req.file; @@ -159,9 +159,20 @@ exports.deleteSubmissions = function(req, res) { res.status(400).send({ message: errorHandler.getErrorMessage(err) }); + return; } - res.status(200).send('Form submissions successfully deleted'); + form.analytics.visitors = []; + form.save(function(err){ + if(err){ + res.status(400).send({ + message: errorHandler.getErrorMessage(err) + }); + return; + } + res.status(200).send('Form submissions successfully deleted'); + + }); }); }; @@ -171,8 +182,6 @@ exports.deleteSubmissions = function(req, res) { exports.createSubmission = function(req, res) { var form = req.form; - // console.log('in createSubmission()'); - // console.log(req.body); var submission = new FormSubmission({ admin: req.form.admin._id, @@ -210,10 +219,9 @@ exports.createSubmission = function(req, res) { } submission.save(function(err, submission){ - // console.log('in submissions.save()\n submission: '+JSON.stringify(submission) ) if(err){ console.log(err.message); - res.status(400).send({ + res.status(500).send({ message: errorHandler.getErrorMessage(err) }); } @@ -281,8 +289,9 @@ exports.create = function(req, res) { exports.read = function(req, res) { var validUpdateTypes= Form.schema.path('plugins.oscarhost.settings.updateType').enumValues; - var newForm = JSON.parse(JSON.stringify(req.form)); + var newForm = req.form.toJSON({virtuals : true}); newForm.plugins.oscarhost.settings.validUpdateTypes = validUpdateTypes; + res.json(newForm); }; @@ -377,11 +386,12 @@ exports.formByID = function(req, res, next, id) { } else { //Remove sensitive information from User object - form.admin.password = undefined; - form.admin.salt = undefined; - form.provider = undefined; + var _form = form; + _form.admin.password = undefined; + _form.admin.salt = undefined; + _form.provider = undefined; - req.form = form; + req.form = _form; return next(); } }); diff --git a/app/models/form.server.model.js b/app/models/form.server.model.js index 4fd607ad..0be5d656 100644 --- a/app/models/form.server.model.js +++ b/app/models/form.server.model.js @@ -46,6 +46,21 @@ var ButtonSchema = new Schema({ } }); +var VisitorDataSchema = new Schema({ + referrer: { + type: String + }, + lastActiveField: { + type: Schema.Types.ObjectId + }, + timeElapsed: { + type: Number + }, + isSubmitted: { + type: Boolean + } +}); + /** * Form Schema */ @@ -65,10 +80,15 @@ var FormSchema = new Schema({ type: String, default: '' }, - form_fields: { - type: [FieldSchema] + + analytics:{ + gaCode: { + type: String + }, + visitors: [VisitorDataSchema] }, + form_fields: [FieldSchema], submissions: [{ type: Schema.Types.ObjectId, ref: 'FormSubmission' @@ -195,16 +215,91 @@ var FormSchema = new Schema({ }, auth: { user: { - type: String, + type: String }, pass: { - type: String, + type: String } } } } }); +/* +** In-Form Analytics Virtual Attributes + */ +FormSchema.virtual('analytics.views').get(function () { + return this.analytics.visitors.length; +}); + +FormSchema.virtual('analytics.submissions').get(function () { + return this.submissions.length; +}); + +FormSchema.virtual('analytics.conversionRate').get(function () { + return this.submissions.length/this.analytics.visitors.length*100; +}); + +FormSchema.virtual('analytics.fields').get(function () { + var fieldDropoffs = []; + var visitors = this.analytics.visitors; + var that = this; + + 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 = continueViews/totalViews*100; + var dropoffRate = dropoffViews/totalViews*100; + + fieldDropoffs[i] = { + dropoffViews: dropoffViews, + continueViews: continueViews, + totalViews: totalViews, + continueRate: continueRate, + dropoffRate: dropoffRate, + field: field + }; + + } + } + + return fieldDropoffs; +}); + FormSchema.plugin(mUtilities.timestamp, { createdPath: 'created', modifiedPath: 'lastModified', @@ -397,7 +492,7 @@ FormSchema.pre('save', function (next) { //Find FormSubmissions that contain field with _id equal to 'deleted_id' FormSubmission. - find({ form: that._id, admin: that.admin, form_fields: {$elemMatch: {_id: deleted_id} } }). + find({ form: that._id, admin: that.admin, form_fields: {$elemMatch: {submissionId: deleted_id} } }). exec(function(err, submissions){ if(err) { console.error(err); diff --git a/app/models/form_field.server.model.js b/app/models/form_field.server.model.js index 643295af..1a0452bf 100644 --- a/app/models/form_field.server.model.js +++ b/app/models/form_field.server.model.js @@ -4,6 +4,7 @@ * Module dependencies. */ var mongoose = require('mongoose'), + util = require('util'), mUtilities = require('mongoose-utilities'), _ = require('lodash'), Schema = mongoose.Schema; @@ -23,85 +24,188 @@ var FieldOptionSchema = new Schema({ } }); +var RatingFieldSchema = new Schema({ + steps: { + type: Number, + min: 1, + max: 10 + }, + shape: { + type: String, + enum: [ + 'Heart', + 'Star', + 'thumbs-up', + 'thumbs-down', + 'Circle', + 'Square', + 'Check Circle', + 'Smile Outlined', + 'Hourglass', + 'bell', + 'Paper Plane', + 'Comment', + 'Trash' + ] + }, + validShapes: { + type: [String] + } +}); /** * FormField Schema */ -var FormFieldSchema = new Schema({ - title: { - type: String, - trim: true, - required: 'Field Title cannot be blank' - }, - description: { - type: String, - default: '' - }, +function BaseFieldSchema(){ + Schema.apply(this, arguments); - logicJump: { - type: Schema.Types.ObjectId, - ref: 'LogicJump' - }, + this.add({ + isSubmission: { + type: Boolean, + default: false + }, + submissionId: { + type: Schema.Types.ObjectId + }, + title: { + type: String, + trim: true, + required: 'Field Title cannot be blank' + }, + description: { + type: String, + default: '' + }, - fieldOptions: [FieldOptionSchema], - required: { - type: Boolean, - default: true - }, - disabled: { - type: Boolean, - default: false - }, + logicJump: { + type: Schema.Types.ObjectId, + ref: 'LogicJump' + }, - deletePreserved: { - type: Boolean, - default: false - }, - validFieldTypes: { - type: [String] - }, - fieldType: { - type: String, - required: true, - enum: [ - 'textfield', - 'date', - 'email', - 'link', - 'legal', - 'url', - 'textarea', - 'statement', - 'welcome', - 'thankyou', - 'file', - 'dropdown', - 'scale', - 'rating', - 'radio', - 'checkbox', - 'hidden', - 'yes_no', - 'natural', - 'number' - ] - }, - fieldValue: Schema.Types.Mixed + ratingOptions: { + type: RatingFieldSchema, + required: false, + default: {} + }, + fieldOptions: [FieldOptionSchema], + required: { + type: Boolean, + default: true + }, + disabled: { + type: Boolean, + default: false + }, + + deletePreserved: { + type: Boolean, + default: false + }, + validFieldTypes: { + type: [String] + }, + fieldType: { + type: String, + required: true, + enum: [ + 'textfield', + 'date', + 'email', + 'link', + 'legal', + 'url', + 'textarea', + 'statement', + 'welcome', + 'thankyou', + 'file', + 'dropdown', + 'scale', + 'rating', + 'radio', + 'checkbox', + 'hidden', + 'yes_no', + 'natural', + 'number' + ] + }, + fieldValue: Schema.Types.Mixed + }); + + this.plugin(mUtilities.timestamp, { + createdPath: 'created', + modifiedPath: 'lastModified', + useVirtual: false + }); + + this.pre('save', function (next) { + 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; + } + + next(); + }); +} +util.inherits(BaseFieldSchema, Schema); + +var FormFieldSchema = new BaseFieldSchema(); + +FormFieldSchema.pre('validate', function(next) { + var error = new mongoose.Error.ValidationError(this); + + //If field is rating check that it has ratingOptions + if(this.fieldType !== 'rating'){ + + if(this.ratingOptions && this.ratingOptions.steps && this.ratingOptions.shape){ + error.errors.ratingOptions = new mongoose.Error.ValidatorError({path: 'ratingOptions', message: 'ratingOptions is only allowed for type \'rating\' fields.', type: 'notvalid', value: this.ratingOptions}); + return(next(error)); + } + + }else{ + //Setting default values for ratingOptions + 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; + } + } + + + //If field is multiple choice check that it has field + if(this.fieldType !== 'dropdown' && this.fieldType !== 'radio' && this.fieldType !== 'checkbox'){ + 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}); + return(next(error)); + } + } + + return next(); }); -FormFieldSchema.plugin(mUtilities.timestamp, { - createdPath: 'created', - modifiedPath: 'lastModified', - useVirtual: false -}); +//Submission fieldValue correction +FormFieldSchema.pre('save', function(next) { -FormFieldSchema.pre('save', function (next){ - this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues; - next(); + if(this.fieldType === 'dropdown' && this.isSubmission){ + //console.log(this); + this.fieldValue = this.fieldValue.option_value; + //console.log(this.fieldValue); + } + + return next(); }); -mongoose.model('Field', FormFieldSchema); +var Field = mongoose.model('Field', FormFieldSchema); +var RatingOptions = mongoose.model('RatingOptions', RatingFieldSchema); module.exports = FormFieldSchema; diff --git a/app/models/form_submission.server.model.js b/app/models/form_submission.server.model.js index cf1774ce..a02c9568 100644 --- a/app/models/form_submission.server.model.js +++ b/app/models/form_submission.server.model.js @@ -17,6 +17,8 @@ var mongoose = require('mongoose'), FieldSchema = require('./form_field.server.model.js'), OscarSecurity = require('../../scripts/oscarhost/OscarSecurity'); +var FieldSchema = require('./form_field.server.model.js'); + var newDemoTemplate = { address: '880-9650 Velit. St.', city: '', @@ -41,6 +43,18 @@ var newDemoTemplate = { yearOfBirth: '2015' }; + +// Setter function for form_fields +function formFieldsSetter(form_fields){ + for(var i=0; i + + + {% for bowerJSFile in bowerJSFiles %} @@ -103,7 +107,7 @@ + +
+
+
Google Analytics Tracking Code
+
+ +
+ +
+
Language
diff --git a/public/modules/forms/views/directiveViews/form/edit-form.client.view.html b/public/modules/forms/views/directiveViews/form/edit-form.client.view.html index f2e2130e..dd8fceb3 100644 --- a/public/modules/forms/views/directiveViews/form/edit-form.client.view.html +++ b/public/modules/forms/views/directiveViews/form/edit-form.client.view.html @@ -220,13 +220,13 @@

-
+
Description:

-
+
Options:
@@ -244,6 +244,32 @@
+ +

+
+
Number of Steps:
+
+ +
+
+
Shape:
+
+ +
+
+

diff --git a/public/modules/forms/views/directiveViews/form/edit-submissions-form.client.view.html b/public/modules/forms/views/directiveViews/form/edit-submissions-form.client.view.html index a612e108..c66184f1 100644 --- a/public/modules/forms/views/directiveViews/form/edit-submissions-form.client.view.html +++ b/public/modules/forms/views/directiveViews/form/edit-submissions-form.client.view.html @@ -1,5 +1,47 @@
+
+
+ Total Views: {{myform.analytics.views}} +
+
+ Submissions: {{myform.analytics.submissions}} +
+ +
+ Conversion Rate: {{myform.analytics.conversionRate}}% +
+
+
+
+
+ +
+ Field Title +
+
+ Field Views +
+ +
+ User dropoff rate at this field +
+
+
+ +
+ {{fieldStats.field.title}} +
+
+ {{fieldStats.totalViews}} +
+ +
+ {{fieldStats.dropoffRate}}% +
+
+
+
diff --git a/public/modules/forms/views/submit-form.client.view.html b/public/modules/forms/views/submit-form.client.view.html index 07ca08fd..edd50e58 100644 --- a/public/modules/forms/views/submit-form.client.view.html +++ b/public/modules/forms/views/submit-form.client.view.html @@ -1,4 +1,17 @@ + +
+ + + diff --git a/public/populate_template_cache.js b/public/populate_template_cache.js index 49b95f19..c10344b3 100644 --- a/public/populate_template_cache.js +++ b/public/populate_template_cache.js @@ -68,7 +68,7 @@ angular.module('NodeForm.templates', []).run(['$templateCache', function($templa "

{{field.title}} *(required)

press ENTER
"); $templateCache.put("../public/modules/forms/views/directiveViews/field/textfield.html", - "

{{field.title}} *(required)

press ENTER
"); + "

{{field.title}} *(required)

press ENTER
"); $templateCache.put("../public/modules/forms/views/directiveViews/field/yes_no.html", "

{{field.title}} *(required)

{{field.description}}


"); $templateCache.put("../public/modules/forms/views/directiveViews/form/configure-form.client.view.html", diff --git a/scripts/setup.js b/scripts/setup.js index 9589696b..b70ebe32 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -172,50 +172,51 @@ var questions = [ } ]; -console.log(chalk.green('\n\nHi, welcome to TellForm Setup')); +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 setup TellForm\n--------------------------------------------------\n\n')); + console.log(chalk.green('You should only run this the first time you setup TellForm\n--------------------------------------------------\n\n')); -inquirer.prompt([questions[0]]).then(function (confirmAns) { - if(confirmAns['shouldContinue']) { + inquirer.prompt([questions[0]]).then(function (confirmAns) { + if (confirmAns['shouldContinue']) { - inquirer.prompt(questions.slice(1)).then(function (answers) { - answers['NODE_ENV'] = 'production'; - answers['SIGNUP_DISABLED'] = false ? answers['SIGNUP_DISABLED'] === false : true; + inquirer.prompt(questions.slice(1)).then(function (answers) { + answers['NODE_ENV'] = 'production'; + answers['SIGNUP_DISABLED'] = false ? answers['SIGNUP_DISABLED'] === false : true; + var email = answers['email']; + var pass = answers['password']; + delete answers['email']; + delete answers['password']; - var email = answers['email']; - var pass = answers['password']; - delete answers['email']; - delete answers['password']; + envfile.stringify(answers, function (err, str) { + fs.outputFile('./\.env', str, function (err) { + if (err) return console.error(chalk.red(err)); + console.log(chalk.green('Successfully created .env file')); + }); + user = new User({ + firstName: 'Admin', + lastName: 'Account', + email: email, + username: email, + password: pass, + provider: 'local', + roles: ['admin', 'user'] + }); - envfile.stringify(answers, function (err, str) { - fs.outputFile('..//.env', str, function(err){ - if (err) return console.error(chalk.red(err)); - console.log(chalk.green('Successfully created .env file')); - }); - user = new User({ - firstName: 'Admin', - lastName: 'Account', - email: email, - username: email, - password: pass, - provider: 'local', - roles: ['admin', 'user'] - }); + user.save(function (err) { + if (err) return console.error(chalk.red(err)); + console.log(chalk.green('Successfully created user')); + delete email; + delete pass; - user.save(function (err) { - if (err) return console.error(chalk.red(err)); - console.log(chalk.green('Successfully created user')); - delete email; - delete pass; - - console.log(chalk.green('Have fun using TellForm!')); - process.exit(1); + console.log(chalk.green('Have fun using TellForm!')); + process.exit(1); + }); }); }); - }); - } else { - console.log(chalk.green('Have fun using TellForm!')); - } -}); + } else { + console.log(chalk.green('Have fun using TellForm!')); + } + }); +} diff --git a/server.js b/server.js index c9214844..13074a4e 100755 --- a/server.js +++ b/server.js @@ -53,7 +53,7 @@ if (process.env.NODE_ENV === 'secure') { console.log('--'); process.on('uncaughtException', function (err) { - console.error((new Date).toUTCString() + ' uncaughtException:', err.message); + console.error((new Date()).toUTCString() + ' uncaughtException:', err.message); console.error(err.stack); process.exit(1); });