This commit is contained in:
David Baldwynn 2016-06-07 14:22:20 -07:00
commit a8530472d5
48 changed files with 1467 additions and 412 deletions

View file

@ -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();
}
});

View file

@ -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<this.form_fields.length; i++){
var field = this.form_fields[i];
if(!field.deletePreserved){
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);
}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);

View file

@ -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;

View file

@ -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<form_fields.length; i++){
form_fields[i].isSubmission = true;
form_fields[i].submissionId = form_fields[i]._id;
form_fields[i]._id = new mongoose.mongo.ObjectID();
}
//console.log(form_fields)
return form_fields;
}
/**
* Form Submission Schema
*/
@ -55,9 +69,7 @@ var FormSubmissionSchema = new Schema({
required: true
},
form_fields: {
type: [Schema.Types.Mixed]
},
form_fields: [FieldSchema],
form: {
type: Schema.Types.ObjectId,
@ -99,7 +111,7 @@ var FormSubmissionSchema = new Schema({
},
timeElapsed: {
type: Number,
type: Number
},
percentageComplete: {
type: Number
@ -116,9 +128,10 @@ var FormSubmissionSchema = new Schema({
default: false
}
}
});
FormSubmissionSchema.path('form_fields').set(formFieldsSetter);
FormSubmissionSchema.plugin(mUtilities.timestamp, {
createdPath: 'created',
modifiedPath: 'lastModified',
@ -214,7 +227,7 @@ FormSubmissionSchema.pre('save', function (next) {
});
});
}
},
}
], function(err, result) {
if(err) return next(err);
@ -254,7 +267,6 @@ FormSubmissionSchema.pre('save', function (next) {
self = this,
_form = this.form;
if(this.pdf && this.pdf.path){
dest_filename = self.title.replace(/ /g,'')+'_submission_'+Date.now()+'.pdf';
var __path = this.pdf.path.split('/').slice(0,this.pdf.path.split('/').length-1).join('/');

View file

@ -0,0 +1,80 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
config = require('../../config/config'),
errorHandler = require('../controllers/errors.server.controller'),
Form = mongoose.model('Form');
// Create the chat configuration
module.exports = function (io, socket) {
var visitorsData = {};
var saveVisitorData = function (data, cb){
Form.findById(data.formId, function(err, form) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
var newVisitor = {
referrer: data.referrer,
lastActiveField: data.lastActiveField,
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted
};
form.analytics.visitors.push(newVisitor);
form.save(function (err) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
console.log('\n\nVisitor data successfully added!');
delete visitorsData[socket.id];
if(cb) cb();
});
});
socket.disconnect(0);
};
io.on('connection', function(socket) {
// a user has visited our page - add them to the visitorsData object
socket.on('form-visitor-data', function(data) {
console.log('\n\nuser has visited our page');
visitorsData[socket.id] = data;
console.log(data);
if (data.isSubmitted) {
saveVisitorData(data, function () {
console.log('\n\n user submitted form');
socket.disconnect(0);
});
}
});
socket.on('disconnect', function() {
var data = visitorsData[socket.id];
if(data){
if(!data.isSubmitted) {
saveVisitorData(data);
}
}
});
});
};

View file

@ -80,8 +80,12 @@
<!--Embedding The signupDisabled Boolean-->
<script type="text/javascript">
var signupDisabled = {{signupDisabled | safe}};
var socketPort = {{socketPort | safe}};
</script>
<!--Socket.io Client Dependency-->
<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
<!--Bower JS dependencies-->
{% for bowerJSFile in bowerJSFiles %}
<script type="text/javascript" src="{{bowerJSFile}}"></script>
@ -103,7 +107,7 @@
<script src="https://cdn.ravenjs.com/2.3.0/angular/raven.min.js"></script>
<script>
Raven.config('http://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
</script>
<!-- [if lt IE 9]>

View file

@ -27,7 +27,6 @@
"angular-bootstrap-colorpicker": "~3.0.19",
"angular-ui-router-tabs": "~1.7.0",
"angular-scroll": "^1.0.0",
"ui-select": "angular-ui-select#^0.16.1",
"angular-sanitize": "^1.5.3",
"v-button": "^1.1.1",
"angular-busy": "^4.1.3",
@ -35,10 +34,13 @@
"raven-js": "^3.0.4",
"tableExport.jquery.plugin": "^1.5.1",
"angular-translate": "~2.11.0"
"js-yaml": "^3.6.1",
"angular-ui-select": "whitef0x0/ui-select#compiled"
},
"resolutions": {
"angular-bootstrap": "^0.14.0",
"angular": "1.4.x"
"angular": "~1.4.7",
"angular-ui-select": "compiled"
},
"overrides": {
"BOWER-PACKAGE": {

View file

@ -12,6 +12,7 @@ var _ = require('lodash'),
var exists = require('path-exists').sync;
var minBowerFiles = function(type){
console.log(type);
return bowerFiles(type).map( function(path, index, arr) {
var newPath = path.replace(/.([^.]+)$/g, '.min.$1');
return exists( newPath ) ? newPath : path;

36
config/env/all.js vendored
View file

@ -7,7 +7,9 @@ module.exports = {
description: process.env.APP_DESC || 'Opensource form builder alternative to TypeForm',
keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs'
},
port: process.env.PORT || 3000,
port: process.env.PORT || 5000,
socketPort: process.env.SOCKET_PORT || 35729,
templateEngine: 'swig',
reCAPTCHA_Key: process.env.reCAPTCHA_KEY || '',
@ -70,9 +72,9 @@ module.exports = {
assets: {
css: [
'public/modules/**/css/*.css',
'!public/modules/**/demo/**/*.css',
'!public/modules/**/dist/**/*.css',
'!public/modules/**/node_modules/**/*.css'
'public/modules/**/demo/**/*.css',
'public/modules/**/dist/**/*.css',
'public/modules/**/node_modules/**/*.css'
],
js: [
'public/dist/populate_template_cache.js',
@ -82,29 +84,29 @@ module.exports = {
'public/modules/*/*.js',
'public/modules/*/*/*.js',
'public/modules/**/*.js',
'!public/modules/**/gruntfile.js',
'!public/modules/**/demo/**/*.js',
'!public/modules/**/dist/**/*.js',
'!public/modules/**/node_modules/**/*.js',
'!public/modules/**/tests/**/*.js'
'public/modules/**/gruntfile.js',
'public/modules/**/demo/**/*.js',
'public/modules/**/dist/**/*.js',
'public/modules/**/node_modules/**/*.js',
'public/modules/**/tests/**/*.js'
],
views: [
'public/modules/**/*.html',
'!public/modules/**/demo/**/*.html',
'!public/modules/**/dist/**/*.html',
'!public/modules/**/node_modules/**/*.html',
'!public/modules/**/tests/**/*.html'
'public/modules/**/demo/**/*.html',
'public/modules/**/dist/**/*.html',
'public/modules/**/node_modules/**/*.html',
'public/modules/**/tests/**/*.html'
],
unit_tests: [
'public/lib/angular-mocks/angular-mocks.js',
'public/modules/*/tests/unit/**/*.js',
'!public/modules/**/demo/**/*.js',
'!public/modules/**/node_modules/**/*.js'
'public/modules/**/demo/**/*.js',
'public/modules/**/node_modules/**/*.js'
],
e2e_tests: [
'public/modules/*/tests/e2e/**.js',
'!public/modules/**/demo/**/*.js',
'!public/modules/**/node_modules/**/*.js'
'public/modules/**/demo/**/*.js',
'public/modules/**/node_modules/**/*.js'
]
}
};

View file

@ -1,7 +1,7 @@
'use strict';
module.exports = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
baseUrl: process.env.BASE_URL || 'http://localhost:5000',
db: {
uri: 'mongodb://'+(process.env.DB_HOST || 'localhost')+'/mean',
options: {

17
config/env/local.js vendored
View file

@ -1,17 +0,0 @@
'use strict';
var mandrill_api_key = 'AVCCf1C2dFlrNhx9Iyi_yQ';
//Use mandrill mock API_key if we are testing
// if(process.env.NODE_ENV === 'test') mandrill_api_key = '_YNOKLgT9DGb2sgVGR66yQ';
module.exports = {
// db: {
// uri: 'mongodb://localhost/local-dev',
// options: {
// user: '',
// pass: ''
// }
// },
sessionSecret: process.env.SESSION_SECRET || 'somethingheresecret',
};

View file

@ -1,65 +0,0 @@
'use strict';
module.exports = {
baseUrl: process.env.BASE_URL || 'dev.tellform.com',
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean',
options: {
user: 'admin',
pass: process.env.MONGOLAB_PASS || 'admin'
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
sessionCookie: {
domain: process.env.BASE_URL || 'dev.tellform.com'
},
assets: {
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'no-reply@dev.tellform.com',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || '',
secure: false,
requireTLS: true,
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
}
}
};

View file

@ -26,7 +26,16 @@ var fs = require('fs-extra'),
device = require('express-device'),
client = new raven.Client(config.DSN);
/**
* Configure Socket.io
*/
var configureSocketIO = function (app, db) {
// Load the Socket.io configuration
var server = require('./socket.io')(app, db);
// Return server object
return server;
};
module.exports = function(db) {
// Initialize express app
@ -43,6 +52,7 @@ module.exports = function(db) {
app.locals.signupDisabled = config.signupDisabled;
app.locals.description = config.app.description;
app.locals.keywords = config.app.keywords;
app.locals.socketPort = config.socketPort;
app.locals.bowerJSFiles = config.getBowerJSAssets();
app.locals.bowerCssFiles = config.getBowerCSSAssets();
@ -147,11 +157,11 @@ module.exports = function(db) {
// Add headers for Sentry
/*
app.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', 'http://sentry.polydaic.com');
res.setHeader('Access-Control-Allow-Origin', 'https://sentry.polydaic.com');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
@ -166,7 +176,7 @@ module.exports = function(db) {
// Pass to next layer of middleware
next();
});
*/
// Sentry (Raven) middleware
// app.use(raven.middleware.express.requestHandler(config.DSN));
@ -212,6 +222,8 @@ module.exports = function(db) {
return httpsServer;
}
app = configureSocketIO(app, db);
// Return Express server instance
return app;
};

24
config/socket.io.js Normal file
View file

@ -0,0 +1,24 @@
'use strict';
// Load the module dependencies
var config = require('./config'),
path = require('path'),
http = require('http'),
socketio = require('socket.io');
// Define the Socket.io configuration method
module.exports = function (app, db) {
var server = http.createServer(app);
// Create a new Socket.io server
var io = socketio.listen(server);
// Add an event listener to the 'connection' event
io.on('connection', function (socket) {
config.getGlobbedFiles('./app/sockets/**.js').forEach(function (socketConfiguration) {
require(path.resolve(socketConfiguration))(io, socket);
});
});
return server;
};

View file

@ -0,0 +1,12 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
AnonymousStrategy = require('passport-anonymous').Strategy;
module.exports = function() {
// Use local strategy
passport.use(new AnonymousStrategy());
};

View file

@ -19,7 +19,7 @@
"scripts": {
"start": "grunt",
"test": "grunt test && grunt coveralls",
"postinstall": "bower install --config.interactive=false; grunt build"
"postinstall": "bower install --config.interactive=false; grunt build; node scripts/setup.js;"
},
"dependencies": {
"async": "^1.4.2",
@ -63,13 +63,14 @@
"math": "0.0.3",
"method-override": "~2.3.0",
"mkdirp": "^0.5.1",
"mongoose": "3.8.40",
"mongoose": "~4.4.19",
"mongoose-utilities": "~0.1.1",
"morgan": "~1.6.1",
"multer": "~1.1.0",
"node-freegeoip": "0.0.1",
"nodemailer": "~1.10.0",
"passport": "~0.3.0",
"passport-anonymous": "^1.0.1",
"passport-facebook": "~2.0.0",
"passport-github": "~1.0.0",
"passport-google-oauth": "~0.2.0",
@ -82,6 +83,7 @@
"random-js": "^1.0.8",
"raven": "^0.9.0",
"soap": "^0.11.0",
"socket.io": "^1.4.6",
"swig": "~1.4.1"
},
"devDependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
(function () {
'use strict';
// Create the Socket.io wrapper service
angular
.module('core')
.factory('Socket', Socket);
Socket.$inject = ['$timeout', '$window'];
function Socket($timeout, $window) {
var service = {
connect: connect,
emit: emit,
on: on,
removeListener: removeListener,
socket: null
};
console.log('https://'+window.location.hostname+':'+$window.socketPort);
connect('https://'+window.location.hostname+':'+$window.socketPort);
return service;
// Connect to Socket.io server
function connect(url) {
service.socket = io();
}
// Wrap the Socket.io 'emit' method
function emit(eventName, data) {
if (service.socket) {
service.socket.emit(eventName, data);
}
}
// Wrap the Socket.io 'on' method
function on(eventName, callback) {
if (service.socket) {
service.socket.on(eventName, function (data) {
$timeout(function () {
callback(data);
});
});
}
}
// Wrap the Socket.io 'removeListener' method
function removeListener(eventName) {
if (service.socket) {
service.socket.removeListener(eventName);
}
}
}
}());

View file

@ -79,7 +79,7 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun
}
});
//Autosave Form when model (specificed in $attrs.autoSaveWatch) changes
//Autosave Form when model (specified in $attrs.autoSaveWatch) changes
$scope.$watch($attrs.autoSaveWatch, function(newValue, oldValue) {
newValue = angular.copy(newValue);

View file

@ -24,8 +24,25 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
forcePlaceholderSize: true
};
console.log($scope.sortableOptions);
/*
** Setup Angular-Input-Star Shape Dropdown
*/
//Populate Name to Font-awesomeName Conversion Map
$scope.select2FA = {
'Heart': 'Heart',
'Star': 'Star',
'thumbs-up': 'Thumbs Up',
'thumbs-down':'Thumbs Down',
'Circle': 'Circle',
'Square':'Square',
'Check Circle': 'Checkmark',
'Smile Outlined': 'Smile',
'Hourglass': 'Hourglass',
'bell': 'Bell',
'Paper Plane': 'Paper Plane',
'Comment': 'Chat Bubble',
'Trash': 'Trash Can'
};
//Populate AddField with all available form field types
$scope.addField = {};
@ -67,11 +84,11 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
/*
** FormFields (ui-sortable) drag-and-drop configuration
*/
$scope.dropzone = {
handle: ' .handle',
containment: '.dropzoneContainer',
cursor: 'grabbing'
};
$scope.dropzone = {
handle: '.handle',
containment: '.dropzoneContainer',
cursor: 'grabbing'
};
/*
** Field CRUD Methods
@ -79,7 +96,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
// Add a new field
$scope.addNewField = function(modifyForm, fieldType){
// incr field_id counter
// increment lastAddedID counter
$scope.addField.lastAddedID++;
var fieldTitle;
@ -98,12 +115,19 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
disabled: false,
deletePreserved: false
};
// console.log('\n\n---------\nAdded field CLIENT');
// console.log(newField);
// newField._id = _.uniqueId();
// put newField into fields array
if($scope.showAddOptions(newField)){
newField.fieldOptions = [];
newField.fieldOptions.push({
'option_id' : Math.floor(100000*Math.random()), //Generate pseudo-random option id
'option_title' : 'Option 0',
'option_value' : 'Option 0'
});
}
if(modifyForm){
//Add newField to form_fields array
$scope.myform.form_fields.push(newField);
}
return newField;
@ -168,20 +192,17 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
// add new option to the field
$scope.addOption = function(field_index){
var currField = $scope.myform.form_fields[field_index];
console.log(field_index);
console.log(currField);
//console.log(field_index);
//console.log(currField);
if(currField.fieldType === 'checkbox' || currField.fieldType === 'dropdown' || currField.fieldType === 'radio'){
if(!currField.fieldOptions) $scope.myform.form_fields[field_index].fieldOptions = [];
if(!currField.fieldOptions){
$scope.myform.form_fields[field_index].fieldOptions = [];
}
var lastOptionID = 0;
if(currField.fieldOptions[currField.fieldOptions.length-1]){
lastOptionID = currField.fieldOptions[currField.fieldOptions.length-1].option_id;
}
var lastOptionID = $scope.myform.form_fields[field_index].fieldOptions.length+1;
// new option's id
var option_id = lastOptionID + 1;
var newOption = {
'option_id' : Math.floor(100000*Math.random()),
@ -219,7 +240,17 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
}
};
}
// decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showRatingOptions = function (field){
if(field.fieldType === 'rating'){
return true;
} else {
return false;
}
};
}
};
}

View file

@ -271,6 +271,22 @@
<!-- <span class="required-error" ng-show="field.required && !field.fieldValue">* required</span> -->
</div>
</div>
<div class="row field">
<div class="field-title col-sm-4">
<h5>Google Analytics Tracking Code</h5>
</div>
<div class="col-sm-8">
<input type="text"
ng-model="myform.analytics.gaCode"
value="{{myform.analytics.gaCode}}"
style="width: 100%;"
ng-minlength="4"
placeholder="UA-XXXXX-Y"
ng-pattern="/\bUA-\d{4,10}-\d{1,4}\b/">
</div>
</div>
<div class="row field">
<div class="col-xs-6 field-title">Language</div>

View file

@ -220,13 +220,13 @@
<div class="row"><br></div>
<div class="row description">
<div class="row description" ng-hide="showRatingOptions(field)">
<div class="col-md-4 col-sm-12">Description:</div>
<div class="col-md-8 col-sm-12"><textarea type="text" ng-model="field.description" name="description{{field._id}}"value="{{field.description}}"></textarea> </div>
</div>
<div class="row" ng-show="showAddOptions(field)"><br></div>
<div class="row options" ng-show="showAddOptions(field)">
<div class="row options" ng-if="showAddOptions(field)">
<div class="col-md-4 col-xs-12">Options:</div>
<div class="col-md-8 col-xs-12">
<div ng-repeat="option in field.fieldOptions track by option.option_id" class="row">
@ -244,6 +244,32 @@
</div>
</div>
<div class="row" ng-show="showRatingOptions(field)"><br></div>
<div class="row" ng-if="showRatingOptions(field)">
<div class="col-md-9 col-sm-9">Number of Steps:</div>
<div class="col-md-3 col-sm-3">
<input style="width:100%" type="number"
min="1" max="10"
ng-model="field.ratingOptions.steps"
name="ratingOptions_steps{{field._id}}"
ng-value="{{field.ratingOptions.steps}}"
required>
</div>
<br>
<div class="col-md-5 col-sm-9">Shape:</div>
<div class="col-md-7 col-sm-3">
<select style="width:100%" ng-model="field.ratingOptions.shape"
value="{{field.ratingOptions.steps}}"
name="ratingOptions_shape{{field._id}}" required>
<option ng-repeat="shapeType in field.ratingOptions.validShapes"
value="{{shapeType}}">
{{select2FA[shapeType]}}
</option>
</select>
</div>
</div>
<div class="row"><br></div>
<div class="row">

View file

@ -1,5 +1,47 @@
<div class="submissions-table row container" ng-init="initFormSubmissions()">
<div class="row">
<div class="col-xs-4">
Total Views: {{myform.analytics.views}}
</div>
<div class="col-xs-4">
Submissions: {{myform.analytics.submissions}}
</div>
<div class="col-xs-4">
Conversion Rate: {{myform.analytics.conversionRate}}%
</div>
</div>
<br>
<div class="row">
<div class="col-xs-12">
<div class="col-xs-2">
<strong>Field Title</strong>
</div>
<div class="col-xs-2">
<strong>Field Views</strong>
</div>
<div class="col-xs-4">
<strong>User dropoff rate at this field</strong>
</div>
</div>
<div class="col-xs-12" ng-repeat="fieldStats in myform.analytics.fields">
<div class="col-xs-2">
{{fieldStats.field.title}}
</div>
<div class="col-xs-2">
{{fieldStats.totalViews}}
</div>
<div class="col-xs-4">
{{fieldStats.dropoffRate}}%
</div>
</div>
</div>
<br>
<div class="row">
<div class="col-xs-2">
<button class="btn btn-danger" ng-click="deleteSelectedSubmissions()" ng-disabled="!isAtLeastOneChecked();">
@ -69,11 +111,8 @@
<th class="scope">
{{$index+1}}
</th>
<td ng-if="field.fieldType == 'dropdown'" data-ng-repeat="field in row.form_fields">
{{field.fieldValue.field_title}}
</td>
<td ng-if="field.fieldType != 'dropdown'" data-ng-repeat="field in row.form_fields">
<td data-ng-repeat="field in row.form_fields">
{{field.fieldValue}}
</td>
<td ng-if="myform.plugins.oscarhost.baseUrl">

View file

@ -237,12 +237,6 @@ form .row.field {
form .row.field.dropdown > .field-input input:focus {
border: none;
}
form .row.field.dropdown > .field-input .ui-select-match {
border: 0 grey solid;
border-width: 0 0 2px 0;
border-radius: 5px;
}
form .dropdown > .field-input .ui-select-choices-row-inner {
border-radius: 3px;

View file

@ -1,7 +1,7 @@
'use strict';
angular.module('forms').directive('fieldIconDirective', function() {
return {
template: '<i class="{{typeIcon}}"></i>',
restrict: 'E',
@ -28,6 +28,6 @@ angular.module('forms').directive('fieldIconDirective', function() {
'number': 'fa fa-slack'
};
$scope.typeIcon = iconTypeMap[$scope.typeName];
},
}
};
});
});

View file

@ -13,9 +13,24 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
var getTemplateUrl = function(fieldType) {
var type = fieldType;
var templateUrl = 'modules/forms/base/views/directiveViews/field/';
if (__indexOf.call(supportedFields, type) >= 0) {
var supported_fields = [
'textfield',
'textarea',
'date',
'dropdown',
'hidden',
'password',
'radio',
'legal',
'statement',
'rating',
'yes_no',
'number',
'natural'
];
if (__indexOf.call(supported_fields, type) >= 0) {
var templateUrl = 'modules/forms/views/directiveViews/field/';
templateUrl = templateUrl+type+'.html';
}
return $templateCache.get(templateUrl);
@ -24,7 +39,7 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
return {
template: '<div>{{field.title}}</div>',
restrict: 'E',
scope: {
scope: {
field: '=',
required: '&',
design: '=',
@ -85,7 +100,7 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
}
var template = getTemplateUrl(fieldType);
element.html(template).show();
$compile(element.contents())(scope);
var output = $compile(element.contents())(scope);
}
};
}]);

View file

@ -1,12 +1,18 @@
'use strict';
//TODO: DAVID: Need to refactor this
angular.module('forms').directive('onEnterKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if(keyCode === 13 && !event.shiftKey) {
var onEnterKeyDisabled = false;
if($attrs.onEnterKeyDisabled !== null) onEnterKeyDisabled = $attrs.onEnterKeyDisabled;
if(keyCode === 13 && !event.shiftKey && !onEnterKeyDisabled) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onEnterKey);
@ -15,4 +21,56 @@ angular.module('forms').directive('onEnterKey', ['$rootScope', function($rootSco
});
}
};
}]).directive('onTabKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && !event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabKey);
});
}
});
}
};
}]).directive('onEnterOrTabKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if((keyCode === 13 || keyCode === 9) && !event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onEnterOrTabKey);
});
}
});
}
};
}]).directive('onTabAndShiftKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabAndShiftKey);
});
}
});
}
};
}]);

View file

@ -4,7 +4,7 @@ angular.module('forms').directive('onFinishRender', function ($rootScope, $timeo
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Don't do anything if we don't have a ng-repeat on the current element
if(!element.attr('ng-repeat') && !element.attr('data-ng-repeat')){
return;

View file

@ -1,8 +1,8 @@
'use strict';
angular.module('forms').directive('submitFormDirective',
['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth',
function ($http, TimeCounter, $filter, $rootScope, Auth) {
angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth', 'SendVisitorData',
function ($http, TimeCounter, $filter, $rootScope, Auth, SendVisitorData) {
return {
templateUrl: 'modules/forms/base/views/directiveViews/form/submit-form.client.view.html', restrict: 'E',
scope: {
@ -49,6 +49,7 @@ angular.module('forms').directive('submitFormDirective',
TimeCounter.restartClock();
};
//Fire event when window is scrolled
$window.onscroll = function(){
$scope.scrollPos = document.body.scrollTop || document.documentElement.scrollTop || 0;
var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect();
@ -88,13 +89,22 @@ angular.module('forms').directive('submitFormDirective',
}
};
$rootScope.setDropdownOption = function(){
console.log('setDropdownOption index: ');
};
/*
** Field Controls
*/
var 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 === 'submit_field') {
return $scope.myform.form_fields.length - 1;
} else {
return $scope.selected.index;
}
};
$scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) {
if($scope.selected === null || $scope.selected._id === field_id){
//console.log('not scrolling');
@ -121,12 +131,15 @@ angular.module('forms').directive('submitFormDirective',
$document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() {
$scope.noscroll = false;
setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn')[0]) {
//console.log(document.querySelectorAll('.activeField .focusOn')[0]);
if (document.querySelectorAll('.activeField .focusOn').length) {
//Handle default case
document.querySelectorAll('.activeField .focusOn')[0].focus();
} else {
//console.log(document.querySelectorAll('.activeField input')[0]);
} 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();
}
});
});
@ -134,13 +147,15 @@ angular.module('forms').directive('submitFormDirective',
}else {
setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn')[0]) {
//console.log(document.querySelectorAll('.activeField .focusOn')[0]);
//FIXME: DAVID: Figure out how to set focus without scroll movement in HTML Dom
document.querySelectorAll('.activeField .focusOn')[0].focus();
} else {
document.querySelectorAll('.activeField input')[0].focus();
}
});
}
SendVisitorData.send($scope.myform, getActiveField(), TimeCounter.getTimeElapsed());
};
$rootScope.nextField = $scope.nextField = function(){
@ -178,30 +193,39 @@ angular.module('forms').directive('submitFormDirective',
}
};
$scope.goToInvalid = function() {
$rootScope.goToInvalid = $scope.goToInvalid = function() {
document.querySelectorAll('.ng-invalid.focusOn')[0].focus();
};
$scope.submitForm = function() {
$rootScope.submitForm = $scope.submitForm = function() {
var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true;
var form = _.cloneDeep($scope.myform);
form.timeElapsed = _timeElapsed;
form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100;
delete form.visible_form_fields;
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, headers) {
//console.log('form submitted successfully');
console.log($scope.myform.form_fields[0]);
$scope.myform.submitted = true;
$scope.loading = false;
SendVisitorData.send($scope.myform, getActiveField(), _timeElapsed);
})
.error(function (error) {
$scope.loading = false;
//console.log(error);
console.error(error);
$scope.error = error.message;
});
}, 500);

View file

@ -8,7 +8,7 @@ angular.module('forms').factory('Forms', ['$resource', 'FORM_URL',
}, {
'query' : {
method: 'GET',
isArray: true,
isArray: true
//DAVID: TODO: Do we really need to get visible_form_fields for a Query?
// transformResponse: function(data, header) {
// var forms = angular.fromJson(data);
@ -24,8 +24,8 @@ angular.module('forms').factory('Forms', ['$resource', 'FORM_URL',
method: 'GET',
transformResponse: function(data, header) {
var form = angular.fromJson(data);
//console.log(form);
form.visible_form_fields = _.filter(form.form_fields, function(field){
form.visible_form_fields = _.filter(form.form_fields, function(field){
return (field.deletePreserved === false);
});
return form;

View file

@ -2,22 +2,29 @@
angular.module('forms').service('TimeCounter', [
function(){
var _startTime, _endTime, that=this;
var _startTime, _endTime = null, that=this;
this.timeSpent = 0;
this.restartClock = function(){
_startTime = Date.now();
_endTime = _startTime;
_endTime = null;
// console.log('Clock Started');
};
this.getTimeElapsed = function(){
if(_startTime) {
return Math.abs(Date.now().valueOf() - _startTime.valueOf()) / 1000;
}
};
this.stopClock = function(){
if(_startTime){
if(_startTime && _endTime === null){
_endTime = Date.now();
that.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000;
// console.log('Clock Ended');
return that.timeSpent;
this.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000;
this._startTime = this._endTime = null;
return this.timeSpent;
}else{
return new Error('Clock has not been started');
}
@ -28,4 +35,4 @@ angular.module('forms').service('TimeCounter', [
};
}
]);
]);

View file

@ -9,11 +9,13 @@
{{field.title}}
<span class="required-error" ng-show="!field.required && !field.fieldValue">{{ 'OPTIONAL' | translate }}</span>
</h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">
<div class="control-group input-append">
<input ng-focus="setActiveField(field._id, index, true)"
class="focusOn"
<input class="focusOn"
ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-class="{ 'no-border': !!field.fieldValue }"
ui-date="dateOptions"
@ -22,7 +24,9 @@
ng-required="field.required"
ng-disabled="field.disabled"
placeholder="MM/DD/YYYY"
on-enter-key="nextField()"
ng-focus="setActiveField(field._id, index, true)"
on-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-change="$root.nextField()">
</div>
</div>

View file

@ -1,5 +1,4 @@
<div class="field row dropdown"
ng-click="setActiveField(field._id, index, true)"
ng-if="field.fieldOptions.length > 0">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3>
@ -10,15 +9,22 @@
{{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">
<ui-select ng-model="field.fieldValue"
theme="selectize"
search-enabled="true"
search-by="option_value"
set-search-to-answer="true"
ng-required="field.required"
ng-disabled="field.disabled"
on-tab-and-shift-key="prevField()"
on-tab-key="nextField()"
ng-change="$root.nextField()">
<ui-select-match placeholder="Type or select an option">
{{$select.selected.option_value}}
</ui-select-match>
<ui-select-choices repeat="option in field.fieldOptions | filter: $select.search"
ng-class="{'active': option.option_value === field.fieldValue }">

View file

@ -1,5 +1,5 @@
<div class="field row radio legal"
on-enter-key="nextField()"
on-enter-or-tab-key="nextField()"
key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3>
@ -11,10 +11,12 @@
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3>
<br>
<p style="color:#ddd;">{{field.description}}</p>
<p class="col-xs-12">{{field.description}}</p>
</div>
<div class="col-xs-12 field-input container">
<div class="row-fluid">
<div class="row-fluid"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()">
<label class="btn col-md-5 col-xs-12"
ng-class="{activeBtn: field.fieldValue == 'true'}">
<input class="focusOn"

View file

@ -1,5 +1,5 @@
<div class="field row radio"
on-enter-key="nextField()"
on-enter-or-tab-key="nextField()"
key-to-option field="field"
ng-if="field.fieldOptions.length > 0">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
@ -11,6 +11,9 @@
{{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">

View file

@ -1,5 +1,5 @@
<div class="textfield field row"
on-enter-key="nextField()">
on-enter-or-tab-key="nextField()">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3>
<small class="field-number">
@ -8,19 +8,27 @@
</small>
{{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">
<input-stars max="5"
<input-stars max="{{field.ratingOptions.steps}}"
ng-init="field.fieldValue = 1"
on-star-click="$root.nextField()"
icon-full="fa-star"
icon-full="{{field.ratingOptions.shape}}"
icon-base="fa fa-3x"
icon-empty="fa-star-o"
icon-empty="{{field.ratingOptions.shape}}"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-disabled="field.disabled"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-focus="setActiveField(field._id, index, true)"
class="angular-input-stars focusOn">
</input-stars>
</div>

View file

@ -1,9 +1,13 @@
<div class="statement field row"
on-enter-key="$root.nextField()"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-focus="setActiveField(field._id, index, true)">
<div class="row field-title field-title">
<div class="col-xs-1"><i class="fa fa-quote-left fa-1"></i></div>
<h2 class="text-left col-xs-9">{{field.title}}</h2>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="row field-title field-input">
<p class="col-xs-12" ng-if="field.description.length">{{field.description}} </p>

View file

@ -1,5 +1,4 @@
<div class="field row" ng-click="setActiveField(field._id, index, true)"
ng-focus="setActiveField(field._id, index, true)">
<div class="field row" ng-click="setActiveField(field._id, index, true)">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3>
<small class="field-number">
@ -10,8 +9,12 @@
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3>
<small>{{ 'NEWLINE' | translate }}</small>
<p>
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">
<small style="font-size:0.6em;">Press SHIFT+ENTER to add a newline</small>
<textarea class="textarea focusOn" type="text"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
@ -19,14 +22,16 @@
value="{{field.fieldValue}}"
ng-required="field.required"
ng-disabled="field.disabled"
ng-focus="setActiveField(field._id, index, true)"
on-enter-key="nextField()">
ng-focus="setActiveField(field._id, index, true)"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
style="border: none; border-left: lightgrey dashed 2px;">
</textarea>
</div>
</div>
<div>
<div class="btn btn-lg btn-default col-xs-12 col-sm-4"
<div class="btn btn-lg btn-default col-xs-12 col-sm-4 hidden-xs"
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"

View file

@ -1,7 +1,7 @@
<div class="textfield field row"
ng-click="setActiveField(field._id, index, true)">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3>
<div class="col-xs-12 field-title row-fluid" ng-style="{'color': design.colors.questionColor}">
<h3 class="col-xs-12">
<small class="field-number">
{{index+1}}
<i class="fa fa-angle-double-right" aria-hidden="true"></i>
@ -13,10 +13,13 @@
({{ 'OPTIONAL' | translate }})
</span>
</h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div>
<div class="col-xs-12 field-input">
<input ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-focus="setActiveField(field._id, index, true)"
name="{{field.fieldType}}{{index}}"
type="{{field.input_type}}"
ng-pattern="field.validateRegex"
@ -26,10 +29,15 @@
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
value="field.fieldValue"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
value="field.fieldValue"
ng-focus="setActiveField(field._id, index, true)"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-required="field.required"
ng-disabled="field.disabled"
aria-describedby="inputError2Status"
on-enter-key="nextField()">
aria-describedby="inputError2Status">
</div>
<div class="col-xs-12">
<div ng-show="forms.myForm.{{field.fieldType}}{{index}}.$invalid && !!forms.myForm.{{field.fieldType}}{{index}}.$viewValue " class="alert alert-danger" role="alert">
@ -42,7 +50,7 @@
</div>
</div>
<div>
<div class="btn btn-lg btn-default col-xs-12 col-sm-4"
<div class="btn btn-lg btn-default col-xs-12 col-sm-4 hidden-xs"
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"

View file

@ -1,6 +1,6 @@
<div class="field row radio"
ng-click="setActiveField(field._id, index, true)"
on-enter-key="nextField()"
on-tab-and-shift-key="prevField()"
key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3 class="row">
@ -25,10 +25,10 @@
<input type="radio" value="true"
class="focusOn"
style="opacity: 0; margin-left: 0px;"
ng-focus="setActiveField(field._id, index, true)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-focus="setActiveField(field._id, index, true)"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="$root.nextField()"
ng-disabled="field.disabled" />
<div class="letter">
@ -45,10 +45,9 @@
<input type="radio" value="false"
style="opacity:0; margin-left:0px;"
ng-focus="setActiveField(field._id, index, true)"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="$root.nextField()"
ng-disabled="field.disabled"/>

View file

@ -53,6 +53,7 @@ ng-style="{'color':button.color}">
data-id="{{field._id}}"
ng-class="{activeField: selected._id == field._id }"
class="row field-directive">
<field-directive field="field" design="myform.design" index="$index" forms="forms">
</field-directive>
</div>
@ -76,8 +77,10 @@ ng-style="{'color':button.color}">
<button ng-if="!forms.myForm.$invalid"
class="Button btn col-sm-2 col-xs-8 focusOn"
v-busy="loading" v-busy-label="Please wait" v-pressable
ng-disabled="loading"
ng-disabled="loading || forms.myForm.$invalid"
ng-click="submitForm()"
on-enter-key="submitForm()"
on-enter-key-disabled="loading || forms.myForm.$invalid"
ng-style="{'background-color':myform.design.colors.buttonColor, 'color':myform.design.colors.buttonTextColor}"
style="font-size: 1.6em; margin-left: 1em; margin-top: 1em;">
@ -85,8 +88,10 @@ ng-style="{'color':button.color}">
</button>
<button ng-if="forms.myForm.$invalid"
class="Button btn col-sm-2 col-xs-8"
class="Button btn col-sm-2 col-xs-8 focusOn"
ng-click="goToInvalid()"
on-enter-key="goToInvalid()"
on-enter-key-disabled="!forms.myForm.$invalid"
style="font-size: 1.6em; margin-left: 1em; margin-top: 1em; background-color:#990000; color:white">
{{ 'REVIEW' | translate }}
</button>

View file

@ -1,4 +1,17 @@
<section class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<submit-form-directive myform="myform"></submit-form-directive>
</section>
<!-- User's Google Analytics -->
<script ng-if="myform.analytics.gaCode">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{myform.analytics.gaCode}}', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

View file

@ -0,0 +1,44 @@
(function () {
'use strict';
// Create the SendVisitorData service
angular
.module('forms')
.factory('SendVisitorData', SendVisitorData);
SendVisitorData.$inject = ['Socket', '$state'];
function SendVisitorData(Socket, $state) {
// Create a controller method for sending visitor data
function send(form, lastActiveIndex, timeElapsed) {
console.log(lastActiveIndex);
// Create a new message object
var visitorData = {
referrer: document.referrer,
isSubmitted: form.submitted,
formId: form._id,
lastActiveField: form.form_fields[lastActiveIndex]._id,
timeElapsed: timeElapsed
};
Socket.emit('form-visitor-data', visitorData);
}
function init(){
// Make sure the Socket is connected
if (!Socket.socket) {
Socket.connect();
}
}
var service = {
send: send
};
init();
return service;
}
}());

View file

@ -68,7 +68,7 @@ angular.module('NodeForm.templates', []).run(['$templateCache', function($templa
"<div class=\"field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><textarea class=textarea type=text ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-class=\"{ 'no-border': !!field.fieldValue }\" value={{field.fieldValue}} class=focusOn ng-required=field.required ng-disabled=field.disabled ng-focus=\"setActiveField(field._id, index, true)\">\n" +
" </textarea></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=!field.fieldValue ng-click=$root.nextField() ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>");
$templateCache.put("../public/modules/forms/views/directiveViews/field/textfield.html",
"<div class=\"textfield field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><input ng-style=\"{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}\" ng-focus=\"setActiveField(field._id, index, true)\" name={{field.fieldType}}{{index}} type={{field.input_type}} placeholder={{field.placeholder}} ng-class=\"{ 'no-border': !!field.fieldValue }\" class=\"focusOn text-field-input\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" value=field.fieldValue ng-required=field.required ng-disabled=field.disabled aria-describedby=inputError2Status on-enter-key=nextField()></div><div class=col-xs-12><div ng-show=forms.myForm.{{field.fieldType}}{{index}}.$invalid class=\"alert alert-danger\" role=alert><span class=\"glyphicon glyphicon-exclamation-sign\" aria-hidden=true></span> <span class=sr-only>Error:</span> Enter a valid email address</div></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=\"!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid\" ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" ng-click=$root.nextField() class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>");
"<div class=\"textfield field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><input ng-style=\"{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}\" ng-focus=\"setActiveField(field._id, index, true)\" name={{field.fieldType}}{{index}} type={{field.input_type}} placeholder={{field.placeholder}} ng-class=\"{ 'no-border': !!field.fieldValue }\" class=\"focusOn text-field-input\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" value=field.fieldValue ng-required=field.required ng-disabled=field.disabled aria-describedby=inputError2Status on-enter-or-tab-key=nextField()></div><div class=col-xs-12><div ng-show=forms.myForm.{{field.fieldType}}{{index}}.$invalid class=\"alert alert-danger\" role=alert><span class=\"glyphicon glyphicon-exclamation-sign\" aria-hidden=true></span> <span class=sr-only>Error:</span> Enter a valid email address</div></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=\"!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid\" ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" ng-click=$root.nextField() class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>");
$templateCache.put("../public/modules/forms/views/directiveViews/field/yes_no.html",
"<div class=\"field row radio\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3 class=row><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3><p class=row>{{field.description}}</p></div><div class=\"col-xs-12 field-input\"><div class=row><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=true class=focusOn style=\"opacity: 0; margin-left: 0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>Y</div><span>Yes</span> <i ng-show=\"field.fieldValue === 'true'\" class=\"fa fa-check\" aria-hidden=true></i></label></div><div class=row style=\"margin-top: 10px\"><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=false style=\"opacity:0; margin-left:0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>N</div><span>No</span> <i ng-show=\"field.fieldValue === 'false'\" class=\"fa fa-check\" aria-hidden=true></i></label></div></div></div><br>");
$templateCache.put("../public/modules/forms/views/directiveViews/form/configure-form.client.view.html",

View file

@ -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!'));
}
});
}

View file

@ -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);
});