2015-06-29 22:51:29 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Module dependencies.
|
|
|
|
*/
|
|
|
|
var mongoose = require('mongoose'),
|
|
|
|
Schema = mongoose.Schema,
|
|
|
|
_ = require('lodash'),
|
|
|
|
config = require('../../config/config'),
|
|
|
|
path = require('path'),
|
2015-10-06 20:14:38 +00:00
|
|
|
mUtilities = require('mongoose-utilities'),
|
2015-06-30 14:21:53 +00:00
|
|
|
fs = require('fs-extra'),
|
2015-07-27 18:11:43 +00:00
|
|
|
async = require('async'),
|
2016-04-29 02:48:02 +00:00
|
|
|
mkdirp = require('mkdirp'),
|
|
|
|
Random = require('random-js'),
|
|
|
|
mt = Random.engines.mt19937(),
|
2015-08-04 21:06:16 +00:00
|
|
|
util = require('util');
|
|
|
|
|
2016-04-29 02:48:02 +00:00
|
|
|
mt.autoSeed();
|
|
|
|
|
2015-08-04 21:06:16 +00:00
|
|
|
//Mongoose Models
|
2015-09-18 16:32:17 +00:00
|
|
|
var FieldSchema = require('./form_field.server.model.js');
|
|
|
|
var Field = mongoose.model('Field');
|
|
|
|
|
2015-09-10 22:06:28 +00:00
|
|
|
var FormSubmissionSchema = require('./form_submission.server.model.js'),
|
|
|
|
FormSubmission = mongoose.model('FormSubmission', FormSubmissionSchema);
|
2015-06-29 22:51:29 +00:00
|
|
|
|
2015-09-18 16:32:17 +00:00
|
|
|
|
2015-08-18 21:44:36 +00:00
|
|
|
var ButtonSchema = new Schema({
|
|
|
|
url: {
|
|
|
|
type: String,
|
|
|
|
match: [/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/],
|
|
|
|
},
|
|
|
|
action: String,
|
|
|
|
text: String,
|
|
|
|
bgColor: {
|
|
|
|
type: String,
|
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
|
|
|
default: '#5bc0de'
|
|
|
|
},
|
|
|
|
color: {
|
|
|
|
type: String,
|
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
|
|
|
default: '#ffffff'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-06-07 00:37:09 +00:00
|
|
|
var VisitorDataSchema = new Schema({
|
|
|
|
referrer: {
|
2016-06-17 21:51:17 +00:00
|
|
|
type: String
|
2016-06-07 00:37:09 +00:00
|
|
|
},
|
|
|
|
lastActiveField: {
|
|
|
|
type: Schema.Types.ObjectId
|
|
|
|
},
|
|
|
|
timeElapsed: {
|
2016-06-20 22:06:41 +00:00
|
|
|
type: Number
|
2016-06-07 00:37:09 +00:00
|
|
|
},
|
|
|
|
isSubmitted: {
|
2016-06-20 22:06:41 +00:00
|
|
|
type: Boolean
|
2016-06-17 21:33:33 +00:00
|
|
|
},
|
|
|
|
language: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
ipAddr: {
|
|
|
|
type: String,
|
|
|
|
default: ''
|
|
|
|
},
|
|
|
|
deviceType: {
|
|
|
|
type: String,
|
|
|
|
enum: ['desktop', 'phone', 'tablet', 'other'],
|
2016-06-17 21:51:17 +00:00
|
|
|
default: 'other'
|
2016-06-17 21:33:33 +00:00
|
|
|
},
|
|
|
|
userAgent: {
|
|
|
|
type: String
|
2016-06-07 00:37:09 +00:00
|
|
|
}
|
2016-06-17 21:33:33 +00:00
|
|
|
|
2016-06-07 00:37:09 +00:00
|
|
|
});
|
|
|
|
|
2016-11-09 18:02:12 +00:00
|
|
|
var formSchemaOptions = {
|
|
|
|
toJSON: {
|
|
|
|
virtuals: true
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-06-29 22:51:29 +00:00
|
|
|
/**
|
|
|
|
* Form Schema
|
|
|
|
*/
|
2015-09-10 22:06:28 +00:00
|
|
|
var FormSchema = new Schema({
|
2015-06-29 22:51:29 +00:00
|
|
|
title: {
|
|
|
|
type: String,
|
|
|
|
trim: true,
|
2016-04-29 02:48:02 +00:00
|
|
|
required: 'Form Title cannot be blank'
|
2015-06-29 22:51:29 +00:00
|
|
|
},
|
2015-07-07 01:21:43 +00:00
|
|
|
language: {
|
|
|
|
type: String,
|
2016-06-16 00:38:22 +00:00
|
|
|
enum: ['en', 'fr', 'es', 'it', 'de'],
|
|
|
|
default: 'en',
|
|
|
|
required: 'Form must have a language'
|
2015-07-07 01:21:43 +00:00
|
|
|
},
|
2016-06-07 00:37:09 +00:00
|
|
|
analytics:{
|
|
|
|
gaCode: {
|
|
|
|
type: String
|
|
|
|
},
|
|
|
|
visitors: [VisitorDataSchema]
|
|
|
|
},
|
|
|
|
|
2016-06-01 01:06:45 +00:00
|
|
|
form_fields: [FieldSchema],
|
2015-06-30 02:14:43 +00:00
|
|
|
submissions: [{
|
2015-06-29 22:51:29 +00:00
|
|
|
type: Schema.Types.ObjectId,
|
|
|
|
ref: 'FormSubmission'
|
|
|
|
}],
|
|
|
|
|
|
|
|
admin: {
|
|
|
|
type: Schema.Types.ObjectId,
|
2015-07-21 23:25:45 +00:00
|
|
|
ref: 'User',
|
|
|
|
required: 'Form must have an Admin'
|
2015-06-29 22:51:29 +00:00
|
|
|
},
|
2015-08-07 21:02:44 +00:00
|
|
|
startPage: {
|
|
|
|
showStart:{
|
|
|
|
type: Boolean,
|
2016-04-29 02:48:02 +00:00
|
|
|
default: false
|
2015-08-07 21:02:44 +00:00
|
|
|
},
|
2015-08-18 21:44:36 +00:00
|
|
|
introTitle:{
|
2015-08-07 21:02:44 +00:00
|
|
|
type: String,
|
2015-08-18 21:44:36 +00:00
|
|
|
default: 'Welcome to Form'
|
2015-08-07 21:02:44 +00:00
|
|
|
},
|
2015-08-18 21:44:36 +00:00
|
|
|
introParagraph:{
|
2016-03-30 03:45:16 +00:00
|
|
|
type: String
|
2015-08-18 21:44:36 +00:00
|
|
|
},
|
2016-04-21 21:44:15 +00:00
|
|
|
introButtonText:{
|
|
|
|
type: String,
|
|
|
|
default: 'Start'
|
|
|
|
},
|
2015-08-18 21:44:36 +00:00
|
|
|
buttons:[ButtonSchema]
|
2015-08-06 05:52:59 +00:00
|
|
|
},
|
2015-08-18 21:44:36 +00:00
|
|
|
|
2015-06-30 11:05:44 +00:00
|
|
|
hideFooter: {
|
|
|
|
type: Boolean,
|
2016-03-30 03:45:16 +00:00
|
|
|
default: false
|
2015-06-30 11:05:44 +00:00
|
|
|
},
|
2015-07-02 02:49:35 +00:00
|
|
|
isLive: {
|
|
|
|
type: Boolean,
|
2016-03-30 03:45:16 +00:00
|
|
|
default: false
|
2015-07-02 02:49:35 +00:00
|
|
|
},
|
2015-08-07 21:02:44 +00:00
|
|
|
|
2015-07-30 00:22:53 +00:00
|
|
|
design: {
|
|
|
|
colors:{
|
2016-03-30 03:45:16 +00:00
|
|
|
backgroundColor: {
|
2015-08-07 22:30:41 +00:00
|
|
|
type: String,
|
2015-10-30 21:40:22 +00:00
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
|
|
|
default: '#fff'
|
2015-08-07 22:30:41 +00:00
|
|
|
},
|
2016-03-30 03:45:16 +00:00
|
|
|
questionColor: {
|
2015-08-07 22:30:41 +00:00
|
|
|
type: String,
|
2015-10-30 21:40:22 +00:00
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
2016-04-21 23:48:59 +00:00
|
|
|
default: '#333'
|
2015-08-07 22:30:41 +00:00
|
|
|
},
|
2016-03-30 03:45:16 +00:00
|
|
|
answerColor: {
|
2015-08-07 22:30:41 +00:00
|
|
|
type: String,
|
2015-10-30 21:40:22 +00:00
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
2016-04-21 23:48:59 +00:00
|
|
|
default: '#333'
|
2015-08-07 22:30:41 +00:00
|
|
|
},
|
2016-03-30 03:45:16 +00:00
|
|
|
buttonColor: {
|
2015-08-07 22:30:41 +00:00
|
|
|
type: String,
|
2016-04-21 23:48:59 +00:00
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
|
|
|
default: '#fff'
|
2016-04-21 22:25:22 +00:00
|
|
|
},
|
|
|
|
buttonTextColor: {
|
|
|
|
type: String,
|
|
|
|
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
|
|
|
default: '#333'
|
2016-04-29 02:48:02 +00:00
|
|
|
}
|
2015-07-30 00:22:53 +00:00
|
|
|
},
|
2016-08-26 19:31:40 +00:00
|
|
|
font: String
|
2017-03-06 20:30:31 +00:00
|
|
|
}
|
2016-11-09 18:02:12 +00:00
|
|
|
}, formSchemaOptions);
|
2015-06-29 22:51:29 +00:00
|
|
|
|
2016-06-07 00:37:09 +00:00
|
|
|
/*
|
|
|
|
** 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;
|
|
|
|
|
2017-03-01 20:11:20 +00:00
|
|
|
if(this.form_fields.length == 0) return null;
|
2016-06-07 00:37:09 +00:00
|
|
|
for(var i=0; i<this.form_fields.length; i++){
|
|
|
|
var field = this.form_fields[i];
|
|
|
|
|
2017-03-01 20:11:20 +00:00
|
|
|
if(field && !field.deletePreserved){
|
2016-06-07 00:37:09 +00:00
|
|
|
|
|
|
|
var dropoffViews = _.reduce(visitors, function(sum, visitorObj){
|
|
|
|
|
|
|
|
if(visitorObj.lastActiveField+'' === field._id+'' && !visitorObj.isSubmitted){
|
|
|
|
return sum + 1;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
var continueViews, nextIndex;
|
|
|
|
|
|
|
|
if(i !== this.form_fields.length-1){
|
|
|
|
continueViews = _.reduce(visitors, function(sum, visitorObj){
|
|
|
|
nextIndex = that.form_fields.indexOf(_.find(that.form_fields, function(o) {
|
|
|
|
return o._id+'' === visitorObj.lastActiveField+'';
|
|
|
|
}));
|
|
|
|
|
|
|
|
if(nextIndex > i){
|
|
|
|
return sum + 1;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}, 0);
|
2016-11-14 19:14:25 +00:00
|
|
|
} else {
|
2016-06-07 00:37:09 +00:00
|
|
|
continueViews = _.reduce(visitors, function(sum, visitorObj){
|
|
|
|
if(visitorObj.lastActiveField+'' === field._id+'' && visitorObj.isSubmitted){
|
|
|
|
return sum + 1;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
var totalViews = dropoffViews+continueViews;
|
2016-06-17 22:11:02 +00:00
|
|
|
var responses = continueViews;
|
2016-06-07 00:37:09 +00:00
|
|
|
var continueRate = continueViews/totalViews*100;
|
|
|
|
var dropoffRate = dropoffViews/totalViews*100;
|
|
|
|
|
|
|
|
fieldDropoffs[i] = {
|
|
|
|
dropoffViews: dropoffViews,
|
2016-06-17 22:11:02 +00:00
|
|
|
responses: continueViews,
|
2016-06-07 00:37:09 +00:00
|
|
|
totalViews: totalViews,
|
|
|
|
continueRate: continueRate,
|
|
|
|
dropoffRate: dropoffRate,
|
|
|
|
field: field
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fieldDropoffs;
|
|
|
|
});
|
|
|
|
|
2015-10-06 20:14:38 +00:00
|
|
|
FormSchema.plugin(mUtilities.timestamp, {
|
|
|
|
createdPath: 'created',
|
|
|
|
modifiedPath: 'lastModified',
|
|
|
|
useVirtual: false
|
|
|
|
});
|
2015-12-12 20:08:48 +00:00
|
|
|
|
2015-07-28 22:29:07 +00:00
|
|
|
var _original;
|
2015-07-03 00:49:23 +00:00
|
|
|
|
2015-07-21 23:25:45 +00:00
|
|
|
function getDeletedIndexes(needle, haystack){
|
|
|
|
var deletedIndexes = [];
|
|
|
|
|
|
|
|
if(haystack.length > 0){
|
|
|
|
for(var i = 0; i < needle.length; i++){
|
2015-07-27 18:11:43 +00:00
|
|
|
if(haystack.indexOf(needle[i]) === -1){
|
2015-07-21 23:25:45 +00:00
|
|
|
deletedIndexes.push(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return deletedIndexes;
|
|
|
|
}
|
|
|
|
|
2017-03-06 19:43:42 +00:00
|
|
|
|
|
|
|
FormSchema.pre('save', function (next) {
|
|
|
|
var that = this;
|
|
|
|
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:
|
|
|
|
this.language = 'en';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
2015-09-10 22:06:28 +00:00
|
|
|
FormSchema.pre('save', function (next) {
|
2016-04-29 02:48:02 +00:00
|
|
|
var that = this;
|
|
|
|
|
2016-04-29 06:22:47 +00:00
|
|
|
async.series([function(cb) {
|
2016-04-29 02:48:02 +00:00
|
|
|
that.constructor
|
|
|
|
.findOne({_id: that._id}).exec(function (err, original) {
|
|
|
|
if (err) {
|
|
|
|
console.log(err);
|
2016-04-29 06:00:41 +00:00
|
|
|
return cb(err);
|
2016-04-29 02:48:02 +00:00
|
|
|
} else {
|
|
|
|
_original = original;
|
2016-04-29 06:00:41 +00:00
|
|
|
return cb(null);
|
2015-07-03 00:49:23 +00:00
|
|
|
}
|
2015-07-02 02:49:35 +00:00
|
|
|
});
|
2016-04-29 02:48:02 +00:00
|
|
|
},
|
2016-11-14 19:14:25 +00:00
|
|
|
function(cb) {
|
|
|
|
var hasIds = true;
|
|
|
|
for(var i=0; i<that.form_fields.length; i++){
|
|
|
|
if(!that.form_fields.hasOwnProperty('_id')){
|
|
|
|
hasIds = false;
|
|
|
|
break;
|
2016-11-02 18:30:04 +00:00
|
|
|
}
|
2016-11-14 19:14:25 +00:00
|
|
|
}
|
|
|
|
if(that.isModified('form_fields') && that.form_fields && _original && hasIds){
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
var old_form_fields = _original.form_fields,
|
|
|
|
new_ids = _.map(_.pluck(that.form_fields, 'id'), function(id){ return ''+id;}),
|
|
|
|
old_ids = _.map(_.pluck(old_form_fields, 'id'), function(id){ return ''+id;}),
|
|
|
|
deletedIds = getDeletedIndexes(old_ids, new_ids);
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
//Preserve fields that have at least one submission
|
|
|
|
if( deletedIds.length > 0 ){
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
var modifiedSubmissions = [];
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
async.forEachOfSeries(deletedIds,
|
|
|
|
function (deletedIdIndex, key, cb_id) {
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
var deleted_id = old_ids[deletedIdIndex];
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
//Find FormSubmissions that contain field with _id equal to 'deleted_id'
|
|
|
|
FormSubmission.
|
|
|
|
find({ form: that._id, admin: that.admin, form_fields: {$elemMatch: {submissionId: deleted_id} } }).
|
|
|
|
exec(function(err, submissions){
|
|
|
|
if(err) {
|
|
|
|
console.error(err);
|
|
|
|
return cb_id(err);
|
|
|
|
} else {
|
|
|
|
//Delete field if there are no submission(s) found
|
|
|
|
if (submissions.length) {
|
|
|
|
//Add submissions
|
|
|
|
modifiedSubmissions.push.apply(modifiedSubmissions, submissions);
|
|
|
|
}
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
return cb_id(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function (err) {
|
|
|
|
if(err){
|
|
|
|
console.error(err.message);
|
|
|
|
return cb(err);
|
|
|
|
} else {
|
|
|
|
|
|
|
|
//Iterate through all submissions with modified form_fields
|
|
|
|
async.forEachOfSeries(modifiedSubmissions, function (submission, key, callback) {
|
|
|
|
|
|
|
|
//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._id + '';
|
|
|
|
return tmp_id === old_ids[deletedIds[i]];
|
|
|
|
});
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
var deletedField = submission.form_fields[index];
|
2016-04-29 02:48:02 +00:00
|
|
|
|
2016-11-14 19:14:25 +00:00
|
|
|
//Hide field if it exists
|
|
|
|
if (deletedField) {
|
|
|
|
// console.log('deletedField\n-------\n\n');
|
|
|
|
// console.log(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);
|
|
|
|
that.form_fields.unshift(deletedField);
|
|
|
|
// console.log('form.form_fields\n--------\n\n');
|
|
|
|
// console.log(that.form_fields);
|
2016-04-29 02:48:02 +00:00
|
|
|
}
|
2016-11-14 19:14:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
submission.save(function (err) {
|
|
|
|
if (err) return callback(err);
|
|
|
|
else return callback(null);
|
2016-04-29 02:48:02 +00:00
|
|
|
});
|
2016-11-14 19:14:25 +00:00
|
|
|
}, function (err) {
|
|
|
|
if (err) {
|
|
|
|
console.error(err.message);
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
else return cb();
|
|
|
|
});
|
2016-04-29 02:48:02 +00:00
|
|
|
}
|
2016-11-14 19:14:25 +00:00
|
|
|
}
|
|
|
|
);
|
2016-04-29 02:48:02 +00:00
|
|
|
}
|
2016-04-29 06:00:41 +00:00
|
|
|
else return cb(null);
|
2016-11-14 19:14:25 +00:00
|
|
|
}
|
|
|
|
else return cb(null);
|
|
|
|
}],
|
|
|
|
function(err, results){
|
|
|
|
if (err) return next(err);
|
|
|
|
return next();
|
|
|
|
});
|
2015-07-27 18:11:43 +00:00
|
|
|
});
|
2015-06-29 22:51:29 +00:00
|
|
|
|
2015-09-10 22:06:28 +00:00
|
|
|
mongoose.model('Form', FormSchema);
|
2015-06-29 22:51:29 +00:00
|
|
|
|